QA@IT
«回答へ戻る

いいですね!! 細かいですけど、可変長引数を使うようにすれば、names一個でもArrayで取れますし、呼び出し側もスッキリ書けそうです。

887
 ```ruby
 class Movie < ActiveRecord::Base
   has_and_belongs_to_many :categories
-  scope :categorized_as, -> names do
-    target_ids = Movie.
+  scope :categorized_as, ->(*names) do
+    target_ids = self.
       select("movies.id").
       joins(:categories).
       merge(Category.where(name: names)).
       group("movies.id").
-      having("count(distinct categories.name) = ?", names.class == Array ? names.size : 1)
+      having("count(distinct categories.name) = ?", names.size)
 
     where(id: target_ids)
   end
 end
 ```
 ```ruby
-irb(main):018:0> Movie.categorized_as(['Action', 'Adventure']).count
+irb(main):018:0> Movie.categorized_as('Action', 'Adventure').count
 ```
 SQL は下のようになります。
 ```SQL

SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。

class Movie < ActiveRecord::Base
  has_and_belongs_to_many :categories
  scope :categorized_as, ->(*names) do
    target_ids = self.
      select("movies.id").
      joins(:categories).
      merge(Category.where(name: names)).
      group("movies.id").
      having("count(distinct categories.name) = ?", names.size)

    where(id: target_ids)
  end
end
irb(main):018:0> Movie.categorized_as('Action', 'Adventure').count

SQL は下のようになります。

SELECT COUNT(*) 
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )

これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。
```ruby
class Movie < ActiveRecord::Base
  has_and_belongs_to_many :categories
  scope :categorized_as, ->(*names) do
    target_ids = self.
      select("movies.id").
      joins(:categories).
      merge(Category.where(name: names)).
      group("movies.id").
      having("count(distinct categories.name) = ?", names.size)

    where(id: target_ids)
  end
end
```
```ruby
irb(main):018:0> Movie.categorized_as('Action', 'Adventure').count
```
SQL は下のようになります。
```SQL
SELECT COUNT(*) 
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )
```
これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

プレースホルダを使うようにしよう

40
       joins(:categories).
       merge(Category.where(name: names)).
       group("movies.id").
-      having("count(distinct categories.name) = #{names.class == Array ? names.size : 1}")
+      having("count(distinct categories.name) = ?", names.class == Array ? names.size : 1)
 
     where(id: target_ids)
   end

SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。

class Movie < ActiveRecord::Base
  has_and_belongs_to_many :categories
  scope :categorized_as, -> names do
    target_ids = Movie.
      select("movies.id").
      joins(:categories).
      merge(Category.where(name: names)).
      group("movies.id").
      having("count(distinct categories.name) = ?", names.class == Array ? names.size : 1)

    where(id: target_ids)
  end
end
irb(main):018:0> Movie.categorized_as(['Action', 'Adventure']).count

SQL は下のようになります。

SELECT COUNT(*) 
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )

これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。
```ruby
class Movie < ActiveRecord::Base
  has_and_belongs_to_many :categories
  scope :categorized_as, -> names do
    target_ids = Movie.
      select("movies.id").
      joins(:categories).
      merge(Category.where(name: names)).
      group("movies.id").
      having("count(distinct categories.name) = ?", names.class == Array ? names.size : 1)

    where(id: target_ids)
  end
