Skip to content

Instantly share code, notes, and snippets.

@ajw725
Last active March 19, 2016 15:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ajw725/28980d21b6f0ab351659 to your computer and use it in GitHub Desktop.
Save ajw725/28980d21b6f0ab351659 to your computer and use it in GitHub Desktop.
Linking two ActiveRecord models with multiple different foreign keys

Linking Two ActiveRecord Models with Multiple Foreign Keys

My colleague and I have been working on a project for a client in the real estate industry. The product we're creating is a Rails app that tracks apartment listings, and one of the key features is that our client's agents must be able to create and edit "specials" that modify the prices of existing listings. There are many different types of specials: those that simply award a credit upon move-in; those that are relative to the rent of the property, such as one month free; and those that only apply during a particular date range, either for the signing of the lease or for the actual move-in date.

Any given special can apply to any number of matching listings, but any given listing can only have one special. We want to be able to retrieve the special linked to a particular listing quickly and easily, so this leads to a pretty straightforward association:

class Property < ActiveRecord::Base
  belongs_to :special
end

class Special < ActiveRecord::Base
  has_many :properties
end

We also need a migration, which we can generate with this line:

rails g migration add_special_to_properties special:references

and which will look like this:

class AddSpecialToProperties < ActiveRecord::Migration
  def change
    add_reference :properties, :special, index: true, foreign_key: true
  end
end

This will result in the Property model having a special_id column. For an instance of Special called special, its properties can be retrieved by calling special.properties.

There's a slight complication, though. There's another type of special typically called "look-and-lease," to which I'll refer as L&L. This basically means that the special only applies if you sign a lease within a certain limited time after viewing the property. Aside from this limitation, an L&L has the same attributes as a regular special, so it didn't really make sense to create a separate model. However, it turns out that an L&L can actually apply in addition to a regular special. We therefore needed to change our association, but how? Saying that a Property has_many :specials isn't entirely accurate; it can only have one of each type, and we want to be able to distinguish easily between the two.

We decided the best approach was to have two separate foreign key references in Property: a special_id, for a regular special; and a look_and_lease_id, for an L&L. Since these need to refer to the same model, Our Property class now looks like this:

class Property < ActiveRecord::Base
  belongs_to :special, class_name: 'Special'
  belongs_to :look_and_lease, class_name: 'Special'
end

We also need another migration, which looks exactly the same as before, except with "look_and_lease" instead of "special."

Ok, great! Property now has both special_id and look_and_lease_id, each of which refers to a separate instance of the Special model. Are we done?

The last step

Nope - not quite yet!

Whenever we create, update, or destroy a special, we want to make sure we check all of the matching properties to be sure each is still associated with the best possible special of each type (best regular and best L&L). To accomplish this, we wrote an update_special method in the Property model to check all matching regular and L&L specials and create a reference only to the best instance of each type, and we invoked this method whenever we created, updated, or destroyed a special. The code we've written above works fine for all operations on regular specials, and it also works fine when an L&L special is created. However, depending on how you write your code, you might run into a sneaky little problem when trying to update or destroy L&L specials.

As we've mentioned, when updating or destroying a special, you need to check two distinct (albeit possibly overlapping) groups of properties: those that were linked to the special before the update, and those that match the special after the update. If you try to access the linked properties using the special.properties method, you'll have no problems when dealing with regular specials, but you won't be so lucky with the L&Ls. To illustrate, let's take a look at the raw SQL query that this method produces for an imaginary special called best_deal:

irb(main):019:0> best_deal.properties
  Property Load (0.2ms)  SELECT "properties".* FROM "properties" WHERE "properties"."special_id" = ?  [["special_id", 30]]

It's actually pretty simple: it looks for properties with a special_id matching our best_deal. This is equivalent to Property.where( special_id: best_deal.id ).

This is fine for regular specials, but for L&Ls, we're not using special_id; we're using look_and_lease_id. As a result, best_deal.properties will return an empty collection even when there are matching properties. One solution is simply to use an explicit ActiveRecord query, such as Property.where( look_and_lease_id: best_deal.id ). However, this is a bit of a workaround, and there's a better way to make our code cleaner and clearer.

Remember how we made two separate belongs_to statements in the Property model to describe the two foreign key references? Well, we should really have done the same thing in the Special model to describe two separate has_many associations. Instead of what we've shown above, Special should look like this:

class Special < ActiveRecord::Base
  has_many :properties, class_name: 'Property', foreign_key: 'property_id'
  has_many :ll_properties, class_name: 'Property', foreign_key: 'look_and_lease_id'
end

We can name these associations whatever we want; ll_properties is simply what we've chosen for the L&L association. Now, best_deal.properties will give us the collection of Properties linked via property_id, and best_deal.ll_properties will give us the collection linked via look_and_lease_id. And that's all there is to it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment