Suppose you've got a legacy database, or some other non-Railsy schema situation, and you want to define an association but the tables don't have the standard Active Record foreign key columns. You can accomplish this, to a certain extent, with an association scope that defines a new
where relation. But there are some hitches.
For example, you might have an Order that
has_one CustomLogo, but rather than a foreign key it relies on matching values for several other fields:
client_id. Here's how you could manage that:
I wish I had thought of this technique, but credit goes to Dwight on Stack Overflow. It's a neat way to handle this situation, but it does have a few pitfalls. One issue is that since the scope uses attributes from a specific object instance, we can't eagerly load all the logos for a collection of orders:
The same thing happens with an
includes relation. Active Record will raise this exception regardless of whether the association scope actually uses the argument. That's a pretty reasonable design choice since the alternative is to attempt to do a data flow analysis inside the scope's
You'll also run into problems when using some of the methods generated by a
has_one declaration. If you attempt to assign a record via the association accessor you'll get an exception around the missing foreign key:
Incidentally, to me the
unscope call looks like a complicated part of this scope. But all it does is replace any existing
where clause objects in the relation with nil. And it returns the relation, so we can then define a new
where clause like we do in this example. Notice that this is different than
ActiveRecord::Relation#rewhere, which lets you redefine the
where clause for a particular attribute.
Even more incidentally, how does the association scope only return one record even though the
where clause doesn't constrain the resulting relation? This is a more general
has_one question, and the answer is in
limit(1) explains that.
Finally, there's some interesting behavior around creation helper methods. Using the association helper with no arguments to create a record results in only one
CustomLogo record being created, no matter how many invocations:
This makes sense because Active Record is using the equality Arel nodes in the association scope to create the record. So if the first
Order has an
origin_value of "facebook" and a
client_id of 42, so will the one associated
rescue nil is there because when we call
create_custom_logo after the first record is in place,
ActiveRecord::Associations::Association#set_owner_attributes with an
order_id value in an attempt to set a foreign key. That raises the same
MissingAttributeError exception that we saw earlier, so I'm rescuing that to prevent the script from exiting early.
The interesting thing is what happens if you pass arguments to the creation helper; calling that repeatedly will create a bunch of records:
This goes through a different code path. All record creations, even the first one, call
set_owner_attributes and thus we need to rescue the
MissingAttributeError every time.
Pulling back a little on this, if
client_id form effectively a composite unique key, a good way to prevent odd data would be to add a partial uniqueness constraint on those two columns.
To sum up, you can define associations without foreign keys - but don't be surprised when some things don't work the way they normally do.