2018/02/15
PLAZMA OSS Day: TD Tech Talk 2018
去年の8月にRailsコミッターになったのでIssues/PRをがんばって減らせるようになった
Rails 5.2.0 RC1: Active Storage, Redis Cache Store, HTTP/2 Early Hints, CSP, Credentials
リリースノートに載るような新機能はないけど5.1以前では解決されていなかった多くの問題が5.2のActive Recordでは改善されている
eager_load
の問題count
が複雑すぎる問題apply_join_dependency
忘れすぎ問題association scope
内のJOINを無視する問題eager_load
とはActive Recordで定義したassociationをJOINして親レコードをロードするときに同時に先読みする機能
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
Comment.eager_load(:post).each do |comment|
puts "#{comment.body} : commented on #{comment.post.title}"
end
count
が複雑すぎる問題性能特性を維持するために#26972でサブクエリ内(なぜcount
でサブクエリが生成されるかは後述する)のORDER BYを維持するよう修正したら(そもそもORDER BYを取り除くと結果が異なってしまうケースもある), PostgreSQL + DISTINCT + ORDER BYの制約に引っかかる問題を生み出してしまった
count
が複雑すぎる問題count
が複雑すぎる問題集合関数系メソッドの中でcount
だけLIMITの扱いが特殊(count
する前にLIMITが適応された結果を返す仕様になっている)
posts = Post.order(:id).limit(3)
posts.to_a
# => SELECT posts.* FROM posts ORDER BY posts.id LIMIT 3
posts.count
# => SELECT COUNT(*) FROM (SELECT posts.* FROM posts ORDER BY posts.id LIMIT 3)
posts.count(:published)
# => SELECT COUNT(count_column) FROM (SELECT posts.published AS count_column FROM posts ORDER BY posts.id LIMIT 3)
count
が複雑すぎる問題joins
/left_joins
とincludes
/eager_load
では同じ結果になるとは限らない(eager_load
は主テーブルの件数を維持するためにCOUNT(DISTINCT id)
なクエリを生成する)
post = Post.create!(title: "post1")
Post.create!(title: "post2")
Post.create!(title: "post3")
post.comments.create!([{ body: "comment1" }, { body: "comment2" }])
Post.joins(:comments).count # => 2
Post.left_joins(:comments).count # => 4
Post.eager_load(:comments).count # => 3
count
が複雑すぎる問題joins
/left_joins
とincludes
/eager_load
では同じ結果になるとは限らない(eager_load
は主テーブルの件数を維持するためにCOUNT(DISTINCT id)
なクエリを生成する)
post = Post.create!(title: "post1")
Post.create!(title: "post2")
Post.create!(title: "post3")
post.comments.create!([{ body: "comment1" }, { body: "comment2" }])
Post.joins(:comments).map(&:id) # => [1, 1]
Post.left_joins(:comments).map(&:id) # => [1, 1, 2, 3]
Post.eager_load(:comments).map(&:id) # => [1, 2, 3]
count
で起きていた問題SELECT COUNT(*) FROM (SELECT ...)
)にすればほぼ期待した結果になる
eager_load
のときに付けるDISTINCTだけなのでそのケースだけPostgreSQLにも配慮したクエリが生成されるようにしたapply_join_dependency
apply_join_dependency
はeager_load
をJOINに変換してくれるやつ。eager_load
をJOINにしないと正しいクエリにならないケースでは常にクエリ組み立て前に明示的に呼び出す必要がある。
apply_join_dependency
5.1以前では#to_a
, #to_sql
, #pluck
, #exists?
, 集合関数系(#count
etc)だけでしか明示的に呼ばれてないのでそれ以外のJOINにしないといけないケース(#from
, #where
, #update_all
, #delete_all
, #cache_key
)では正しく動かなかった
apply_join_dependency
5.2では#exists?
や#count
が動くケースではほぼ#update_all
, #delete_all
も動くようになった
posts = Post.joins(:comments).where("comments.hidden": true)
if posts.exists?
assert_equal posts.count, posts.delete_all
end
association scope
内のJOINが無視される問題5.1以前ではeager_load
のassociation scope
内のJOINはすべて捨てられてしまうので、
例えば特定のタグが付いたコメントだけをeager_load
したいようなケースで問題になる。
association scope
内のJOINが無視される問題
class Post < ActiveRecord::Base
has_many :tagged_comments,
-> { left_joins(:tags).where("tags.name": "Tagged") },
class_name: "Comment", inverse_of: :post
end
class Comment < ActiveRecord::Base
belongs_to :post, inverse_of: :tagged_comments
has_many :tags
has_many :tagged_tags, -> { where(name: "Tagged") }, class_name: "Tag"
end
class Tag < ActiveRecord::Base
belongs_to :comment
end
association scope
内のJOINが無視される問題5.2ではJOINを捨てないように修正したので以下の例は動くようになった
class Post < ActiveRecord::Base
has_many :tagged_comments,
-> { left_joins(:tags).where("tags.name": "Tagged") },
class_name: "Comment", inverse_of: :post
end
Post.eager_load(:tagged_comments).first
SQLでは同じテーブルを複数回JOINすると別の名前を付けないとどちらのテーブルのカラムを参照しているのか曖昧になってしまうので、 同じ名前がJOIN内に出現しないようにtable aliasを振っていく必要がある
JOINをこれまで捨てていたのでActive Recordにはassociation scope
内のJOINにまでalias trackingが考慮されていない問題があった。
以下の例はtags
テーブルが2回JOINされてエラーになる。
class Post < ActiveRecord::Base
has_many :tagged_comments,
-> { left_joins(:tags).where("tags.name": "Tagged") },
class_name: "Comment", inverse_of: :post
end
Post.eager_load(tagged_comments: :tags).first
alias trackingを直したので直接テーブル名を指定しないようにしたら動いて欲しかったけど現時点では以下の例は動かない…
class Post < ActiveRecord::Base
has_many :tagged_comments,
-> { left_joins(:tagged_tags) },
class_name: "Comment", inverse_of: :post
end
Post.eager_load(tagged_comments: :tags).first
少し効率は悪くなるけどworkaroundとして以下の例なら動作する
class Post < ActiveRecord::Base
has_many :comments
has_many :tagged_tags, through: :comments, source: :tagged_tags
has_many :tagged_comments, through: :tagged_tags, source: :comment
end
Post.eager_load(tagged_comments: :tags).first
#save
performanceLockWaitTimeout
/StatementTimeout
/QueryCanceled
eager_load
の問題count
が複雑すぎる問題apply_join_dependency
忘れすぎ問題association scope
内のJOINを無視する問題Use a spacebar or arrow keys to navigate