Specifying a polymorphic has_many through: association in Rails
Tags: rails, ruby, webdev, programming
Published: 2022-12-14 23:10:36

In my recent project that I'm working on I faced this problem where I have a `Client` model and two different models for properties, `Sale` and `Rental`. To avoid having multiple tables for `ClientSales` and `ClientRentals` I wanted to just have `ClientProperties`, and for that I need a polymorphic `has_many through:`

In Ruby on Rails, the `has_many :through` association is a way of creating a many-to-many connection with an extra model. This association is often used to create a join table that connects two other tables.

In my example I have three models a `Property::Rental`, `Property::Sale` model and a `Customer::Client` model. A rental can have multiple clients(tenants), and a client can buy multiple properties. To create this association, we need to create a join table called `client_properties` that has foreign keys for the client, and property. Since we want to only use one model called `Property::ClientProperty` for both of these, we need a polymorphic association.

First, we our table for `property_client_properties`.

```ruby
class CreatePropertyClientProperties < ActiveRecord::Migration[7.0]
  def change
    create_table :property_client_properties do |t|
      t.references :client
      t.integer :property_id
      t.string :property_type

      t.timestamps
    end
  end
end
```

`:property_type` column will be used to store which model we are actually referring to when passing an id. Let's run `rails db:migrate`.

Now in our `Property::ClientProperty` we only need a few adjustments.

```ruby
# app/models/property/client_property.rb

belongs_to :client, class_name: 'Customer::Client'
belongs_to :property, polymorphic: true
```

Now that we have a polymorphic `:property` association, we can refer to this model outside of it as `:property`. Here's how we can use this association with one of our models, for example `Property::Sale`.

```ruby
# app/models/property/sale.rb

has_many :client_properties, as: :property
has_many :clients, through: :client_properties
```

With this setup we can now add a `client` to `sale`.

```irb
irb(main):004:0> sale = Property::Sale.last  
=>                                               
#<Property::Sale:0x0000000109bdd738>

irb(main):005:0> client = Customer::Client.last
=>                                
#<Customer::Client:0x0000000118086308>

irb(main):006:0> sale.clients << client
=>                                
[#<Customer::Client:0x0000000118086308, <Customer::Client:0x0000000107454018]
```

Now, two things are happening when we append another client to this association. One is creating our `Property::ClientProperty` record by running INSERT and two, returning a relation with this new record already in it. We can access clients for this sale using normal association methods like `sale.clients`.

To finish this up, we want it to be bidirectional, so let's add the rest of the logic on the client side.

```ruby
# app/models/customer/client.rb

has_many :client_properties, class_name: 'Property::ClientProperty',
dependent: :destroy

has_many :sales, through: :client_properties, source: :property,
source_type: 'Property::Sale', class_name: 'Property::Sale'

has_many :rentals, through: :client_properties, source: :property,
source_type: 'Property::Rental', class_name: 'Property::Rental'
```
Here, we specify the source_type which is exactly what is going to be inserted into our `property_type` column.
And that's it!