简体   繁体   中英

Rails + MySQL, Intersection of Many to Many Relation

I have Sites and Tags in a many to many relationship, connected by join table SitesTags:

Site
  has_and_belogs_to_many :tags
  id name
  1  siteA
  2  siteB

Tag
  # has_and_belogs_to_many :sites
  id name
  1  tagA
  2  tagB
  3  tagC

SitesTags
  site_id tag_id
  1       1
  1       2
  2       2
  2       3

I would like to get the COUNT of tags that two sites have in common. In this example, there would be one common tag of siteA and siteB (tagB).

Ideally I would like a solution on the Databases level, but I'm using MySQL. I have tried (Site.find(1).tags & Site.find(2).tags).count but I can see that this is doing multiple queries, and it's not using COUNT(*) but fetching all Data:

Site Load (0.3ms)  SELECT  `sites`.* FROM `sites` WHERE `sites`.`id` = 1 LIMIT 1
Site Load (0.3ms)  SELECT  `sites`.* FROM `sites` WHERE `sites`.`id` = 2 LIMIT 1
Tag Load (0.3ms)  SELECT `tags`.* FROM `tags` INNER JOIN `sites_tags` ON `tags`.`id` = `sites_tags`.`tag_id` WHERE `sites_tags`.`site_id` = 1
Tag Load (0.4ms)  SELECT `tags`.* FROM `tags` INNER JOIN `sites_tags` ON `tags`.`id` = `sites_tags`.`tag_id` WHERE `sites_tags`.`site_id` = 2

Another thing I have tried is

Site.find(1).tags.where("`sites_tags`.`site_id` = 2")

which is generating

SELECT `tags`.* FROM `tags` INNER JOIN `sites_tags` ON `tags`.`id` = `sites_tags`.`tag_id` WHERE `sites_tags`.`site_id` = 1 AND (`sites_tags`.`site_id` = 2)

This doesn't work, I think it's trying to find a single record where the site_id is 1 AND 2

To get count try this:

Site.find(1).tags.count

To get tags count common in site1 and site2 :

s1 = Site.find(1).tags.map(&:name)

s2 = Site.find(2).tags.map(&:name)

common_tags s1 & s2

For a solution on the Database level you could use raw sql, for example, to get count of common tags:

sql = <<~SQL
  SELECT COUNT(DISTINCT(a.tag_id))
  FROM sites_tags a JOIN sites_tags b ON a.tag_id = b.tag_id
  WHERE a.site_id != b.site_id
  AND a.site_id IN (1, 2);
SQL

count = ActiveRecord::Base.connection.select_rows(sql).flatten
#=> [1]

Or, if you would like an array with the name of all common tags (and count them later), you could use this query:

sql = <<~SQL
  SELECT DISTINCT(c.name)
  FROM sites_tags a
    JOIN sites_tags b ON a.tag_id = b.tag_id
    JOIN tags c ON a.tag_id = c.id
  WHERE a.site_id != b.site_id
  AND a.site_id IN (1, 2);
SQL

tags = ActiveRecord::Base.connection.select_rows(sql).flatten
#=> ["tagB"]

Both will work with MySQL.

Use merge

Site.find(1).tags.merge(Site.find(2).tags).count

It will do it in 3 efficient queries

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