简体   繁体   中英

Chained where clauses on has_many :through association (Rails 4.1.0.rc1)

BACKGROUND: I'm on edge Rails (4.1.0.rc1). Users has many Communities through CommunityUser model. The following users belong to various communities:

USERS TABLE
ID | COMMUNITY_IDS
---|--------------
1  | [2, 7, 8]
2  | [3, 4, 8]
3  | [4, 5, 7]
4  | [3, 5, 7]
5  | [3, 8, 10]
6  | [4, 6, 7]
7  | [1, 8, 10]
8  | [3, 8, 10]
9  | [2, 9, 10]
10 | [3, 6, 10]

User.joins(:communities).where(communities: {id: [5,7]}).uniq

Returns all users associated with either Community 5 or Community 7:

SQL => SELECT DISTINCT "users".* FROM "users" INNER JOIN "community_users" ON "community_users"."user_id" = "users"."id" INNER JOIN "communities" ON "communities"."id" = "community_users"."community_id" WHERE "communities"."id" IN (5, 7)

ID | COMMUNITY_IDS
---|--------------
1  | [2, 7, 8]
3  | [4, 5, 7]
4  | [3, 5, 7]
6  | [4, 6, 7]

Trying to filter these further (to return those from this group also associated with Community 6) by adding another where clause is returning an empty ActiveRecord::Relation:

User.joins(:communities).where(communities: {id: [5,7]}).where(communities: {id: [6]}).uniq

=> SQL: SELECT DISTINCT "users".* FROM "users" INNER JOIN "community_users" ON "community_users"."user_id" = "users"."id" INNER JOIN "communities" ON "communities"."id" = "community_users"."community_id" WHERE "communities"."id" IN (5, 7) AND "communities"."id" IN (6)

=> #<ActiveRecord::Relation []>

OBJECTIVE: Is this the correct behavior for this query? If so, how would I write this query to return Users associated with EITHER Community 5 or Community 7 AND associated with Community 6.

The problem is you think about community_ids in a way, that you displays above.

ID | COMMUNITY_IDS
---|--------------
1  | [2, 7, 8]
3  | [4, 5, 7]
4  | [3, 5, 7]
6  | [4, 6, 7]

Actually, you'll get this intermediate result set:

UID| COMMUNITY_ID
---|--------------
3  | 5
4  | 5
1  | 7
3  | 7
4  | 7
6  | 7

Then you say: Okay, AND "communities"."id" IN (6) ! There are no communities with such id in results set.

To get users with communities [5,7]:

users = Community.where(id:[5,7]).map(&:users).flatten.uniq

To filter users by Community with id=7:

filtered = users.select{|u| u.communities.map(&:id).include?(6) }

This code is work (it returns array with single user with id = 6) but this is not the best solution.

This solution requires you to write the join queries on your own instead of using Ruby. On the other hand, this is just one (fast) SQL query and will scale better if you are comparing communities a lot.

As Vitalyp pointed out the CommunityUsers table looks something like this

CommunityUsers
user_id | community_id
--------|--------------
 1      | 5
 2      | 5
 1      | 7
 2      | 7
 1      | 6
 3      | 6

In order to find those records that include either community 5 or 7 as well as 6, we can select both and join them.

User.joins('INNER JOIN "community_users" c1 ON c1."user_id" = "users"."id" AND c1."id" IN [5, 7]')
    .joins('INNER JOIN "community_users" c2 ON c2."user_id" = "users"."id" AND c2."id" = 6')

Using inner joins, the query only selects those users, which are in community 5 or 7 (first join clause) and in community 6 (second join clause).


Additional tip: Creating an index on community_users.user_id will speed up the query as well.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM