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_dependencyapply_join_dependencyはeager_loadをJOINに変換してくれるやつ。eager_loadをJOINにしないと正しいクエリにならないケースでは常にクエリ組み立て前に明示的に呼び出す必要がある。
apply_join_dependency5.1以前では#to_a, #to_sql, #pluck, #exists?, 集合関数系(#count etc)だけでしか明示的に呼ばれてないのでそれ以外のJOINにしないといけないケース(#from, #where, #update_all, #delete_all, #cache_key)では正しく動かなかった
apply_join_dependency5.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