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
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.
# 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
.
# 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(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.
# 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!
Top comments (0)