end
```
```ruby
irb(main):018:0> Movie.categorized_as(['Action', 'Adventure']).count
```
SQL は下のようになります。
```SQL
SELECT COUNT(*) 
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )
```
これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

質問の例に合わせて count にする

40
 end
 ```
 ```ruby
-irb(main):018:0> Movie.categorized_as(['Action', 'Adventure'])
+irb(main):018:0> Movie.categorized_as(['Action', 'Adventure']).count
 ```
 SQL は下のようになります。
 ```SQL
-SELECT "movies".*
+SELECT COUNT(*) 
   FROM "movies"
  WHERE "movies"."id" IN (
        SELECT movies.id

SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。

class Movie < ActiveRecord::Base
  has_and_belongs_to_many :categories
  scope :categorized_as, -> names do
    target_ids = Movie.
      select("movies.id").
      joins(:categories).
      merge(Category.where(name: names)).
      group("movies.id").
      having("count(distinct categories.name) = #{names.class == Array ? names.size : 1}")

    where(id: target_ids)
  end
end
irb(main):018:0> Movie.categorized_as(['Action', 'Adventure']).count

SQL は下のようになります。

SELECT COUNT(*) 
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )

これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。
```ruby
class Movie < ActiveRecord::Base
  has_and_belongs_to_many :categories
  scope :categorized_as, -> names do
    target_ids = Movie.
      select("movies.id").
      joins(:categories).
      merge(Category.where(name: names)).
      group("movies.id").
      having("count(distinct categories.name) = #{names.class == Array ? names.size : 1}")

    where(id: target_ids)
  end
end
```
```ruby
irb(main):018:0> Movie.categorized_as(['Action', 'Adventure']).count
```
SQL は下のようになります。
```SQL
SELECT COUNT(*) 
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )
```
これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

ちゃんとした回答にしよう

40
-SQL 的なアプローチとして、私なら下の様に書きます。
+SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。
 ```ruby
-category_names = ['Action', 'Adventure']
-target_ids = Movie.
-  select("movies.id").
-  joins(:categories).
-  merge(Category.where(name: category_names)).
-  group("movies.id").
-  having("count(distinct categories.name) = #{category_names.size}")
-Movie.where(id: target_ids)
+class Movie < ActiveRecord::Base
+  has_and_belongs_to_many :categories
+  scope :categorized_as, -> names do
+    target_ids = Movie.
+      select("movies.id").
+      joins(:categories).
+      merge(Category.where(name: names)).
+      group("movies.id").
+      having("count(distinct categories.name) = #{names.class == Array ? names.size : 1}")
+
+    where(id: target_ids)
+  end
+end
 ```
+```ruby
+irb(main):018:0> Movie.categorized_as(['Action', 'Adventure'])
+```
 SQL は下のようになります。
 ```SQL
 SELECT "movies".*
        HAVING count(distinct categories.name) = 2
  )
 ```
-この手の多対多の AND 絞り込みには、先に回答があった ARRAY や、今回紹介した HAVING を使います。RDBMS の縛りがあって ARRAY が使えないとき等には HAVING が助けてくれそうです。
-
--- 追記
-あ、ほぼ同じアプローチが大元の書評エントリの中にありましたね。
-「同じテーブルを何度も JOIN する上手い方法は?」という質問だったのに脱線してしまい申し訳ないです。
+これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。

class Movie < ActiveRecord::Base
  has_and_belongs_to_many :categories
  scope :categorized_as, -> names do
    target_ids = Movie.
      select("movies.id").
      joins(:categories).
      merge(Category.where(name: names)).
      group("movies.id").
      having("count(distinct categories.name) = #{names.class == Array ? names.size : 1}")

    where(id: target_ids)
  end
end
irb(main):018:0> Movie.categorized_as(['Action', 'Adventure'])

SQL は下のようになります。

SELECT "movies".*
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )

これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

SQL 的なアプローチを Rails になじませる実装として、私なら下の様に書きます。
```ruby
class Movie < ActiveRecord::Base
  has_and_belongs_to_many :categories
  scope :categorized_as, -> names do
    target_ids = Movie.
      select("movies.id").
      joins(:categories).
      merge(Category.where(name: names)).
      group("movies.id").
      having("count(distinct categories.name) = #{names.class == Array ? names.size : 1}")

    where(id: target_ids)
  end
