10

I want to do a search across 2 tables that have a many-to-one relationship, eg

class User << ActiveRecord::Base
  has_many :pets
end

class Pet << ActiveRecord::Base
  belongs_to :users
end

Now let's say I have some data like so

users

id        name
1         Bob
2         Joe
3         Brian

pets

id        user_id  animal
1         1        cat
2         1        dog
3         2        cat
4         3        dog

What I want to do is create an active record query that will return a user that has both a cat and a dog (i.e. user 1 - Bob).

My attempt at this so far is

User.joins(:pets).where('pets.animal = ? AND pets.animal = ?','dog','cat')

Now I understand why this doesn't work - it's looking for a pet that is both a dog and a cat so returns nothing. I don't know how to modify this to give me the answer I want however. Does anyone have any suggestions? This seems like it should be easy - it doesn't seem like an especially unusual situation.

---edit---

Just adding a little coda to this question as I have just discovered Squeel. This allows you to build a subquery like so;

User.where{id.in(Pet.where{animal == 'Cat'}.select{user_id} & id.in(Pet.where{animal == 'Dog'}.select{user_id}))

This is what will find its way into my app.

brad
  • 9,573
  • 12
  • 62
  • 89

9 Answers9

10

Andomar - Unfortunately, writing the query like that will not necessarily always work as desired. Specifically, having 2 cats will cause the user to show up, and having 3 pets - say, 2 cats and a dog - will cause them to be excluded.
I don't know much about ActiveRecord, but the following is standard SQL syntax that would work:

SELECT users.id
FROM Users
JOIN (SELECT user_id 
      FROM Pets
      WHERE animal IN ('dog', 'cat')
      GROUP BY user_id
      HAVING COUNT(DISTINCT animal)) Pets
  ON Pets.user_id = Users.id

This works differently than existing versions by counting the distinct "type" of pet ('cat' versus 'dog').

Clockwork-Muse
  • 12,806
  • 6
  • 31
  • 45
  • If you have two cats and a dog, `count(distinct pet.id)` returns two. What do you think the `distinct` is for? – Andomar Jul 03 '11 at 21:03
  • Actually, no, not given his table structure; it should return *three*. To return two, use `count(distinct pet.animal)`. The catch is that the `pet.id` column is unique (or at least appears so) for every pet, so it's automatically distinct - I'm also assuming that every `pet` row would also have a `name` column... – Clockwork-Muse Jul 05 '11 at 17:28
  • if you will group by animal also then this will not give count(*) >1 just think about this again – pratik garg Jul 06 '11 at 05:36
  • This is all very useful - thanks. I've withheld the tick/bounty though because I really want as much activerecord and as little sql in my code as possible. – brad Jul 06 '11 at 13:30
  • Understandable, as I was approaching the problem from an SQL perspective anyways. – Clockwork-Muse Jul 06 '11 at 15:20
  • @pratikgarg - You were correct, thank you. There were other issues with that attempt anyways. – Clockwork-Muse Oct 21 '11 at 15:57
6

Use sub-selects to constrain the results:

User.joins(:pets).where(
  'id IN (SELECT user_id FROM pets WHERE animal = ?) AND
   id IN (SELECT user_id FROM pets WHERE animal = ?)',
  'cat', 'dog')
Jeremy Weathers
  • 2,556
  • 1
  • 16
  • 24
  • This gives me an error in my test setup: ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: id: SELECT "users".* FROM "users" INNER JOIN "pets" ON "pets"."user_id" = "users"."id" WHERE (id IN (SELECT user_id FROM pets WHERE animal = 'cat') AND id IN (SELECT user_id FROM pets WHERE animal = 'dog')). I'm not familiar enough with sql to spot the problem (but will keep looking). Any ideas? – brad Jul 01 '11 at 02:29
  • Actually, this seems to work: User.joins(:pets).where('users.id IN (SELECT user_id FROM pets WHERE animal = ?) AND users.id IN (SELECT user_id FROM pets WHERE animal = ?)', 'cat', 'dog'). But, it returns the user twice. => [#, #]. I realise I can whittle this down to a single result with .uniq but I really want to know how to do this in the database. – brad Jul 01 '11 at 02:34
  • 1
    That is odd - I'm not sure if the duplicate is caused by SQLite or ActiveRecord. If you haven't already, you might try `User.joins(:pets).where('users.id IN (SELECT DISTINCT(id) FROM users WHERE id IN (SELECT user_id FROM pets WHERE animal = ?) AND id IN (SELECT user_id FROM pets WHERE animal = ?))', 'cat', 'dog')` – Jeremy Weathers Jul 01 '11 at 07:06
  • Sorry for delay in reply - that still gives rise to the duplication. – brad Jul 03 '11 at 22:03
  • 1
    User.joins(:pets).where('users.id in (select user_id from pets where animal=?) and users.id in (select user_id from pets where animal=?)','cat','dog').group(:id) The group will remove the duplicates. – Anatortoise House Jul 04 '11 at 09:18
  • Thanks Anatortoise. That does the trick nicely. Jeremy, you get the tick/bounty as the bulk of the answer was from you. Anatortoise, sorry I can't offer you more reputation. Perhaps post an answer with your code and I can at least vote it up! – brad Jul 06 '11 at 13:32
  • Yes, I also thought the bounty and reputation should go to Jeremy, so posted a comment instead of an answer on purpose. I'm glad the comment helped. – Anatortoise House Jul 27 '11 at 17:31
3

The usual approach is to filter for cats OR dogs in the where clause. Then you group by on user_id, and demand that the resulting group having count(distinct pet.id) = 2.

I'm not sure how you express having in ActiveRecord; this post seems to contain a workaround.

Andomar
  • 232,371
  • 49
  • 380
  • 404
  • Hmmm, there is a .having method in ActiveRecord but so far I can't make it accept this query. I'll keep playing though. I looked up this pattern and it does seem to be the way to do it in SQL. It seems like a real hack to me though. Not too surprising that it's difficult to express in ActiveRecord. – brad Jun 27 '11 at 06:59
  • (-1) @Andomar -- cat OR dog gives a different result than cat AND dog. Your use of HAVING count(distinct...) would help if the pets table were a lookup table, but it's not -- it's a transaction table. Given the structure, it's possible for a person to have two cats (each with a different pet.id, even though they're both cats). – Chains Jul 06 '11 at 15:59
2

for this question there are so many ways to get solution you can do as follow also...

select dog_table.user_name from 
(
    select * 
    FROM users,pets  
    where pets.user_id = users.id 
    and  pets.animal = 'dog'
) dog_table,
(
    select * 
    FROM users,pets  
    where pets.user_id = users.id 
    and  pets.animal = 'cat'
) cat_table
where dog_table.user_id = cat_table.user_id
pratik garg
  • 3,282
  • 1
  • 17
  • 21
1

This should be helpful. try this

select u.* from users u, pets p1 where u.id = p1.user_id AND p1.animal="cat" AND p1.user_id in (select user_id from pets where animal='dog')

Mahesh
  • 6,378
  • 2
  • 26
  • 35
1

I rarely answer questions on SO but I will give it a try. :)

