Rafał Jaroszewicz

Specifying a polymorphic has_many through: association in Rails

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.

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


: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.

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.

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

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

irb(main):004:0> sale = Property::Sale.last  

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

irb(main):006:0> sale.clients << client

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.

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!

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top