Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AST を使って ActiveRecord の where の条件式をブロックで記述しよう

osyo
October 29, 2021

AST を使って ActiveRecord の where の条件式をブロックで記述しよう

osyo

October 29, 2021
Tweet

More Decks by osyo

Other Decks in Programming

Transcript

  1. 今日話すこと! User.where("? <= age", 20) を User.where { 20 <=

    :age } とかけるようにしたい!!! ` ` ` `
  2. 自己紹介 名前:osyo Twitter : @pink_bangbi github : osyo-manga ブログ :

    Secret Garden(Instrumental) RubyKaigi Takeout 2021 Use Macro all the time ~ マクロを使いまくろ ~
  3. 自己紹介 名前:osyo Twitter : @pink_bangbi github : osyo-manga ブログ :

    Secret Garden(Instrumental) RubyKaigi Takeout 2021 Use Macro all the time ~ マクロを使いまくろ ~ 銀座Rails これからの Ruby と今の Ruby について 12月25日にリリースされる Ruby 3.0 に備えよう!
  4. 自己紹介 名前:osyo Twitter : @pink_bangbi github : osyo-manga ブログ :

    Secret Garden(Instrumental) RubyKaigi Takeout 2021 Use Macro all the time ~ マクロを使いまくろ ~ 銀座Rails これからの Ruby と今の Ruby について 12月25日にリリースされる Ruby 3.0 に備えよう! BuriKaigi2021 Ruby 2.0 から Ruby 3.0 を駆け足で振り返る
  5. 自己紹介 名前:osyo Twitter : @pink_bangbi github : osyo-manga ブログ :

    Secret Garden(Instrumental) RubyKaigi Takeout 2021 Use Macro all the time ~ マクロを使いまくろ ~ 銀座Rails これからの Ruby と今の Ruby について 12月25日にリリースされる Ruby 3.0 に備えよう! BuriKaigi2021 Ruby 2.0 から Ruby 3.0 を駆け足で振り返る Ruby 3.1 で楽しみな機能は debug.gem と Hash のショートハンド
  6. activerecord-refinements Refinements を利用してブロック内でのみ Symbol#== などを再定義している実装 元々は Ruby 2.0 の Refinements

    実装時に実験的に作られた gem ブロック内の :name == 'matz' は table[:name].eq 'matz' を返す Symbol#== だと table[col].eq val を呼び出すような実装 現在はブロック内で using を適用する事ができなくなっており動かない 1 # ブロック内でのみ再定義した Symbol#== が有効になる 2 User.where { :name == 'matz' }.to_sql 3 # => SELECT "users".* FROM "users" WHERE "users"."name" = 'matz' 4 5 User.where { :name =~ 'tender%' }.to_sql 6 # => SELECT "users".* FROM "users" ("users"."name" LIKE 'tender%') ` ` ` ` ` ` ` `
  7. activerecord-blockwhere #method_missing を利用してブロック内のメソッド呼び出しをフックしている実装 #method_missing の戻り値が arel_table[name] を返す実装 ブロック内の id.eq(1) は

    arel_table[:id].eq(1) を返す 1 Person.where { id.eq(1) }.to_sql 2 # => SELECT "people".* FROM "people" WHERE "people"."id" = 1 3 4 # & で && を模倣している 5 Person.where { id.eq(1) & name.matches('%alice%') }.to_sql 6 # => SELECT "people".* FROM "people" WHERE ("people"."id" = 1 AND "people"."name" LIKE '%alice%') 7 8 # 関連先を join する 9 Person.where { name.eq('alice') & entries.name.matches('%hello%') }.to_sql 10 # => SELECT "people".* FROM "people" 11 # INNER JOIN "entries" ON "entries"."person_id" = "people"."id" 12 # WHERE ("people"."name" = 'alice' AND "entries"."name" LIKE '%hello%') ` ` ` ` ` ` ` ` ` `
  8. [PR #39445] Where with block Rails で提案されている実装 ブロックの引数に対してカラムを参照して Arel の処理を呼び出す

    動的に元のモデルのカラムのメソッドを定義して処理をフックしている また #method_missing を利用して関連先のテーブルに対してのクエリも記述できる 1 # ブロックの引数に対してクエリを記述する 2 Post.where { |post| post[:updated_at].gt(1.day.ago) } 3 4 # comments は method_missing 経由で呼び出している 5 Post.joins(comments: :user).where { |post| post.comments.user[:first_name].eq("John") } 6 7 # こっちはブロックの引数なしでクエリを記述する 8 Post.where { updated_at.gt(1.day.ago) } 9 10 # 関連先のクエリを記述する 11 Post.joins(comments: :user).where { comments.user.first_name.eq("John") } ` `
  9. AST とは AST とは Abstract Syntax Tree の略 日本語だと抽象構文木と呼ばれるもの Ruby

    のコードを抽象化し、扱いやすくしたデータ構造 AST を利用することで Ruby のコードをメタデータ的に扱うことができる 今回は RubyVM::AbstractSyntaxTree を利用する ` `
  10. AST とは AST とは Abstract Syntax Tree の略 日本語だと抽象構文木と呼ばれるもの Ruby

    のコードを抽象化し、扱いやすくしたデータ構造 AST を利用することで Ruby のコードをメタデータ的に扱うことができる 今回は RubyVM::AbstractSyntaxTree を利用する AST は『種類』と『子ノード』の2つの情報を持っておりそれが再帰的なデータ構造にな っている ` `
  11. AST とは AST とは Abstract Syntax Tree の略 日本語だと抽象構文木と呼ばれるもの Ruby

    のコードを抽象化し、扱いやすくしたデータ構造 AST を利用することで Ruby のコードをメタデータ的に扱うことができる 今回は RubyVM::AbstractSyntaxTree を利用する AST は『種類』と『子ノード』の2つの情報を持っておりそれが再帰的なデータ構造にな っている AST の種類は構文ごとに細かく分かれていて100種類以上ある ` `
  12. コード 1 src = ":name == 'homu'" 2 3 ast

    = RubyVM::AbstractSyntaxTree.parse(src) 4 5 # Ruby 上の AST のデータ構造 6 pp ast 7 # => (SCOPE@1:0-1:15 8 # tbl: [] 9 # args: nil 10 # body: 11 # (OPCALL@1:0-1:15 (LIT@1:0-1:5 :name) :== 12 # (LIST@1:9-1:15 (STR@1:9-1:15 "homu") nil)) 13 14 # ast の種類 15 pp ast.type # => :SCOPE 16 17 # 自身の子ノード 18 pp ast.children 19 # => [[], 20 # nil, 21 # (OPCALL@1:0-1:15 (LIT@1:0-1:5 :name) :== 22 # (LIST@1:9-1:15 (STR@1:9-1:15 "homu") nil))] 抽象構文木 tbl args body SCOPE [] nil OPCALL LIT :name :== LIST STR 'homu' nil
  13. AST の対応表(一部) type コード AST 意味 LIT 1 [:LIT, [1]]

    数値やシンボルリテ ラルなど STR "string" [:STR, ["string"]] 文字列リテラル VCALL func [:VCALL, [:func]] メソッド呼び出し CALL func.bar [:CALL, [[:VCALL, [:func]], :bar, nil]] . 呼び出し QCALL func&.bar [:QCALL, [[:VCALL, [:func]], :bar, nil]] &. 呼び出し OPCALL 1 + a [:OPCALL, [[:LIT, [1]], :+, [:LIST, [[:VCALL, [:a]], nil]]]] 演算子呼び出し AND a && b [:AND, [[:LIT, [1]], [:VCALL, [:b]]]] && 演算子 NOTE: 実データは RubyVM::AST::Node になるがわかりやすく配列で表記している ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` `
  14. activerecord-where_with_block where のブロック内のコードが SQL として展開される 実装は AST……ではなくて kenma というライブラリのマクロ機能を使ってる 間接的に

    AST を使っているのでセーフ 1 # シンボルリテラルをカラムとして参照するようになる 2 puts User.where { :name == "homu" }.to_sql 3 # => SELECT "users".* FROM "users" WHERE "users"."name" = 'homu' 4 # シンボルと値を逆にしても動作する 5 puts User.where { "homu" == :name }.to_sql 6 # => SELECT "users".* FROM "users" WHERE "users"."name" = 'homu' 7 8 # 変数やメソッドも参照できる 9 def age; 20 end 10 name = "homu" 11 puts User.where { :name == name || :age < age }.to_sql 12 # => SELECT "users".* FROM "users" WHERE ("users"."name" = 'homu' OR "users"."age" < 20) 13 14 # ブロック内に式を書くとその結果が SQL に反映される 15 puts User.where { :name == "homu#{"homu"}" && :age < (1 + 2) }.to_sql 16 # => SELECT "users".* FROM "users" WHERE "users"."name" = 'homuhomu' AND "users"."age" < 3 ` `
  15. activerecord-where_with_block && で AND したりインスタンス変数が参照できるのがおしゃれポイント 1 # ブロック内でインスタンス変数 2 @name

    = "mami" 3 puts User.where { :name == @name }.to_sql 4 # => SELECT "users".* FROM "users" WHERE "users"."name" = 'mami' 5 6 # && を書くと SQL 文の AND として展開する 7 puts User.where { :name == "homu" && :age < 20 }.to_sql 8 # => SELECT "users".* FROM "users" WHERE "users"."name" = 'homu' AND "users"."age" < 20 9 10 # アソシエーションを参照する 11 puts User.joins(:comments).where { :comments.text == "OK" && :name == "homu" }.to_sql 12 # => SELECT "users".* 13 # FROM "users" 14 # INNER 15 # JOIN "comments" 16 # ON "comments"."user_id" = "users"."id" 17 # WHERE "comments"."text" = 'OK' 18 # AND "users"."name" = 'homu' ` ` ` `