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?
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!