https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58 ORM の join については下の方に書いてある。5,000 回くらい調べてるので先頭にまとめを書いておく
- joins
- INNER JOIN
- ActiveRecord キャッシュしない
- left_joins
- LEFT OUTER JOIN
- ActiveRecord キャッシュしない
- Rails v5 から
- preload
- 指定 Association を
_id IN(1, 2, 3, ...)
形式で別クエリ発行 - ActiveRecord キャッシュする
- Association 先で
where()
すると例外発生
- 指定 Association を
- eager_load
- 指定 Association を LEFT OUTER JOIN で連結
- ActiveRecord キャッシュする
- Association 先の絞り込み可能、クエリが少ない分 preload より早いケースも
- includes
- ↓ 条件のとき eager_load, そうでないとき preload する
- 指定 Association への絞り込みをする
- 指定 Association への joins or references を呼ぶ
- 何らかの Association を eager_load する
Active Record Transactions
【Rails】絶対に抑えたいTransactionのポイント rubyの例外についてまとめてみた
- transaction だけだと楽観ロックとなる
lock!
やmodel.lock
model.with_lock {}
だと悲観ロック- https://qiita.com/kamohicokamo/items/e7c31f61c99c9fc0fe7f
def saveFinances(balance, account)
begin
ActiveRecord::Base.transaction do
# しくじったら例外 + コールバック実行
balance.save!
account.save!
# raise ActiveRecord::Rollback # 手動で Rollback を実行したりもできる
end
rescue ActiveRecord::RecordInvalid => e
# 例外処理
rescue => e
# 例外処理
end
end
一応ネストも可能だが、ネストされた先で ::Rollback
を叩けなくなったり、分岐処理が複雑になるため可能な限り避ける。
モデルA.transaction do
#モデルAでやりたい処理
モデルB.transaction do
# モデルBでやりたい処理
モデルC.transaction do
# モデルCでやりたい処理
end
end
end
ActiveRecordのコネクションプールの理解を深める
並列マルチプロセスブロック内でActiveRecordトランザクションを使用する方法は?
Parallel
を利用してマルチ ( 並列 ) プロセスで、かつ各プロセスで ActiveRecord::Base.connection_pool.with_connection
で DB 接続を使いまわして、かつトランザクション張る ... という場合の書き方。
# in_threads で並列プロセスのワーカータイプを「プロセス→スレッド」へ明示的に変更
Parallel.each(new_records, in_threads: 8) do |record|
ActiveRecord::Base.connection_pool.with_connection do
ActiveRecord::Base.transaction do
edit_record(record)
record.save!
end
end
end
ユーザークラス - railstutorial.jp
【attr_accessor】って?
ActiveModel::Attributes が最高すぎるんだよな。
Rails Virtual Attributes
Railsで学ぶ責務のお話
class User
# Ruby の attr_accessor で @name, @email へのアクセサを自動登録
attr_accessor :name, :email
# インスタンス化された際のコンストラクタで @name, @email を用意
def initialize(*) # 可変長引数 * をとり以下 super へ投げる
super # オーバライドすることになるので super で親処理を呼んでおくこと
@name = self.name # super で self が組みあがってる
@email = self.email
end
# MVC 各所で利用する仮想プロパティみたいなものをつくる
# `puts "#{@user.name} <#{@user.email}>"` という
# 出力上のロジックをコードのあちこちにまき散らさなくて済む
def formatted_email
"#{@name} <#{@email}>"
# ここまで書いてなんだけど attr_accessor と initialize 使わなくても
# ここで "#{self.name} #{self.email}" って書いてもとりあえずは動きます
end
end
- ActiveRecord コールバック - railsguides.jp
- ActiveRecord の attribute 更新方法まとめ
- 各種メソッドで callback や validate が走るかどうかまとめてる
- Railsの意外な落とし穴? before_xxx で false を返すと処理を停止するが、 nil を返すと続行する
- ActiveRecordにおけるdestroyとdestroy!の違い
- Callback でバリデーションしてコントローラでハンドリングする例
- has_many とかの関連定義の前に置くかどうかで挙動が変わることあるので注意
アプリケーションの中で大きなビジネスロジックに膨れそうなものは callback で作り込むとすぐ死ぬので注意。バリデーションは極力 validates でやって、本当に普遍的な callback だけにとどめたほうがいい。
callback を大量に仕込む代替手段としては、多分 observer パターンで event 発行したときに service class を do するみたいにした方が良さそう。
バッチ処理や rspec なんかで callback skip したいときは skip_callback
とかもある。但しスレッドセーフでないため、ご利用は計画的に。
# https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-skip_callback
User.skip_callback(:save, :after, :fuga)
user = User.find(1)
user.name = 'john'
user.save # after_save の fuga メソッドスキップされる
あとは attr_accessor つかってフラグっぽく使う手段も。
# Model
attr_accessor :skip_my_callback
after_save :my_callback, unless: :skip_my_callback
#controller
Model.create skip_my_callback: true
Rails enumについてまとめておく
ActiveRecord::Enum
Rails5 から enum 使う時は_prefix(接頭辞)_suffix(接尾辞)を使おう
よくある「 xx タイプ」みたいな固定値を割り振って「ステータス」や「タイプ」を管理するアトリビュートは enum
メソッドで「ステータス - 未読:1、既読2」みたいな感じで定義するのが Rails 4 から可能になっている。
Rails 4 時点では where 句でシンボル使えないので注意。
class Article < ActiveRecord::Base
# enumの定義(キーと数字のハッシュを渡す。数字がDBカラムに設定される)
enum status: { drafted: 0, published: 1 }
end
# マイグレーションはこんなかんじ
class CreateArticles < ActiveRecord::Migration
def change
create_table :articles do |t|
t.integer :status, default: 0, null: false, limit: 1
t.timestamps null: false
end
add_index :articles, :status
end
end
# 勝手に確認メソッドが override & 追加される
article = Article.first
article.status # 'drafted' とかが返る
article.drafted? # status により bool を返す
article.published? # status により bool を返す
article.published! # status を published へ変更
# enum 一覧を取得
Article.statuses
=> { drafted: 0, published: 1 }
Rails 4 以前に開発されたシステムでは enum
を利用せず、以下のように Module で切り出してクラス定数化しちゃうのが一般的だったみたい?
class Message < ActiveRecord::Base
module ReadStatus
UN_READ = 0
READ = 1
LABEL = { UN_READ => '未読', READ => '既読' }
end
READ_STATUSES = [ReadStatus::UN_READ, ReadStatus::READ]
end
class Song < ActiveRecord::Base
# Uses an integer of seconds to hold the length of the song
def length=(minutes)
super(minutes.to_i * 60)
end
def length
super / 60
end
end
ActiveModel::Serialization
ActiveModel::Serializers::JSON ActiveModel ::シリアライザで条件付きでサイドローディング
ActiveModel は to_json や to_xml のときデフォルトで「全 attributes を吐く」ため、秘匿情報を不用意にシリアライズしたくないときに attributes のオーバライドが必要。
class User < ActiveRecord
def attributes
# except は ActiveSupport メソッドなので注意
# あとここはシンボル使えません
super.except('encrypted_token')
end
end
# User.first.to_json や to_xml に encrypted_token がのらなくなる
# https://qiita.com/eggc/items/29a3c9a41d77227fb10a
# 基本
Book.first.to_json
Book.all.to_json
# 特定のカラムを出力しないようにする
Book.first.to_json(except: [:id, :body])
# 特定のカラムだけを出力する
Book.first.to_json(only: [:title])
# メソッドの結果をカラムのようにして出力する
Book.first.to_json(methods: [:summary])
Book.first.to_json(methods: [:summary], only: [:summary]) # summary 以外出力しない
# 関連モデルを出力する
Book.first.to_json(include: [:author, :publisher])
Book.first.to_json(include: [:author, {publisher: {only: :name}}])
Book.first.to_json(include: {author: {include: :group}})
# 詳細に指定
Book.all.to_json(
only: [], # Book のカラムはすべて不要
include: {
publisher: {} # Publisher のオプションは無し(デフォルトで出力)
author: {
include: {
group: { # Group は group_name メソッドの結果だけ出力
methods: :group_name
only: :group_name
}
}
}
}
)
Active Record の関連付け (アソシエーション)
ActiveRecord::Associations::ClassMethods
# 等価?
john = User.new name: 'John', email: 'john@example.com'
john.articles.build({ title: 'My Article 1', body: '....' }, { title: 'My Article 2', body: '....' })
john.save
johns_article_1 = Article.new title, 'My Article 1', body: '....'
johns_article_1.build_user name: 'John', email: 'john@example.com'
johns_article_1.save
johns_article_2 = Article.new user: johns_article_1.user, title, 'My Article 2', body: '....'
johns_article_2.save
john = User.new name: 'John', email: 'john@example.com'
johns_article_1 = Article.new user: john, title: 'My Article 1', body: '....'
johns_article_1.save
johns_article_2 = Article.new user: john, title: 'My Article 2', body: '....'
johns_article_2.save
JOIN しまくって遠めの関連をたどりたいときは :through
で間に入るモデルを中間テーブルとして扱わせるのが楽。
class User < ActiveRecord::Base
has_many :tweets
has_many :favorites, through: :tweets
end
user = User.first
user.favorites # ユーザのツイートにぶら下がるお気にいりカモン!
# ↑ のようにしないで、近い relation しか定義してないと辿るの結構面倒
# 例えば user.tweets.favorites とは書けない ( tweet.favorites なので
# 複数ある tweets を飛ばして favorites にいけるのが楽なポイント
因みに Scoped Relation でも :source
オプション使えばイケる。下手に導出用のモデルメソッド生やしたり Query Object 作ったりするより、関連で表しきったほうが後から見直したときもわかりやすい気がする。
class User < ActiveRecord::Base
has_many :tweets
has_many :tweet_images, through: :tweets
#
# お気に入りされた画像つきツイートの Scoped Relation
#
has_many :favorited_has_image_tweets, lambda {
joins(:tweet_images, :favorites) # INNER JOIN で持ってないやつ落とす
order(created_at: :desc)
}, class_name: 'Tweet'
#
# ↑ を through した仮想 relation 、source で実際のモデル指定
#
has_many :favorited_images, through: :favorited_has_image_tweets, source: :tweet_image
end
user = User.first
user.favorited_images # お気に入りされた画像たち
https://shoken.hatenablog.com/entry/2015/07/14/095211
inverse_ofを指定したリレーションのある2つのモデルでは、双方から同一のインスタンスを参照できるようになる。両者ともメモリ上で同一のインスタンスとして扱われる。逆に、inverse_ofの設定が無いと同一として扱われず、一方からの変更がもう一方から参照しても変更されていない。
なんど調べてもわからんが、ざっくり「逆参照を明示しておいて、あきらかに同じレコード見てるものは SQL 発行 → ActiveRecord 生成を省略してメモリ上の同一インスタンスの参照を渡して節約しよう」ってことみたいです。
- rails 4.1 からは自動決定されてる
- :through や :foreign_key など一部オプションとの併用時には ↑ の自動決定が働かないらしい
- rubocop さんにそれで怒られる
- とはいえ :class_name で指定していて、元の class_name 側の自動決定がちゃんと動作してることもある
- とりあえず逆の設定をしておけばいいみたい
class Member < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :member, inverse_of: :comments
end
comment = Comment.where(member_id: 1).last
member = comment.member
same_comment = member.comments.last
same_member = same_comment.member
#
# ↑ この same_member 探索時点で SQL 発行されない
# inverse_of がないと SQL 発行され別インスタンスとして new される
member.email #=> 'test@example.com'
same_member.email #=> 'test@example.com'
# メモリ上で同一インスタンスなので attribute の変更も同期される
# inverse_of がないと別インスタンスなので同期されない
#
member.email = 'modified@example.com'
member.email #=> 'modified@example.com'
same_member.email #=> 'modified@example.com'
「 1 つしか持たせたくない」を強制するなら unique 制約がいるかなーやっぱ、という印象。「要件的には 1 つ」なんだけど「システム側では一時的に複数持つ瞬間がある」なら has_many にしておいて has_one を scoped で定義した方が柔軟性がありそう。
- has_one と belongs_to についての復習
- Rails で has_one の関係を作るとレコードの状態も has_oneを維持しようとする
user.profile = another_profile
したときは id 書き換え ( has_one 維持 ) してくれる- 但し
Profile.create(user_id: 1)
みたいにしたときは has_one 維持できない ( そこまで見てくれない
- has_oneのcreateとbuild and saveの違い
- ↑ のように create はとにかく新規レコードを作ろうとするのに対して save は空気を呼んで
古いやつを delete してから insert
してくれる
- ↑ のように create はとにかく新規レコードを作ろうとするのに対して save は空気を呼んで
- そこはhas_oneを使いましょうよ
- has_many Article のうち LastestArticle を scoped な has_one で持つ例
class User < ActiveRecord::Base
has_one :password_reset, dependent: :destroy
end
class PasswordReset < ActiveRecord::Base
belongs_to :user
before_save { unique_token(self.class, :token) }
after_save { UserMailer.password_reset(self.user).deliver! }
end
user.build_password_reset.save # delete が走ってから insert になる
user.build_password_reset.save # 2 叩いても ↑ のおかげで has_one が保たれる
class User < ActiveRecord::Base
has_many :posts
has_one :latest_post, lambda {
order(created_at: :desc)
}, class_name: 'Post', inverse_of: :user
end
class Post < ActiveRecord::Base
belongs_to :user
end
user = User.last
user.posts
user.latest_post
ActiveRecord::Associations > Customizing the query
Scopes
:class_name
How to Preload Rails Scopes
- limit は使うな、普通の
scope
で絞れっていう思想みたい
class User < ApplicationRecord
has_many :articles, dependent: :destroy
has_many :public_articles, -> {
# Scope block of association.
where is_public: true
}, class_name: 'Article'
end
# users_controller
def index
@users = User.includes(:public_articles).all
end
class User < ActiveRecord::Base
has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event'
end
# 多分こう
User.birthday_events(current_user)
# このようなインスタンス依存スコープのプリロード ( joins など ) はサポート外
User.includes(:birthday_events) # そもそも引数渡せないし仮に渡せても動きません
class User < ApplicationRecord
# こんなアソシエーションのとき ...
has_one :role
has_many :articles
# 以下設定でビューの form_helper での一括登録/編集が可能になる
accepts_nested_attributes_for :role
accepts_nested_attributes_for :articles
end
:dependent
restrict_with_exception と restrict_with_error の違いをサンプルコードで確認する
# 指定 dependent: :destroy
:destroy # 通常の削除 ( デフォルト )
:delete # コールバックをスキップして DELETE ( has_one 用 )
:delete_all # コールバックをスキップして DELETE ( has_many 用 )
:nulltify # コールバックをスキップして NULL
:restrict_with_exception # 例外が発生
:restrict_with_error # エラーが付与される
Article が has_and_belongs_to_many :likes
なとき、Article が消えたら User たちが記事につけた各 Like が消えて欲しい ... みたいなケース。
Article が has_and_belongs_to_many :likes
に dependent: :destroy
を付けた場合、中間テーブル articles_likes
のレコードは Article の削除時に消えるが、各 User の Like は消えないような挙動になる。このとき User の Like は不要になるはずなので、以下のようにして削除にコールバックを仕込んで殺す。
class Article < ApplicationRecord
has_and_belongs_to_many :likes, dependent: :destroy
before_destroy do
# Article 削除時に Like マスタから多対多で当該 Article を参照するはずの Like を殺す
likes.each { |like| like.destroy }
end
end
class Address < ApplicationRecord
belongs_to :customer, optional: true # アドレス帳は顧客に非依存である
end
ActiveRecord::Persistence#touch()
touch, touch, updated_at に touch
class Anime < ActiveRecord::Base
has_many :characters
end
class Character < ActiveRecord::Base
belongs_to :anime, touch: true
end
@character.save
# このタイミングで @character.anime.touch と同じ処理が走り
# 親モデルの updated_at が更新される
# published_at みたいな独自カラムがある場合はマニュアルを見てね
touch を一時的に無効にしたい ( どうせバッチとか通知とか ) ときは ↓ のように no_touching
ブロックで囲えばおk。
User.no_touching do
user.photos.find_each do |photo|
# userは touch されない
photo.update!(some_attributes)
end
end
# 基本の流れ
class Person < ApplicationRecord
validates :name, presence: true
#
# 独自バリデーションを組みたい場合は validate を使ってメソッド名を指定
# validate :custom_validation_method, on: :create
#
# private
#
# def custom_validation_method
# if うにょなら errors.add するなり好きにして
# end
end
p = Person.new # => #<Person id: nil, name: nil>
p.errors.messages # => {}
p.valid? # => false
p.errors.messages # => {name:['空欄にはできません']}
p = Person.create # => #<Person id: nil, name: nil>
p.errors.messages # => {name:['空欄にはできません']}
p.save # => false
p.save! # => Exception ActiveRecord::RecordInvalid: Validation failed: 空欄にはできません
Person.create! # => ActiveRecord::RecordInvalid: Validation failed: 空欄にはできません
# Errors
Person.new.errors.any? # => false
Person.new.errors[:name].any? # => false
Person.create.errors[:name].any? # => true
person.valid? # => false & バリデーショントリガ
person.errors.details[:name] # => [{error: :blank}]
# バリデーショントリガー Model メソッド
valid? / invalid?
create / create!
save / save!
update / update!
# バリデーションをスキップする Model メソッド
save(validate: false)
toggle!
touch
decrement! / decrement_counter
increment! / increment_counter
update_all / update_attribute / update_column / update_columns / update_counters
# 空でない/空である
validates :title, presence: true
validates :title, absence: true
# ユニーク/一致
validates :name, uniqueness: true
validates :title, uniqueness: { scope: :user_id } # 他カラムとの掛け合わせで
validates :email, confirmation: true # 自動で %{attribute}_confirmation と比較
# 列挙に含まれる/含まれない
validates :is_published, inclusion : { in: [true, false] }
validates :subdomain, exclusion: { in: %w(www us ca jp) }
validates :size, inclusion: {
in: %w(small medium large), # 配列オブジェクトへ
message: "%{value}は有効な値ではありません" # message: では ${value} %{attribute} %{model} などが取れる
}
# チェックボックス
validates :terms_of_service, acceptance: { message: '規約に同意してね' }
# 長さ / length
validates :title, length: { minimum: 1 } # 「1文字以上」
validates :title, length: { maximum: 75 } # 「75文字以下」
validates :title, length: { in: 1..75 } # 「1文字以上75文字以下」
validates :password, length: { is: 8 } # 「8文字のみ」
# has_many や has_and_belongs_to_many の個数制限
validates :tags, length: {
maximum: ->(object) do
object.plan.tag_quantity
end
}
# フォーマット / 数値
validates :age, numericality: true
validates :age, numericality: { only_integer: true }
validates :age, numericality: { greater_than: true }
validates :age, numericality: { greater_than_or_equal_to: true }
validates :age, numericality: { equal_to: true }
validates :age, numericality: { less_than: true }
validates :age, numericality: { less_than_or_equal_to: true }
validates :age, numericality: { odd: true }
validates :age, numericality: { even: true }
# フォーマット / 正規表現
require 'uri'
validates :email, presence: true, uniqueness: true, format: { with: /\A#{URI::MailTo::EMAIL_REGEXP}\z/ }
validates :url, presence: true, format: { with: /\A#{URI::regexp(%w(http https))}\z/ }
# 条件付き / :on
validate :active_customer, on: :create
# 条件付き / :allow_nil, :allow_blank
validate :email, confirmation: true, :allow_blank # nil の場合にスキップ
validate :email, confirmation: true, :allow_blank # blank? => true の場合にスキップ
# 条件付き / :if ~ ならバリデート実行, :unless ~ でないならバリデート実行
validates :password, length: { minimum: 12 }, if: is_admin?
# 真偽値を返すプロシージャ ( クロージャ ) で条件を定義
validates :password, confirmation: true,
unless: Proc.new { |user| user.password.blank? }
# Lambda 式で定義
validates :password, confirmation: true, unless: -> user { user.password.blank? }
# シンボルで定義
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == 'card'
end
Active Record の基礎
Active Record クエリインターフェイス
ActiveRecord::Associations::CollectionProxy
Ruby on RailsのCRUDを学ぶ。Update編。updateコマンドとsaveコマンド。バリーデータの有無
Rails の ORM は アクティブレコードパターン を採用している。
最もシンプルなレコード取得 find() / find_by!()
は失敗時に ActiveRecord::RecordNotFound 例外を投げるため、コントローラで「存在している & 自身の所有 ( 権限がある ) を確認する場合は積極的に利用して例外で 404 に落とすようにするのがきれい。
# SELECT
Book.all # Books || Relation - 一括取得でメモリ即死するやつ
Book.first # Book || nil - second third や take / last も同等
Book.find(7) # Book || Exception - ID 付与なのでなければ例外発生
Book.where(id: 7) # Book || Relation - クエリ続行 .nonzero? とかで存在確認
Book.find_by id: 7 # Book || nil - マッチした最初のレコードを返却
Book.find_by! id: 7 # Book || Exception - なければ例外!パターン
Book.exists?(id: 7) # true || false - 固めの存在確認
# INSERT
## new
user = User.new name: 'John', email: 'john.do@example.jp'
user.new_record? # true
user.save # true / DB へ新規作成
## create
user = User.create({name: 'Jane', email: 'jane.do@example.jp'})
user.new_record? # false / create で生成と DB 保存実行済み
user.persisted? # true / DB に存在するかどうか
# UPDATE
## update
user.update({ email: 'new.email@example.jp' })
## save
user.attributes = { name: 'Chloe', email: 'chloe.lucifer@example.jp' }
user.email = 'chloe.does.lucifer@example.jp' # 更新は ← でも ↑ でも OK
user.save
## save / save! / create! / errors
user = User.new
user.attributes = { email: 'john.do@example.jp' }
user.save # false / 失敗したため DB 変更なし
user.errors.full_messages # ["Email is already exists."]
user.save! # ← も ↓ も ActiveRecord::RecordInvalid 例外
User.create! name: 'John', email: 'john.do@example.jp'
## reload
user.reload # DB 保存されてない dirty なカラム変更を DB 状態へ戻す
## update_all / update_attribute
user.update_attribute(is_active: false) # バリデーションをスキップして更新
User.where(is_active: true).update_all(is_active: false) # こちらもバリデーション省略につき注意
# DELETE
## destroy / destroy_all - ActiveRecord を介した ( dependent 削除コールバック実行 ) 削除
# class User < ApplicationRecord
# has_many :posts, dependent: :destroy
# end
User.find(1).destroy # user#1 と配下の依存モデル全削除
User.find(2).posts.destroy_all # user#2 配下の全 posts とその配下依存モデル全削除
## delete / delete_all - ActiveRecord を介さない SQL による DELETE
User.find(3).delete # user#3 DELETE ( 依存モデルのふるまいは外部キー制約による )
User.find(4).posts.delete_all # user#4 配下の全 posts DELETE ( 依存モデルのふるまいは外部キー制約による )
既にセットされた where / order 条件は rewhere / reorder で上書きできる。( このくらい中でうまいことやって欲しい気もするけど
Article.where(trashed: true).rewhere(trashed: false)
# SELECT * FROM articles WHERE `trashed` = 0
where_values_hash でセットされている where 条件が hash で取得できるので「 IN 句に追加」とかもできる。
statuses = [99]
where_hash = @relation.where_values_hash
if where_hash.key?('status')
statuses.push(*where_hash['status'])
end
@relation.rewhere(status: statuses)
EXPLAIN を実行する - railsguides.jp
PostgreSQL: Using EXPLAIN
MySQL: EXPLAIN Output Format
大量データ検索のためのデータベース対策あれこれ
行数が単一テーブルだけで 100 万行あるような中/大規模な DB 相手にテキトーな Query を叩くと、いくらインフラをスケールアップさせていても RDBMS は単純な SELECT 文であっというまにハングしてしまう。( 基本的には index を適切に貼っていれば大丈夫だが ... )
ActiveRecord は .explain
メソッドで各 RDBMS の EXPLAIN - クエリー実行プラン を整形出力することができる。特に type: ALL
なフルテーブルスキャンを行うようなクエリは大規模 DB では必ず死ぬ、ORM によって組まれた SQL が「実行結果が期待通りか」だけでなく「パフォーマンス上問題ないか」を確認するために積極的に利用すること。
User.where(id: 1).joins(:articles).explain
EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
+----+-------------+----------+-------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+----------+-------+---------------+
| 1 | SIMPLE | users | const | PRIMARY |
| 1 | SIMPLE | articles | ALL | NULL |
+----+-------------+----------+-------+---------------+
+---------+---------+-------+------+-------------+
| key | key_len | ref | rows | Extra |
+---------+---------+-------+------+-------------+
| PRIMARY | 4 | const | 1 | |
| NULL | NULL | NULL | 1 | Using where |
+---------+---------+-------+------+-------------+
ActiveRecord::Relation
ActiveRecord::Relationとは一体なんなのか
覚えておくと幸せになれるActiveRecord::Relationメソッド6選
【Rails5】Active Record のクエリCakePHP の
Cake\ORM\Query
オブジェクトみたいなもん。
# basic
User.where(name: 'taro')
User.where(name: ['taro', 'ziro'])
# and
User.where(name: 'taro').where(age: 10)
User.where(name: 'taro', age: 10)
# not
User.where.not(name: 'taro')
# or
User.where(gender: :male).or(User.where(gender: :female))
User.where(name: 'taro', gender: :male)
.or(User.where(age: nil)) # age: nil な female も含まれる
# placeholder
User.where('age > :age and email != :email', age: 18, email: nil)
# like
User.where('name like :name', name: "%#{name}%")
# order
User.order(:id)
User.order(id: :asc)
User.order(name: :asc, id: :asc)
# order by field - http://mikamisan.hatenablog.com/entry/2017/03/22/230615
User.where(id: ids).order(['field(id, ?)', ids])
# select
User.select(:name)
User.select(:id)
#
# AS で別名つけて through 中間モデルの attribute を has_many 先にくっつける
# https://blog.kotamiyake.me/tech/join-attributes-to-relation-table/
#
# 例: User は複数の Account を through した Role 違いで持てるとして
# User.first.accounts 時に through している Role の attribute が欲しい
#
user = User.first
user.accounts.select('accounts.*, roles.name AS role_name').first.role_name # admin 的な
# limit / offset
User.limit(5)
User.limit(5).offset(10)
# count
User.where(condition).count # 都度 COUNT SQL 発行
User.where(condition).size # COUNT SQL 発行 → キャッシュあればそちらを利用
User.where(condition).length # 取得結果の個数返却 ( to_a.length と同じ?
# count + distinct
#
# 通常 User.where(condition).count で OK だが
# リレーションを無視した INNER JOIN とかしたときに
# JOIN したカラムで DISTINCT するようなときは指定してやる
#
# https://qiita.com/nysalor/items/ec0c0a45c78170bc4d73
#
users = User
.joins('INNER JOIN not_related_articles ON users.id = not_related_articles.user_id ')
.distinct('not_related_articles.id')
users.count('DISTINCT not_related_articles.id')
# distinct
#
# has_many を inner join するときなどは条件によるが基本重複するので忘れずに
# 但し、大きなテーブルにかけると distinct は結構重たいので注意
# FYI: https://www.greptips.com/posts/386/
#
User.joins(:articles).where(articles: { published: true }).distinct
# average / minimum / maximum / sum
User.average(:age) # 22
User.maximum(:age) # 61
User.minimum(:age) # 13
User.sum(:point) # 13
# group / having
User.group(:rank).count # ユーザランク毎のカウント
User.group(:rank).having(rank: ['bronze', 'silver', 'gold']).count # 条件付き
# ids
User.ids # [1, 2, 3 ... ]
# pluck
User.pluck(:id, :name) # [[1, "email.one@example.jp"], [2, "email.two@example.jp"]]
#
# 既にメモリ上に展開された結果セットでないなら
# User.all.map(&:id) とかするより pluck(:id) のが早い
# あと↓みたいな使い方もできる
#
# Ref: Rails ActiveRecordでランダムにレコードを1件取得する
# https://easyramble.com/get-record-randomly-with-active-record.html
#
# user.articles.sample(3) とかすると全取得になって重いので
# Article.find(user.articles.pluck(:id).sample(3)) とかにできる
# none
User.count # 100
User.none # 本当は存在するがダミーとして空の Relation が返る
# find_or_create_by / あれば取得、なければ作成 find_or_create_by! で新規作成失敗時に例外
# find_or_initialize_by もあり、こちらはなければ初期化 ( 保存はまだしない )
Tag.find_or_create_by(:name => 'ruby') do |tag|
tag.type = 'programming'
end
# scoping / ブロックへ Relation を引き渡せる
Comment.where(:post_id => 1).scoping do
Comment.first # SELECT * FROM comments WHERE post_id = 1
end
# to_sql / explain
User.where(1).to_sql # 実行 SQL 文が返る
User.where(1).explain # 実行 SQL がコンソール用に成形されて出力される
find_each も find_in_batches も limit や order は無視される から気をつけてね。
# each / 一括取得 → 一括メモリ展開ループ ( 必ずクエリに LIMIT つけること )
User.all.each { |user| puts user.email }
# find_each / 分割取得 ( デフォルト 1000 件 ) → レコード毎処理
User.where(age: 10..20).find_each { |user| puts user.email } # バッチなので処理順の指定はできない
User.where(age: 10..20).find_each(batch_size: 100) { |user| puts user.email } # 分割サイズ変更
# find_in_batches / 分割取得 ( デフォルト 1000 件 ) → レコードを配列展開し配列毎処理
# レコード毎処理じゃない以外は find_each と同等
# find_each と同様に batch_size 指定や start / finish で開始終了の主キー指定が可能
User.all.find_in_batches(start: 100, finish: 300) { |user| puts user.email }
CakePHP のカスタム Finder のように任意の Relation にシンボルを付けて固めておける。
# app/models/article.rb
# デフォルトスコープ指定 ( スコープ指定していない全検索クエリに適用される )
# あたりまえだが全クエリに暗黙で追加されるので乱用すると即死する、ご利用は計画的に。
default_scope :published, -> { where(published: true) }
# カスタムスコープ定義
scope :published_after, -> (time) { where('published_date >', time) }
# app/controllers/articles_controller.rb
def index
@articles = Article.published_after(Time.now())
end
前段 Relation からクエリ条件を引き継ぎ、条件をさらに結合させることができる。内部的には SQL が
()
で囲われるっぽい。
# merge で joins 先のモデルの条件づけを行う
User.joins(:roles).merge(Role.where(name: :manager))
# or + join / merge はいったんクエリを保存してから
relation = User.joins(:roles)
relation.merge(Role.where(name: :manager))
.or(relation.where(gender: :male) # マネージャロール持ちユーザまたは男性ユーザ
joins
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い
Rails における内部結合、外部結合まとめ
関連するモデルの条件で検索したいjoins は純粋 INNER JOIN 用のメソッドで、関連を親テーブルにくっつける。配下モデルと必ずセットで取得/検索を行いたいときのやつ。ちなみに INNER JOIN なので関連が存在しなければ親レコードは NULL るし配下モデルのアソシエーション構成によっては
.distinct
しないと被りが出る。また、クエリに JOIN をせず関連モデルについて SQL を複数回発行して内部キャッシュし、結果セットに「後から関連モデルデータをくっつける」といったパフォーマンスを考慮した
preload
メソッドというのもある。
# 2013 年映画に出演したアクター全て
Actress.joins(:movies).where(movies: { year: 2013 })
Actress.joins(:movies).merge(Movie.year_2013) # merge を利用してこうも書ける
# コメントとタグを保持する記事を保持するユーザ全て
User.joins(articles: [:comments, :tags]).distinct
# 複数のテーブルからのデータをフィルタして取得する
Person
.select('people.id, people.name, comments.text')
.joins(:comments)
.where('comments.created_at > ?', 1.week.ago)
# 複数のテーブルから特定のデータを取得する
Person
.select('people.id, people.name, companies.name')
.joins(:company)
.find_by('people.name' => 'John') # 名を指定
railsでのeager_loadの結合条件の追加はできますか?
joins 以降の配下モデルへの where は CakePHP の matching のように配下モデルベースで親モデルを条件づけ する。INNER JOIN する配下モデルに対して条件付けし、条件一致する配下モデルを持たない親モデルも取得する ... つまり CakePHP3 の
Cake\ORM\Query::contain()
+ 配下モデルへのwhere()
条件クロージャのような 特定条件配下モデルのみ INNER JOIN したい場合は eager_load による事前の LEFT OUTER JOIN 先読みキャッシュ + joins への SQL による AND 条件追加を利用 する。あんまり自信ないケド、以下では eager_load で関連配下モデルを LEFT OUTER JOIN で先読みキャッシュして、後続の joins の挙動が INNER JOIN から LEFT OUTER JOIN に変更されている。ここで SQL を引数にとれる joins に対して
AND models.attribute = ?
のように条件を渡すことで、LEFT OUTER JOIN の ON 条件が加わり、結果的に eager_load キャッシュから取得する配下モデルレコードが条件で絞りこまれて、親レコード側は配下レコードがないときも NULL にならない ... ということだと思う。多分。ここまで書いておいてナンだけど、部分的な実装でないなら後述する Scope Block Association + includes で対処したほうがスマート。但し current_user が所有する配下モデルだけ ... のように条件が動的なものはコレで対処せざるを得ない。
# Article は様々な User から Like されている
#
# :users has_many :likes
# :articles has_many :likes
# :likes belongs_to :user, :article
#
# Like している / いないに関わらず Articles を全件取得したい
# 加えて current_user がつけた Like がある Article に関して
# 当該 Like のみを Article に INNER JOIN した状態で取得したい
# * has_many なので current_user の Like 1 つ分を likes 配列内に保持
# * Like していない Article の likes は条件なし eager_load 時同様に空配列
#
Article
.eager_load(:likes)
.joins("AND likes.user_id = #{current_user.id}").all.each { |article|
print "#{article.name}"
unless article.likes.empty?
print " ( Liked at #{article.likes.first.created_at} )"
end
print "\n"
}
#
# たとえば Articles が全 3 件で「記事その2」だけ Like してないとすると ...
# ===
# 記事その1 ( Liked at 2017-10-19 23:00:38 UTC )
# 記事その2
# 記事その3 ( Liked at 2018-12-23 11:31:12 UTC )
上記例で 1 点注意があり eager_load().joins()
以降に .includes()
を挟むと絞り込みがリセットされてしまう ことに注意。後続に eager_load を挟んでも preload を挟んでも大丈夫だが、なぜかはわかんないケド includes を挟むと joins の AND ...
で絞った条件が解除されてしまう。includes をクエリにつけた段階で、前段までの LEFT OUTER JOIN 条件が再構築されるのかもしれない ... 。
sanitize_sql_for_conditions のエイリアス
joins に限らないが、プリペアドステートメントのプレースホルダを手動で作成するやつ。この他 sanitize_sql_for_order や sanitize_sql_like とかも使えそう。
# モデルクラス内なら親クラス指定 ( ActiveRecord::Base ) いりません
User.eager_load(:likes)
.joins(ActiveRecord::Base.sanitize_sql([
'AND likes.user_id = :user_id AND likes.is_dislike = :is_dislike',
user_id: 1,
is_dislike: false,
])
ActiveRecordでサブクエリ(副問い合わせ)と内部結合
Rails の where は結構柔軟で
where(model: {id: :relation})
みたいなものも突っ込める。
# タグ 'ruby' を保持する記事 ... を保持するユーザが全員欲しい
User
.joins(:articles) # ここは関連をロードする必要がなければ left_outer_joins でも OK
.where(articles: { # INNER JOIN した :articles で絞り込みをするぜ!
id: Articles # id: Relation で内部的に ID IN (SELECT ...) みたいなサブクエリが走る
.joins(:tags)
.where(tags: {
name: 'ruby'
})
})
.all
INNER JOIN ではなく LEFT OUTER JOIN で配下モデルが存在しなくても親レコードを取得したいときのやつ。
似たようなメソッドに
eager_load
があり、こちらは関連モデルを LEFT OUTER JOIN で先読みキャッシュする。preload
との使い分けはケースバイケースだが、こちらは SQL 発行が 1 回で済むので、テーブルが巨大でない & JOIN はしたいケースではこちら。
# 以下では authors が posts を持っているかどうかにかかわらず
# すべての著者とその記事の数をグループで返す
# 著者 A ( 0 ), 著者 B ( 11 ), 著者C ( 2 ) ... みたいな
Author.left_outer_joins(:posts)
.distinct.select('authors.*, COUNT(posts.*) AS posts_count')
.group('authors.id')
# ページネーションでよくある limit & offset との合わせ技
# 配下関連モデルにより取得レコードに重複が発生する可能性があるので
# group や distinct による絞り込みをしている
page = params[:page] ||= 1
per_page = 10
Feed.limit(per_page).offset(per_page * (page.to_i - 1))
.order(feed_published_at: 'DESC')
.left_outer_joins(:tags, resource: [:brand])
.group('feeds.id')
# left_outer_join 時は自モデルに外部モデルのカラムがくっつかないので
# where とか厳密にやらんといかんです
# 以下は left_outer_join x 外部モデルの where in パターン
query = Article.left_outer_joins(:tags)
if params[:tags] # クエリ文字列で ?tags[]=hoge&tags[]=fuga みたいなの受けてる想定
query = query.where('tags.id in (:tags)', tags: params[:tags]) # プレースホルダ
end
@articles = query.pluck(:id, :title, :body)
関連付けを一括読み込みする
ActiveRecord ~ 複数テーブルにまたがる検索Active Record は関連モデルを「アクセスされたタイミングで」 SQL 発行して取得する。この機能に頼りすぎると N + 1 問題を引き起こすため、includes で予め関連をロードしてキャッシュしておくとパフォーマンス的によろしい。内部的には「後続クエリに絞り込みが入っているかどうか」で
preload
とeager_load
を切り替えているだけ。
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
# rails はこのとき client がまだ保持してないネストした関連をよしなにクエリ実行して取得する
# よって 11 のクエリ実行がおこり、レコード数が増加したときにオーバヘッドが大きくなる
end
# 上記コードを includes で解決する例
clients = Client.includes(:address).limit(10)
clients.each do |client|
puts client.address.postcode
# client は既に address を保持しているためクエリ実行がおきない
# clients 取得 + addresses.client_id IN (....) といった 2 回のクエリ実行で済む
end
# includes 後に where することも可能
Article.includes(:comments)
.where(comments: { visible: true }) # 但しこの場合は joins を利用したほうがよい
# 階層が深いときはこうじゃ
User.includes(projects: [tasks: [:tags, :category]]).all
# ソートはこう ( ハッシュではなく文字列で "テーブル.カラム ASC/DESC" にする )
User.includes(resource: [:feeds]).order('feeds.feed_published_at DESC')
ActiveRecordのincludesに条件を指定する
Conditional Eager Loading in Railsincludes 自体はただの preload / eager_load による関連モデルの事前キャッシュでしかない。このため eager_load + joins AND 条件追加で触れた CakePHP3 の
Cake\ORM\Query::contain()
+where()
条件クロージャのような 配下モデルを条件つきで INNER JOIN するけど、条件一致する配下モデルを持たない親モデルも LEFT OUTER JOIN っぽく欲しいケース は単体で対応できない。includes のみでシンプルにこれを行いたいケースでは 予め親モデル側でスコープブロックを利用して、条件つき配下モデルを「仮想アソシエーション」として定義してフィルタリングした後に
includes()
で関連をキャッシュ する方法で対処するのがよい。こっちのが準備が面倒だけどスマート。
class User < ApplicationRecord
has_many :articles, dependent: :destroy
has_many :public_articles, -> {
# Scope block of association.
where is_public: true
}, class_name: 'Article'
end
# Users に「公開記事があればくっつけて、なければないで」全員集合
@users = User.includes(:public_articles)
# Users のうち「公開記事がある人だけ」全員集合 ( 書き方違うだけで全部同じ結果 )
@users = User.includes(:articles).where(articles: { is_public: true })
@users = User.includes(:articles).where('articles.is_public = ?', true).references(:articles)
@users = User.includes(:articles).merge(Article.where(is_public: true)).references(:articles)
#from - api.rubyonrails.org ActiveRecordを使ってFROM句のサブクエリを書く方法
複雑なサブクエリを FROM に書きたいときは .from
に SQL 打ち込むのが割と何でも対応できる。
User.from(
User.where(何かとても複雑な条件とか),
:users # ここはモデル名というより SQL で実際に吐かれるテーブル名だと思う
).where(id: 1)
ActiveSupport::Inflector.constantize
ActiveSupportのconstantizeが便利
[Rails5] Active Support::Inflectorの便利な活用形メソッド群
The Safest Way to Constantize...CakePHP なら可変変数を利用して
$this->loadModel($model);
して$this->{$model}->find();
みたいにする モデルクラスの動的ロード を Rails でどうやるかというヤツ。基本的にこの手のヤツは良い手段ではないので使いどころ注意。
# Item モデルが存在するとき、以下は Item.find(1) と同じ結果を返す
# 存在しない、同スコープ内で定数が定義済みの場合は例外を投げる
'Item'.constantize.find(1)
# エラー時に例外の代わりに nil を返す safe 版もある
'NotExistsClass'.safe_constantize # nil
# Inflector モジュールの classify で事前にクラス名化してから
model_name = 'public-items'.classify # PublicItem
@record = model_name.constantize.find(1)
ActiveRecord::Calculations
Rails tips: ActiveRecord count系機能の基本と応用(翻訳)
Person.average(:age) # => 35.8
Person.group(:role_id).maximum(:age) # => {1 => 52, 2 => 38, 3 => 23} ロール毎の最大出してくれてるね