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

4.7K Views

December 23, 21

スライド概要

https://ginza-rails.connpass.com/event/226024/

シェア

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

AST を使って ActiveRecord の where の条件式を ブロックで記述しよう 【オンライン開催】銀座Rails#38

2.

今日話すこと!

3.

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

4.
[beta]
今日話すこと!
`User.where("? <= age", 20)`

を

`User.where { 20 <= :age }`

とかけるようにしたい!!!

5.

自己紹介 名前:osyo Twitter : @pink_bangbi github : osyo-manga ブログ : Secret Garden(Instrumental)

6.

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

7.

自己紹介 名前: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 に備えよう!

8.

自己紹介 名前: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 を駆け足で振り返る

9.

自己紹介 名前: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 のショートハンド

10.

なんで where にブロックを渡したいの?

11.

なんで where にブロックを渡したいの? できたら面白そうじゃん

12.

アジェンダ

13.

アジェンダ 既存の実装を紹介

14.

アジェンダ 既存の実装を紹介 AST を利用して Ruby のコードを SQL 文に変換してみる

15.

アジェンダ 既存の実装を紹介 AST を利用して Ruby のコードを SQL 文に変換してみる activerecord-where_with_block をつくったので紹介

16.

伝えたいこと

17.

伝えたいこと AST というデータ構造に付いて少しでも学んでもらいたい

18.

伝えたいこと AST というデータ構造に付いて少しでも学んでもらいたい AST を利用する事でもっと Ruby のコードをメタ的に扱える

19.

伝えたいこと AST というデータ構造に付いて少しでも学んでもらいたい AST を利用する事でもっと Ruby のコードをメタ的に扱える AST を使ったらこんな事が実現できるんだよ!を体験してもらいたい

20.

既存の実装

21.
[beta]
activerecord-refinements

Refinements を利用してブロック内でのみ `Symbol#==` などを再定義している実装
元々は Ruby 2.0 の Refinements 実装時に実験的に作られた gem

ブロック内の :name == 'matz' は table[:name].eq 'matz' を返す
`Symbol#==`

だと `table[col].eq val` を呼び出すような実装

現在はブロック内で `using` を適用する事ができなくなっており動かない
ブロック内でのみ再定義した Symbol#== が有効になる

1

#

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%')

22.
[beta]
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"

3

WHERE "people"."id" = 1

で && を模倣している

4

# &

5

Person.where { id.eq(1) & name.matches('%alice%') }.to_sql

6

# => SELECT "people".* FROM "people"

7

WHERE ("people"."id" = 1 AND "people"."name" LIKE '%alice%')

関連先を join する

8

#

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%')

23.

[PR #39445] Where with block Rails で提案されている実装 ブロックの引数に対してカラムを参照して Arel の処理を呼び出す 動的に元のモデルのカラムのメソッドを定義して処理をフックしている また `#method_missing` を利用して関連先のテーブルに対してのクエリも記述できる ブロックの引数に対してクエリを記述する 1 # 2 Post.where { |post| post[:updated_at].gt(1.day.ago) } 3 は method_missing 経由で呼び出している 4 # comments 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") }

24.

AST とは

25.

AST とは

26.

AST とは AST とは Abstract Syntax Tree の略 日本語だと抽象構文木と呼ばれるもの

27.

AST とは AST とは Abstract Syntax Tree の略 日本語だと抽象構文木と呼ばれるもの Ruby のコードを抽象化し、扱いやすくしたデータ構造

28.

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

29.

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

30.

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

31.
[beta]
コード
1

抽象構文木

src = ":name == 'homu'"

SCOPE

2
3

ast = RubyVM::AbstractSyntaxTree.parse(src)

4

上の AST のデータ構造

5

# Ruby

6

pp ast

7

# => (SCOPE@1:0-1:15

8

#

tbl: []

9

#

args: nil

10

#

body:

11

#

12

#

args

body

[]

nil

OPCALL

(OPCALL@1:0-1:15 (LIT@1:0-1:5 :name) :==
(LIST@1:9-1:15 (STR@1:9-1:15 "homu") nil))

13

LIT

:==

LIST

の種類

14

# ast

15

pp ast.type # => :SCOPE

16

tbl

自身の子ノード

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))]

:name

STR

'homu'

nil

32.

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` になるがわかりやすく配列で表記している 意味 数値やシンボルリテ ラルなど

33.

AST を利用して Ruby のコードから SQL 文に変換してみよう!

34.

AST を利用して Ruby のコードから SQL 文に変換してみよう! 1 ↓ :name == "homu" && 20 <= :age

35.

AST を利用して Ruby のコードから SQL 文に変換してみよう! 1 :name == "homu" && 20 <= :age ↓ 1 name = "homu" AND 20 <= age

36.

実装をライブコーディング

37.

Gem 化したよ!!!

38.
[beta]
activerecord-where_with_block

のブロック内のコードが SQL として展開される
実装は AST……ではなくて kenma というライブラリのマクロ機能を使ってる
`where`

間接的に 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

ブロック内に式を書くとその結果が SQL に反映される

14

#

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

39.
[beta]
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

を書くと SQL 文の AND として展開する

6

# &&

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

#

14

#

15

#

16

#

17

#

18

#

FROM "users"
INNER
JOIN "comments"
ON "comments"."user_id" = "users"."id"
WHERE "comments"."text"
AND "users"."name"

= 'OK'
= 'homu'

40.

まとめ

41.

まとめ

42.

まとめ AST を使うことでメタプロ以上の事ができる 普段フックできないような処理をフックできる

43.

まとめ AST を使うことでメタプロ以上の事ができる 普段フックできないような処理をフックできる 今回やった事は実質トランスコンパイルでは??? SQL 文じゃなくてもっと別の言語に変換できると面白そう いろいろと応用ができそう!!

44.

まとめ AST を使うことでメタプロ以上の事ができる 普段フックできないような処理をフックできる 今回やった事は実質トランスコンパイルでは??? SQL 文じゃなくてもっと別の言語に変換できると面白そう いろいろと応用ができそう!! ブロック内のコードを AST に変換できるの強い

45.

まとめ AST を使うことでメタプロ以上の事ができる 普段フックできないような処理をフックできる 今回やった事は実質トランスコンパイルでは??? SQL 文じゃなくてもっと別の言語に変換できると面白そう いろいろと応用ができそう!! ブロック内のコードを AST に変換できるの強い AST で遊ぶのたのしいよね!!!