Ruby on Rails

Active Recordとデータベース

Rubyのコードでデータを保存・検索し、変更履歴と関連付けを管理します。

01

マイグレーションを使う

テーブル構造の変更はマイグレーションへ記録します。後から別の環境でも同じ変更を再現できるようにするためです。

コマンドCOMMAND
bin/rails generate model Article title:string body:text published:boolean
bin/rails db:migrate
bin/rails db:rollback

db:rollbackは直前の変更を戻す開発用の操作です。共有済みのマイグレーションを勝手に書き換えると環境ごとの差が生まれるため、公開後の変更は新しいマイグレーションで表現します。

02

保存と検索

consoleRUBY
article = Article.new(title: "Rails入門", body: "本文", published: true)
article.save
Article.create!(title: "2つ目", body: "本文", published: false)

Article.where(published: true).order(created_at: :desc)
Article.find_by(title: "Rails入門")
Article.where("title LIKE ?", "%Rails%").pluck(:title)
結果の例OUTPUT
["Rails入門"]

検索条件はハッシュやプレースホルダーを使います。文字列を連結してSQLを作ると入力値がSQLの一部として解釈される危険があるため、値は条件の引数として渡します。

03

関連付けを作る

ユーザーが複数の記事を持つなら、UserとArticleの関係をモデルへ書きます。

モデルと生成COMMAND / RUBY
bin/rails generate model User name:string
bin/rails generate migration AddUserToArticles user:references
bin/rails db:migrate

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  belongs_to :user
end
使い方RUBY
user = User.create!(name: "Mity")
article = user.articles.create!(title: "関連付け", body: "本文")
article.user.name
# => "Mity"

belongs_to側には通常外部キーが必要です。関連付けは便利ですが、削除時の影響も確認してdependentの方針を決めます。

04

バリデーションとエラー

modelRUBY
class Article < ApplicationRecord
  validates :title, presence: true
  validates :body, presence: true
  validates :title, length: { maximum: 100 }
end

article = Article.new(title: "")
article.valid?
# => false
article.errors[:title]
# => ["can't be blank"]

バリデーションは利用者に見せる入力エラーを作る仕組みです。データベース側の制約も必要な場面があるため、重要な一意性や必須条件はアプリケーションとDBの両面から検討します。

05

scopeと再利用できる検索

同じ検索条件を何度も使うならscopeへ名前を付けます。検索条件をモデルへまとめることで、コントローラーにSQLの細部が増えるのを防げます。

app/models/article.rbRUBY
class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc) }
  scope :with_keyword, ->(word) { where("title LIKE ?", "%#{word}%") }
end
利用するRUBY
Article.published.recent.limit(5)
Article.published.with_keyword("Rails").pluck(:title)
結果の例OUTPUT
["Rails入門", "MVCを理解する"]

scopeはRelationを返すため、条件をつなげられます。引数を受け取るscopeでは、検索値をSQL文字列へ直接連結せず、プレースホルダーへ渡します。

06

includesでN+1を避ける

記事一覧で各記事の著者名を表示するとき、記事を1回取得したあと著者を記事ごとに取得するN+1クエリが起きることがあります。関連データを先に読み込むにはincludesを使います。

問題のある例RUBY
articles = Article.limit(10)
articles.each { |article| puts article.user.name }
# Article 1回 + User 10回になる可能性
改善例RUBY
articles = Article.includes(:user).limit(10)
articles.each { |article| puts article.user.name }
# Article 1回 + User 1回を目指せる
調査COMMAND
bin/rails db:prepare
bin/rails test
# development.logのSQL回数も確認する

常にincludesを付ければよいわけではありません。画面で使う関連を把握し、ログやテストでクエリ数を確認してから改善します。

07

コールバックとenum

status列を追加COMMAND
bin/rails generate migration AddStatusToArticles status:integer
bin/rails db:migrate

コールバックは保存前後など決まったタイミングで処理します。enumは状態を名前で扱う仕組みです。どちらも便利ですが、処理を隠しすぎると保存時の副作用が分かりにくくなるため、短い処理に限定します。

状態とコールバックRUBY
class Article < ApplicationRecord
  enum :status, { draft: 0, live: 1, archived: 2 }
  before_validation :set_default_status, on: :create

  private

  def set_default_status
    self.status ||= :draft
  end
end
利用するRUBY
article = Article.create!(title: "下書き", body: "本文")
article.draft?
# => true
article.live!
Article.live.count
# => 1

状態の変更が重要な業務処理を呼ぶ場合は、コールバックへ詰め込まず、名前付きのサービスや明示的なメソッドとして表す方法も検討します。

PRACTICE

練習問題

問題:公開済みの記事を3件まで取得する

公開済みで、作成日の新しい順に3件だけ取得してください。

解答例RUBY
articles = Article.where(published: true)
                  .order(created_at: :desc)
                  .limit(3)