end
```
```ruby
irb(main):018:0> Movie.categorized_as(['Action', 'Adventure'])
```
SQL は下のようになります。
```SQL
SELECT "movies".*
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )
```
これなら、メソッドチェインに組み込めますし、Movie モデルを使う人があまり悩まずに使えるようになると思います。

恥ずかしい

40
  )
 ```
 この手の多対多の AND 絞り込みには、先に回答があった ARRAY や、今回紹介した HAVING を使います。RDBMS の縛りがあって ARRAY が使えないとき等には HAVING が助けてくれそうです。
+
+-- 追記
+あ、ほぼ同じアプローチが大元の書評エントリの中にありましたね。
+「同じテーブルを何度も JOIN する上手い方法は?」という質問だったのに脱線してしまい申し訳ないです。

SQL 的なアプローチとして、私なら下の様に書きます。

category_names = ['Action', 'Adventure']
target_ids = Movie.
  select("movies.id").
  joins(:categories).
  merge(Category.where(name: category_names)).
  group("movies.id").
  having("count(distinct categories.name) = #{category_names.size}")
Movie.where(id: target_ids)

SQL は下のようになります。

SELECT "movies".*
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )

この手の多対多の AND 絞り込みには、先に回答があった ARRAY や、今回紹介した HAVING を使います。RDBMS の縛りがあって ARRAY が使えないとき等には HAVING が助けてくれそうです。

-- 追記
あ、ほぼ同じアプローチが大元の書評エントリの中にありましたね。
「同じテーブルを何度も JOIN する上手い方法は?」という質問だったのに脱線してしまい申し訳ないです。

SQL 的なアプローチとして、私なら下の様に書きます。
```ruby
category_names = ['Action', 'Adventure']
target_ids = Movie.
  select("movies.id").
  joins(:categories).
  merge(Category.where(name: category_names)).
  group("movies.id").
  having("count(distinct categories.name) = #{category_names.size}")
Movie.where(id: target_ids)
```
SQL は下のようになります。
```SQL
SELECT "movies".*
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )
```
この手の多対多の AND 絞り込みには、先に回答があった ARRAY や、今回紹介した HAVING を使います。RDBMS の縛りがあって ARRAY が使えないとき等には HAVING が助けてくれそうです。

-- 追記
あ、ほぼ同じアプローチが大元の書評エントリの中にありましたね。
「同じテーブルを何度も JOIN する上手い方法は?」という質問だったのに脱線してしまい申し訳ないです。

回答を投稿

SQL 的なアプローチとして、私なら下の様に書きます。

category_names = ['Action', 'Adventure']
target_ids = Movie.
  select("movies.id").
  joins(:categories).
  merge(Category.where(name: category_names)).
  group("movies.id").
  having("count(distinct categories.name) = #{category_names.size}")
Movie.where(id: target_ids)

SQL は下のようになります。

SELECT "movies".*
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )

この手の多対多の AND 絞り込みには、先に回答があった ARRAY や、今回紹介した HAVING を使います。RDBMS の縛りがあって ARRAY が使えないとき等には HAVING が助けてくれそうです。

SQL 的なアプローチとして、私なら下の様に書きます。
```ruby
category_names = ['Action', 'Adventure']
target_ids = Movie.
  select("movies.id").
  joins(:categories).
  merge(Category.where(name: category_names)).
  group("movies.id").
  having("count(distinct categories.name) = #{category_names.size}")
Movie.where(id: target_ids)
```
SQL は下のようになります。
```SQL
SELECT "movies".*
  FROM "movies"
 WHERE "movies"."id" IN (
       SELECT movies.id
         FROM "movies"
   INNER JOIN "categories_movies"
           ON "categories_movies"."movie_id" = "movies"."id"
   INNER JOIN "categories"
           ON "categories"."id" = "categories_movies"."category_id"
        WHERE "categories"."name" IN ('Action', 'Adventure')
     GROUP BY movies.id
       HAVING count(distinct categories.name) = 2
 )
```
この手の多対多の AND 絞り込みには、先に回答があった ARRAY や、今回紹介した HAVING を使います。RDBMS の縛りがあって ARRAY が使えないとき等には HAVING が助けてくれそうです。