Problem with find condition and habtm relationship.

Ok. This has been troubling me for some time now. Please bare with me
while I explain the situation to make things clear.

I'm making a restaurant directory in RoR. I have a table of Restaurants
and a table of Features which both have a habtm relationship with each
other using a jointable called features_restaurants which has the
fields feature_id and restaurant_id.

So restaurants can belong to many different features such as
"vegitarian menu", "child friendly" etc. In the view checkboxes are
dynamically created for each feature using the following code:

<%= start_form_tag :action => 'by_feature' %>
  <% for feature in @features %>
    <input type="checkbox"
           id="<%= feature.id %>"
           name="feature_ids[]"
           value="<%= feature.id %>"
    > <%= feature.name %><br />
  <% end %>
  <%= submit_tag "Sort" %>

What I want to do is display only those restaurants that contain ALL
the features checked by the user. So if the user checks the boxes
"vegitarian menu" and "child friendly" the only restaurants that should
show are those which belong to BOTH of those features.

I was told that the following code would work:

Restaurant.find(:all, :conditions => [ feature_ids.map { |f| '
feature_id = ' + f.to_s }.join(' AND ') ], :include => :features)

But this doesn't show anything when more than one checkbox is checked.
I've tried so many different solutions and am getting frustrated.
Thanks for reading all of this!

Someone must know :frowning: This must have been done several times in other
applications.

Ok. This has been troubling me for some time now. Please bare with me
while I explain the situation to make things clear.

I'm making a restaurant directory in RoR. I have a table of Restaurants
and a table of Features which both have a habtm relationship with each
other using a jointable called features_restaurants which has the
fields feature_id and restaurant_id.

So restaurants can belong to many different features such as
"vegitarian menu", "child friendly" etc. In the view checkboxes are
dynamically created for each feature using the following code:

<%= start_form_tag :action => 'by_feature' %>
  <% for feature in @features %>
    <input type="checkbox"
           id="<%= feature.id %>"
           name="feature_ids[]"
           value="<%= feature.id %>"
    > <%= feature.name %><br />
  <% end %>
  <%= submit_tag "Sort" %>

What I want to do is display only those restaurants that contain ALL
the features checked by the user. So if the user checks the boxes
"vegitarian menu" and "child friendly" the only restaurants that should
show are those which belong to BOTH of those features.

I was told that the following code would work:

Restaurant.find(:all, :conditions => [ feature_ids.map { |f| '
feature_id = ' + f.to_s }.join(' AND ') ], :include => :features)

But this doesn't show anything when more than one checkbox is checked.
I've tried so many different solutions and am getting frustrated.
Thanks for reading all of this!

OK. This may help, but it will also make your head hurt. The basic problem that you’re running into is that you want a “for all” operator and sql doesn’t have one. Instead, you have to think of the query in terms of the logically equivalent “not exists not”. Effectively, a proposition is true for all x if you can’t find a counter-example in x. In your case, a restaurant has all of the asked-for features if you can’t find one that it doesn’t have. It’s incredibly annoying to try to think of things this way, but it’s what SQL can do.

Let’s assume, to save some typing, your tables are called rests, feats and rests2feats, and you’re looking for restaurants with a set of features (‘cheap’, ‘kid-friendly’). The SQL you want should go something like

select r.rest from rests r where not exists (select * from feats f where f.feat in (‘cheap’, ‘kid-friendly’) and not exists (select * from rests2feats r2f where r.id = r2f.rest_id and f.id = r2f.feat_id))

The key is in the two nested selects. They are looking for a counter-example: a feat in (‘cheap’, ‘kid-friendly’) that your restaurant doesn’t have – ie, where there is no row in the habtm table linking your restaurant and that feature. The first not exists is there to catch those restaurants for which there is no such missing row – and which therefore have all of the features.

I hope that this helps, and that it works for you. This is painful enough that I’m not entirely confident of the suggested code – though I am confident that the solution lies somewhere in that direction. If I could add one more suggestion, where I feel on much surer ground: vegetarian, one i, two e’s

allbests,

John Browning

Hey, thanks very much for the help. Since posting I found my solution:

  def self.find_by_feature(feature_ids, sort)
    return [] if feature_ids.empty?
    feature_ids = feature_ids.flatten
    find(:all, :readonly => false,
         :joins => "INNER JOIN features_restaurants a ON restaurants.id
= a.restaurant_id",
         :conditions => ["a.feature_id IN (?)", feature_ids],
         :group => "restaurants.id HAVING COUNT(restaurants.id) =
#{feature_ids.size}")
  end

Seems to work fine. I think I made an slight error describing what
exactly I wanted to achieve. What this code achieves is displaying
restaurants that include every checked box which I believe is more
desirable behaviour.