RailsでItem has many Tagの時にTagに対する条件のANDをしたい

やりたいこと

# Item
  has_many :item_tag_relations, dependent: :destroy
  has_many :tags, through: :item_tag_relations, dependent: :destroy
# ItemTagRelation
  belongs_to :item
  belongs_to :tag
  • Tag name: string

という状況で「"red"と"blue"と"yellow"全てのnameのTagを持ったItem」が欲しいという状況がある。 単純に考えて

names = ['red', 'blue', 'yellow']
tag_ids = Tag.where(name: names).select(:id)
item_ids = ItemTagRelation.where(tag_id: tag_ids).select(:item_id)
@items = Item.where(id: item_ids)

としても「"red"と"blue"と"yellow"のいずれかのnameのTagを持ったA」になってしまい、これは条件のORである。 これを「"red"と"blue"と"yellow"全てのnameのTagを持ったItem」という形で条件のANDにしたかった。

やりかた

names = ['red', 'blue', 'yellow']
item_ids = Item.joins(:tags)
               .where(tags: { name: names })
               .group('items.id')
               .having('count(distinct tags.name) = ?', names.uniq.count)
@items = Item.where(id: item_ids)

やってることは単純で、joinした後に条件をクリアしたTagのnameによる重複を排除した数と条件の数が等しいならそれは全ての条件をクリアしているということなのでそれを利用している。