views:

179

answers:

3

Have Addresses and Lists with many-to-many relationship, as shown below.

Sometimes need all the Lists an Address is not in.

Using the find_by_sql query shown, and it works great. But is there a way to do it without using direct SQL?

class List
  has_many :address_list_memberships
  has_many :addresses, :through => :address_list_memberships
end


class Address
  has_many :address_list_memberships, :dependent => :destroy
  has_many :lists, :through => :address_list_memberships

  # Lists that this Address is not in
  def Address.lists_not_in(address_id)
    sql = %Q|
SELECT
  l.*
FROM
  lists l
WHERE
  l.id
NOT IN
(
  SELECT
    l.id
  FROM
    addresses a, lists l, address_list_memberships alm
  WHERE
    a.id = alm.address_id AND l.id = alm.list_id
  AND
    a.id = #{address_id}
)
|
    List.find_by_sql(sql)
  end
end
+3  A: 

I would do this as a scope in List

class List
  named_scope :without_address, lambda { |address_id| { :joins => 'inner join address_list_memberships alm on alm.list_id = lists.id', :conditions => ['alm.address_id <> ?', address_id]}}
end

Now you can call List.without_address(4), and you can call scopes on top of that.

As Matchu points out, you can do it without writing out the join SQL:

class List
  named_scope :without_address, lambda { |address_id| { :joins => :address_list_memberships, :conditions => ['address_list_memberships.address_id <> ?', address_id]}}
end

And make sure your join table has indices!

In a migration:

add_index "address_list_memberships", "address_id"
add_index "address_list_memberships", "list_id"

For other ways you can format the named_scope, see Sam Saffron's gist: http://gist.github.com/162489

Michael Sofaer
Do you need to write out the join explicitly, or can Rails write it in itself if you just substitute :address_list_memberships? Or am I missing something here?
Matchu
And is allowing infinite args and only using the first a style thing, or am I again totally overlooking something? My first thought would be to just do lambda { |address| }
Matchu
Rails API seems to say so, as long as it's just a basic association. And since a list has_many address_list_memberships, it oughta work fine. (http://api.rubyonrails.org/classes/ActiveRecord/Base.html#M002263)
Matchu
Uhhhh, comment delete? My last comment was in the context of :joins taking association names as parameters.
Matchu
Yeah, one arg is better. I tend to write out my joins by hand, since I often use left outer joins, and I like to be able to control behavior when joining the same table twice, but not a big deal, I think.
Michael Sofaer
Sounds like the sort of thing to try the default behavior since it just generally comes out as nicer code, then explicitly fix things when fixing becomes necessary. I'm hoping someone comes up with a solution to the whole multiple-join table-name-conflict deal :x
Matchu
@Michael, I think this reads a bit better if you pull that lambda out to its own function, that line is way too long imho
Sam Saffron
@Sam I've never written a named_scope with a non-anonymous block, I didn't know you could. Can you link to an example?
Michael Sofaer
@Michael see: http://gist.github.com/162489
Sam Saffron
Cool, thanks, Sam!
Michael Sofaer
+1  A: 
WHERE (address_list_memberships.address_id <> 13896)

is going to be expensive on a database with 21849 Addresses and 1483 Lists.

Flip your logic:

def lists_not_in
  List.all - self.lists
end

That way you are only subtracting one array from another instead of checking each record in the database to see if it's in the list.

askegg
+1  A: 

You are not going to get the flexibility you get with direct SQL from ActiveRecord, in particular, it is not going to be possible for you to craft the not in clause in active record.

If you want to get a little bit more control you could try using Sequel http://sequel.rubyforge.org/ or just hand crafting.

Note, the solution you have is risky cause you are allowing for a sql injection. (a.id = #{address_id})

Sam Saffron