Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the purpose of :conditions on a belongs_to association?

Say I have the following association with an attached condition:

belongs_to :admin_user, 
    :class_name => 'User', 
    :foreign_key => :admin_user_id, 
    :conditions=> 'users.admin=TRUE' # or any variation with hash or array, {:admin => true}, etc.

The API doc states that the :conditions option on belongs_to will:

Specify the conditions that the associated object must meet in order to be included as a WHERE SQL fragment, such as authorized = 1.

But the output shows no WHERE clause on the select, and in any case I would expect that conditions like this on a belongs_to would prevent persisting that relationship to begin with, on the INSERT not the SELECT. This option seems to have no effect on a belongs_to association, unless I'm missing something. The option makes sense on a has_many, I just don't see how it applies to belongs_to.

EDIT: Further research reveals that you can indeed persist an association that violates a condition, but you cannot retrieve the associated record after the record is reloaded.

On a class defined like so:

class Widget < ActiveRecord::Base

    belongs_to :big_bloop, 
        :class_name => "Bloop", 
        :foreign_key => :big_bloop_id, 
        :conditions => ["big_bloop = ?", true]

    belongs_to :bloop, :conditions => ["big_bloop = ?", true]

end

...from the console we see:

>> bloop = Bloop.new
=> #<Bloop id: nil, name: nil, big_bloop: nil>
>> widget = Widget.new
=> #<Widget id: nil, name: nil, bloop_id: nil, big_bloop_id: nil>
>> widget.bloop = bloop
=> #<Bloop id: nil, name: nil, big_bloop: nil>
>> widget.save!
=> true
>> widget
=> #<Widget id: 2, name: nil, bloop_id: 2, big_bloop_id: nil>

I've associated a bloop that violates the condition and saved it. The association is persisted to the db (see bloop_id and big_bloop_id on the last line above).

>> big_bloop = Bloop.new
=> #<Bloop id: nil, name: nil, big_bloop: nil>
>> widget.big_bloop = big_bloop
=> #<Bloop id: nil, name: nil, big_bloop: nil>
>> widget.save!
=> true
>> widget
=> #<Widget id: 2, name: nil, bloop_id: 2, big_bloop_id: 3>

Same thing, different attribute.

>> widget.bloop
=> #<Bloop id: 2, name: nil, big_bloop: nil>
>> widget.big_bloop
=> #<Bloop id: 3, name: nil, big_bloop: nil>

Both invalid bloops remain in memory.

>> widget.reload
=> #<Widget id: 2, name: nil, bloop_id: 2, big_bloop_id: 3>
>> widget.bloop
=> nil
>> widget.big_bloop
=> nil

After the reload, they're gone, because the SELECT statement does indeed use a WHERE clause to exclude them.

Bloop Load (0.3ms)   SELECT * FROM `bloops` WHERE (`bloops`.`id` = 2 AND (big_bloop = 1)) 

And yet the widget still has the references:

>> widget
=> #<Widget id: 2, name: nil, bloop_id: 2, big_bloop_id: 3>

Seems odd to me, but there you go.

like image 217
Dave Sims Avatar asked Jan 28 '10 17:01

Dave Sims


1 Answers

That's a nice find!

My first thought was that this might just be some generic thing of the AssociationProxy base class or the like. But digging further down, there appears to be a list of specific options belongs_to permits:

@@valid_keys_for_belongs_to_association = [
  :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions,
  :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly,
  :validate, :touch
]

So at some point, the possibly subconscious decision was made to put that there. :)

I'm not sure how you tested the WHERE, though. My testing clearly shows it does include the WHERE-clause:

class Thing < ActiveRecord::Base; end

class Widget < ActiveRecord::Base
  belongs_to :thing, :conditions => ['name = ?', 'Jigglymabob']
end

Thing.create :name => 'Jigglymabob'
# => #<Thing id: 1, name: "Jigglymabob">
w = Widget.create :name => 'Wookeleywoo', :thing_id => 1
# => #<Widget id: 1, name: "Wookeleywoo", thing_id: 1>
w.thing
# => #<Thing id: 1, name: "Jigglymabob">

After all this, my logfile contains:

Thing Create (0.3ms)   INSERT INTO "things" ("name") VALUES('Jigglymabob')
Widget Create (0.3ms)   INSERT INTO "widgets" ("name", "thing_id") VALUES('Wookeleywoo', 1)
Thing Load (0.6ms)   SELECT * FROM "things" WHERE ("things"."id" = 1 AND (name = 'Jigglymabob'))

As I'm trying and typing this out for you, I realized I still haven't given a real answer to your question. :) I can think of only one reason to have this in ActiveRecord, and that's because it was no extra hassle to implement, and there's no benefit to leaving it out.

Someone out there may be working on a strange edge case with a legacy database, where the golden rule in the office—that everyone learns the hard way—is to never have a Wookeleywoo widget attached to anything other than a Jigglymabob.

like image 186
Stéphan Kochen Avatar answered Sep 29 '22 12:09

Stéphan Kochen