【Rails入門】同じscopeを書くならActiveSupport::Concernを使おう

Ruby on Rails(以降、Rails)に、ActiveSupport::Concernというモジュールがあることを知っていますか?

ActiveSupport::Concernは、ModelやControllerのコードを分割するときに使えるモジュールです。

複数のModelやControllerで書かなくてはいけなくなった同じコードを、別のモジュールに分割してまとめることもできます。

他方、ActiveSupport::Concernの使いどころがわかりにくかったり、使いすぎると逆にコードがわかりにくくなったりするという問題もあります。

この記事では、分かりやすい例としてActiveSupport::Concernを使ってscopeをまとめる方法を説明します。

ぜひ試してみてください!

目次

scopeとは

scopeって何だっけ?というところから始めましょう。

scopeは、特定のSQL文をメソッド化するための構文です。

Modelに以下のようなコードを追加すると…

  scope :birth_before, ->(date) { where("birth < ?", date) }

以下のような使いかたで、1990/1/1より前に産まれた人を抽出できます。

User.birth_before("1990-01-01")

scopeについては、以下の記事で詳しく説明していますので、ぜひご覧ください。

ActiveSupport::Concernとは

ActiveSupport::Concernは、ModelやControllerのコードを分割するときに使えるモジュールです。

言い方を変えると、ControllerやModelのコードをモジュール化するために使います。

Viewの場合は部分テンプレート(partial)を使って小分けにしますが、ControllerやModelの場合はActiveSupport::Concernを使って小分けにするイメージです。

以下の記事では、Banana Controllerの一部をBananaOneモジュールに切り出す例を紹介しました。

この記事では、もう少し実践的な使いかたを見ていきましょう。

動作を理解するためのWebアプリを作成する

scopeとActiveSupport::Concernの雰囲気をつかんだところで、動作を確認するために、Rails 5.1をインストールしてWebアプリを作りましょう。

(1)Railsをインストールします。

私は、以下の記事を参考に、VirtualBoxで作成した仮想パソコンにインストールしたLinux Mintに、Railsの開発環境を作成しました。

基本的には記事の手順に従って操作しますが、app/samurai/sample1ディレクトリを作成する代わりに、app/samurai/concern-demoディレクトリを作成しました。

Railsを起動して、ブラウザで画面が表示されることを確認したら、いったんRailsを終了してから次に進みます。

Linux Mintのインストールについては、以下の記事で詳しく説明しています。

(2)Gemfileの最終行に以下の内容を追記します。

gem 'hirb'
gem 'hirb-unicode'

Hirbについては、以下の記事で詳しく説明していますので、あわせてご覧ください。

(3)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

cd app/samurai/concern-demo
bundle install
bin/rails generate scaffold User name:string birth:date check:boolean
bin/rails generate scaffold Post user_id:integer title:string month:integer check:boolean
bin/rails db:migrate
bin/rails console

(4)以下のコードを1行ずつ順番に入力します。

User.create(name:"山田太郎", birth:"1991-09-22", check: false)
User.create(name:"長瀬来", birth:"1991-10-18", check: false)
User.create(name:"立川裕美", birth:"1968-06-28", check: false)
User.create(name:"前田達郎", birth:"1984-10-12", check: false)
User.create(name:"細川修二", birth:"1971-03-24", check: false)
User.create(name:"木村拓磨", birth:"1995-05-19", check: false)
Post.create(user_id:5, title:"楽しい休日の過ごし方", month:3, check: false)
Post.create(user_id:1, title:"先日の旅行での話", month:2, check: false)
Post.create(user_id:3, title:"昨日の出来事", month:12, check: false)
Post.create(user_id:3, title:"山登りに行きました", month:8, check: true)
Post.create(user_id:4, title:"友人が結婚しました", month:4, check: false)
Post.create(user_id:2, title:"最近少し気になったこと", month:1, check: true)
Post.create(user_id:4, title:"ランニングのコツ", month:9, check: false)
Post.create(user_id:3, title:"Ruby on Railsの日", month:9, check: true)
exit

UserテーブルとPostテーブルにデータが入力され、Railsコンソールが終了します。

これで準備ができました。

scopeを使ってみよう

まずは、ActiveSupport::Concernは脇に置いて、scopeを使ってみましょう。

(1)app/models/user.rbを編集します。

変更前:

class User < ApplicationRecord
end

変更後:

class User < ApplicationRecord
  scope :birth_before, ->(date) { where("birth < ?", date) }
end

(2)「端末」で、以下のコマンドを入力します。

bin/rails console

(3)以下のコードを1行ずつ順番に入力します。

Hirb.enable
User.birth_before("1990-01-01")

実行結果:

  User Load (1.4ms)  SELECT "users".* FROM "users" WHERE (birth < '1990-01-01')
+----+----------+------------+-------+-------------------------+-------------------------+
| id | name     | birth      | check | created_at              | updated_at              |
+----+----------+------------+-------+-------------------------+-------------------------+
| 3  | 立川裕美 | 1968-06-28 | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 4  | 前田達郎 | 1984-10-12 | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 5  | 細川修二 | 1971-03-24 | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
+----+----------+------------+-------+-------------------------+-------------------------+
3 rows in set

確かに、1990/1/1より前に産まれた人だけが抽出されていますね。

scopeで定義したメソッドは、update_allなどと組み合わせて使えます。

update_allについては、以下の記事で説明していますので、あわせてご覧ください。

(4)以下のコードを1行ずつ順番に入力します。

User.birth_before("1990-01-01").update_all(check: true)
User.all

実行結果:

  User Load (0.2ms)  SELECT "users".* FROM "users"