SELECT name
FROM users
WHERE id IN (SELECT a.user_id
             FROM (
                    (SELECT user_id FROM pets WHERE animal = 'cat') a
                    INNER JOIN
                    (SELECT user_id FROM pets WHERE animal = 'dog') b
                    ON a.user_id = b.user_id
                  ));
emeraldhieu
  • 9,380
  • 19
  • 81
  • 139
1

There are a number of ways to do this -- some of the above will work; also, here is a slightly different approach that uses essentially 'views' -- essentially, just inheriting from your generic 'pets' class into two separate classes (cats & dogs).

SELECT
   id,
   name
FROM
      users
   INNER JOIN
      (
      SELECT DISTINCT
         user_id as belongs_to
      FROM
         pets
      WHERE
         animal = 'dog'
      ) dog
   ON users.id = dog.belongs_to
   INNER JOIN
      (
      SELECT DISTINCT
         user_id as belongs_to
      FROM
         pets
      WHERE
         animal = 'cat'
      ) cat
   ON users.id = cat.belongs_to
Chains
  • 12,541
  • 8
  • 45
  • 62
1

Here is another solution. Bit more Rails friendly..

User.all(:select => "DISTINCT users.*",
 :joins=>[:pets, :pets],
 :conditions => ["pets.animal = ? AND pets_users.animal = ?", "cat", "dog"])

Read this article on the relative merits of using JOIN vs GROUP BY + HAVING for such solution.

Refer to this SO question that discusses this problem in detail.

Community
  • 1
  • 1
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
0

you can get this result various ways.

Hope it help you. try this out,

SELECT id FROM (SELECT user_id AS id FROM users
INNER JOIN pets ON pets.user_id=users.id
GROUP BY pets.user_id,pets.animal
HAVING COUNT(pets.user_id)>0 AND (pets.animal='cat' OR pets.animal='dog')
)AS s GROUP BY id HAVING COUNT(id)>1
Sujit
  • 3,677
  • 9
  • 41
  • 50
  • this would give you a positive match for just one cat, or just one dog, etc.; it won't guarantee you get an owner of both a cat AND a dog. – Chains Jul 05 '11 at 21:46