[LRUG] Headache with AR & fields_for

Tim Ruffles tim at cothink.co.uk
Wed Jul 28 01:35:56 PDT 2010


Thanks for taking the time to look through that Tom - your suggested  
solution works but I'm still intrigued about AR's behaviour here. I  
think I've hit this problem because I'm using some SQL in the join to  
let me use AR the way I was using Doctrine, a PHP ORM based on  
Hibernate, where I'd set up the whole graph I wanted to retrieve in  
the controller and only dumb hashes would be passed to the view.

> I don't completely understand what you're trying to do here, but  
> this is exactly what I'd expect to happen.

I'm trying to load a graph that represents the current state of all  
associations with the conditions specified: eg when I call step.todos,  
it reflects the filtered state and will return an empty array if no  
todos were found in the original query, even if there are other todos  
out there.

> ActiveRecord instances don't have any memory of how you found them.  
> I think of Recipe.with_todos_for(User.find(45)).find(1) as returning  
> an object in exactly the same state as the object you get from just  
> Recipe.find(1) -- the named scope doesn't contribute anything to the  
> final object you get back, it just affects whether that object is  
> found or not (i.e. the find will raise ActiveRecord::RecordNotFound  
> if recipe 1 doesn't have any todos for user 45). So yes, naturally,  
> Recipe.find(1).steps will find all steps for that recipe, and  
> Recipe.find(1).steps.first.todos will find all todos for the first  
> step; it doesn't matter how you found that Recipe instance.

I think the problem is coming from my use of raw SQL in the join -  
when you say "(i.e. the find will raise ActiveRecord::RecordNotFound  
if recipe 1 doesn't have any todos for user 45)" you'd be totally  
correct were I using hash conditions, but I used raw SQL.

The thing that was confusing me is that the results of a filtered  
association *are* cached when at least one object is returned (which  
will, as you say, always be the case if I were using AR  
hash :include/:joins). This gives me the expected behaviour: when I  
specify a condition for finding an association, I can use the  
association knowing that only the filtered subset will be present.

The below has verbose logging so you can see when SQL is fired, and  
shows how I can use the filtered subset without additional loading,  
and only when a.steps(true) is used to ignore the cache is the whole,  
unfiltered array queried for and returned.

  > a =  Recipe.find(1, :include => :steps, :conditions => {:steps =>  
{:id => 33}})
   Recipe Load Including Associations (0.2ms)   SELECT "recipes"."id"  
AS t0_r0, "r...

 > a.steps.empty?
  => false

  > a.steps
  => [#<Step id: 33, text: "step 1", url: "",  ... >]

  > a.steps.length
  => 1

 > a.steps(true)
   Step Load (3.7ms)   SELECT * FROM "steps" WHERE ("steps".recipe_id  
= 1)
  => [#<Step id: 33, t...>, #<Step id: 34...>, #<Step id: 35, t...>]

ruby-1.8.7-p174 > a.steps.length
  => 3

However when I artificially created a case where an object could be  
returned even when no associated records were found, the above isn't  
true. The raw SQL :joins that has brought me to grief is ':joins =>  
'LEFT OUTER JOIN todos ON todos.step_id = steps.id AND todos.user_id =  
45', which means that Recipe and Step objects will be found regardless  
of whether any Todo rows can be found as the condition is in the ON  
clause. eg:

 > a =  Recipe.find(1, :joins => 'LEFT OUTER JOIN steps ON  
steps.recipe_id = recipes.id AND steps.id = 66')
=> ...
 > a.steps.length
   Step Load (1.8ms)   SELECT * FROM "steps" WHERE ("steps".recipe_id  
= 1)

Since the condition has returned no associated rows (I specified a  
Step id that doesn't exist), there is nothing cached to prevent the  
SELECT * occurring when I call 'a.steps.length'

Since Rails doesn't let you specify this kind of join (for exactly for  
this reason?), I shouldn't expect to be able to do what I was hoping  
to do. If I were using just the Rails API, I'd never have a case where  
a join/include is specified but no rows are returned, so any  
associations would always reflect the conditions they were found with.

So you've solved my problem there, and trying to load in the whole  
graph when it seems to cause so many problems is not the pragmatic/ 
Rails way to go : ) I've added more nested scopes as you suggested so  
the whole thing is still nicely dry:

form_for(@recipe)
	...
		fields_for(:steps)
			...
				fields_for(:todos, step.todos.for(current_user))

Thanks!



More information about the Chat mailing list