+----+----------+------------+-------+-------------------------+-------------------------+
| id | name     | birth      | check | created_at              | updated_at              |
+----+----------+------------+-------+-------------------------+-------------------------+
| 1  | 山田太郎 | 1991-09-22 | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 2  | 長瀬来   | 1991-10-18 | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 3  | 立川裕美 | 1968-06-28 | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 4  | 前田達郎 | 1984-10-12 | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 5  | 細川修二 | 1971-03-24 | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 6  | 木村拓磨 | 1995-05-19 | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
+----+----------+------------+-------+-------------------------+-------------------------+
6 rows in set

1990/1/1より前に産まれた人のchecktrueに変わりました。

(5)以下のコードを入力します。

exit

Railsコンソールが終了します。

birth_beforeは、app/models/user.rb(User Model)に追加したため、別のModel(たとえば、app/models/post.rb)で使用する場合は、あらためてapp/models/post.rbを編集する必要があります。

ActiveSupport::Concernを使ってみよう

ここからは、複数のModelで同じscopeを使いたい場合は、ActiveSupport::Concernを使おうという話をします。

上で説明した誕生日のように、User Model特有のカラムを扱う場合は、app/models/user.rb(User Model)に直接書くことは正しい対応です。

一方、複数のモデルで共通の処理がある場合は、ActiveSupport::Concernを使ってモジュール化するほうが正しい対応でしょう。

ここでは、User ModelにもPost Modelにも存在するcheckを対象にしたscopeを書いてみましょう。

具体的には、checkがtrueのデータだけを表示するscopeを作成します。

(1)app/models/concerns/common_scope.rbを以下の内容で作成します。

module CommonScope
  extend ActiveSupport::Concern

  included do
    scope :check, ->(bool){ where(check: bool) }
  end
end

(2)app/models/post.rbを編集します。

変更前:

class Post < ApplicationRecord
end

変更後:

class Post < ApplicationRecord
  include CommonScope
end

(3)app/models/user.rbを編集します。

変更前:

class User < ApplicationRecord
  scope :birth_before, ->(date) { where('birth < ?', date) }
end

変更後:

class User < ApplicationRecord
  include CommonScope
  scope :birth_before, ->(date) { where('birth < ?', date) }
end

(4)「端末」で、以下のコマンドを入力します。

bin/rails console

(5)以下のコード1行ずつ順番に入力します。

Hirb.enable
User.check(true)

実行結果:

  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."check" = ?  [["check", "t"]]
+----+----------+------------+-------+-------------------------+-------------------------+
| id | name     | birth      | check | created_at              | updated_at              |
+----+----------+------------+-------+-------------------------+-------------------------+
| 3  | 立川裕美 | 1968-06-28 | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 4  | 前田達郎 | 1984-10-12 | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 5  | 細川修二 | 1971-03-24 | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
+----+----------+------------+-------+-------------------------+-------------------------+
3 rows in set

app/models/concerns/common_scope.rbで定義したscopeが使えていますね。

Post Modelにもcheck(true)が使えるか確認するために、まずはPost Modelの全レコードを確認します。

(6)以下のコードを入力します。

Post.all

実行結果:

  Post Load (0.2ms)  SELECT "posts".* FROM "posts"
+----+---------+------------------------+-------+-------+-------------------------+-------------------------+
| id | user_id | title                  | month | check | created_at              | updated_at              |
+----+---------+------------------------+-------+-------+-------------------------+-------------------------+
| 1  | 5       | 楽しい休日の過ごし方   | 3     | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 2  | 1       | 先日の旅行での話       | 2     | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 3  | 3       | 昨日の出来事           | 12    | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 4  | 3       | 山登りに行きました     | 8     | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 5  | 4       | 友人が結婚しました     | 4     | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 6  | 2       | 最近少し気になったこと | 1     | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 7  | 4       | ランニングのコツ       | 9     | false | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 8  | 3       | Ruby on Railsの日      | 9     | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
+----+---------+------------------------+-------+-------+-------------------------+-------------------------+
8 rows in set

checkを見ると、trueのレコードとfalseのレコードがありますね。

さて、check(true)が使えるでしょうか。

(7)以下のコードを入力します。

Post.check(true)

実行結果:

  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."check" = ?  [["check", "t"]]
+----+---------+------------------------+-------+-------+-------------------------+-------------------------+
| id | user_id | title                  | month | check | created_at              | updated_at              |
+----+---------+------------------------+-------+-------+-------------------------+-------------------------+
| 4  | 3       | 山登りに行きました     | 8     | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 6  | 2       | 最近少し気になったこと | 1     | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
| 8  | 3       | Ruby on Railsの日      | 9     | true  | 2018-07-26 23:41:42 UTC | 2018-07-26 23:41:42 UTC |
+----+---------+------------------------+-------+-------+-------------------------+-------------------------+
3 rows in set

確かにPost Modelに対してもscopeが使えています。

まとめ

今回は、scopeの使いかたをおさらいしてから、ActiveSupport::Concernの使いかたを紹介しました。

ActiveSupport::Concernを使うと、ControllerやModelを小分けにできることも説明しました。

この記事では、複数のModelで共通のscopeを定義しましたが、ControllerでもActiveSupport::Concernを使えますので、ぜひ挑戦してみてください!

ただ、ModelでもControllerでも、小分けにすることで、どこで何が定義されているのか分かりにくくなってしまうという問題もあります。

特に複数人で開発しているときには、無闇に小分けにして開発効率が落ちないように注意しましょう。

いろいろなパターンで試してみて、ちょうどよい方法を見つけてください!

この記事を書いた人

侍エンジニア塾は「人生を変えるプログラミング学習」をコンセンプトに、過去多くのフリーランスエンジニアを輩出したプログラミングスクールです。侍テック編集部では技術系コンテンツを中心に有用な情報を発信していきます。

目次