Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

SQLQL は GraphQL にとってなんなのか

6,590 views

Published on

RailsDM 2019 Day 1

Published in: Technology
  • Be the first to comment

SQLQL は GraphQL にとってなんなのか

  1. 1. SQLQL は GraphQL に とってなんなのか SQLQL は GraphQL に とってなんなのか 2019/03/22 RailsDM @yancya Powered by Rabbit 2.2.2 and COZMIXNG
  2. 2. @yancya の自己紹介 Rubyist, SQList
  3. 3. 全体の構成 基礎、考え方 諸問題の解決 SQLQL プログラミング 事例
  4. 4. はじまり “SQLQL” でググると出てきます
  5. 5. SQLQL とは https://somehost/sqlql みたいな URL に SQL を送信すると、SQL 実行結果の JSON が返って くるという概念
  6. 6. 気をつけたいこと サーバー側の RDBMS の SQL 処理系は便利な のでそのまま使いたい 特に PostgreSQL が便利なので使いたい でも SQLQL で送られてきた SQL で物理的な テーブルを直接触らせたくはない 外に見せたくないテーブルやカラムがある 誰に見せていいというわけではないレコードがあった りもする
  7. 7. サンプルアプリケーション 説明のためのアプリケーションがあります
  8. 8. サンプルアプリケーション サンプルアプリケーションは GitHub のページか らボタン1つでデプロイ出来ます 試してみてね
  9. 9. サンプルアプリケーションの仕様 ユーザーがコメントを投稿して、他のユーザー が「いいね」する、よくあるやつ
  10. 10. サンプルアプリのテーブル
  11. 11. サンプルアプリのテーブル
  12. 12. サンプルアプリのテーブル
  13. 13. 細かい仕様 鍵付きユーザーという概念がある 自身はユーザー自身にしか見えない 「いいね」は他のユーザーからは誰が「いいね」した のか分からない 鍵付き投稿という概念がある 投稿したユーザー自身にしか見えない
  14. 14. 少し複雑なクエリ 鍵付きユーザーの「いいね」について名前を 「匿名」にして出している WITH comments AS ( SELECT comments.id, content, users.name AS user_name, comments.created_at FROM comments JOIN users ON comments.user_id = users.id ), likes AS ( SELECT COALESCE(users.name, '匿名') AS user_name, likes.created_at, comment_id FROM likes LEFT OUTER JOIN users ON likes.user_id = users.id ) SELECT comments.id, content, comments.user_name , JSON_AGG(likes ORDER BY likes.created_at DESC) AS liked_by FROM comments JOIN likes ON likes.comment_id = comments.id GROUP BY 1, 2, 3
  15. 15. CTE(Common Table Expressions) インラインの VIEW サブクエリに名前を付けて、クエリ中で何度 も使える 既存のデフォルトスキーマ(public)のテーブ ル名を上書きする事が出来る さっきのテーブルは本当は public.users とか public.comments だけど public がデフォルトスキーマ なので users とか comments でアクセスできる
  16. 16. CTE(Common Table Expressions) users をシャドウイングしてサブセットのテー ブルに置き換えている様子 クエリーを発行しているユーザー自身と鍵付きではな いユーザーのレコードのみ id, name にしかアクセス出来ない WITH users AS ( SELECT id, name FROM users WHERE id = ? OR privacy = false ) SELECT * FROM users
  17. 17. CTE(Common Table Expressions) comments をシャドウイングしてサブセットの テーブルに置き換えている様子 自身のと鍵付きではないコメントのレコードのみ id, name, content, updated_at にしかアクセス出来ない WITH comments AS ( SELECT id, name, content, updated_at FROM comments WHERE user_id = ? OR privacy = false ) SELECT * FROM comments
  18. 18. CTE(Common Table Expressions) CTE によって予め射影される
  19. 19. CTE(Common Table Expressions) CTE によって予め射影される
  20. 20. CTE(Common Table Expressions) CTE によって予め射影される
  21. 21. 問題点 Mutations が邪魔 スキーマ修飾、システムカタログテーブルへ のアクセス 無限ループ
  22. 22. Mutations INSERT, UPDATE, DELETE, CREATE, TRUNCATE etc… DB の状態を変えてしまう操作は一切してほし くないので、なんとかして殺していく必要が ある
  23. 23. スキーマ修飾 WITH users AS ( SELECT * FROM users WHERE false ) SELECT * FROM users これなら結果が0件になるが
  24. 24. スキーマ修飾 WITH users AS ( SELECT * FROM users WHERE false ) SELECT * FROM public.users 元のテーブルの全カラムが全件見えてしまう スキーマ名で修飾すると、大元のテーブルに アクセス出来てしまうので、スキーマ修飾も 殺さないといけない
  25. 25. システムカタログテーブル pg で始まるテーブルは見られたくない pg_user 以外にもめっちゃ沢山ある SELECT * FROM pg_user
  26. 26. 無限ループ 再帰クエリを使うと無限ループを表現できて しまい、殺意のあるリクエストを送り込まれ る危険がある とは言え、再帰クエリが使える方が嬉しいの で、無限ループだけを殺したい
  27. 27. 無限ループ 再帰クエリを使った無限ループの例 n が 1 〜∞まであるテーブルが生成されようとして死ぬ WITH RECURSIVE r AS ( SELECT 1 AS n UNION ALL SELECT n + 1 AS n FROM r ) SELECT * FROM r
  28. 28. 問題の解決 Mutations の除外 スキーマ修飾、システムカタログテーブルへ のアクセス制限 無限ループ対策
  29. 29. Mutations の除外 複数 DB。 なるほど、これだ。Rails 6.0 すごい
  30. 30. 複数 DB Rails 6.0 から、replica 属性のサブのコネク ションの設定が簡単に書けるようになった development: primary: <<: *default database: sqlql_development readonly: <<: *default database: sqlql_development replica: true
  31. 31. replica 属性のコネクション SQLQL の処理をするときだけ replica 属性の コネクションを使えば、Mutations っぽい SQL は Rails が弾いてくれて便利っぽい ActiveRecord::Base.connected_to(database: :readonly) do User.first.update(name: 'hoge') end #=> ActiveRecord::ReadOnlyError #=> (Write query attempted while in readonly mode...
  32. 32. と思うじゃん? CTE の WITH 句は Rails 的にはホワイトリス トに入ってないっぽい…… ActiveRecord::Base.connected_to(database: :readonly) do ActiveRecord::Base.connection.execute( "WITH t AS (SELECT 1 AS n) SELECT * FROM t" ) end #=> ActiveRecord::ReadOnlyError #=> (Write query attempted while in readonly mode...
  33. 33. どういうルールなのか rails/activerecord/lib/active_record/ connection_adapters/postgresql/ database_statements.rb 便利メソッドで正規表現を生成してるの READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp( :begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback) # :nodoc: private_constant :READ_QUERY def write_query?(sql) # :nodoc: !READ_QUERY.match?(sql) end
  34. 34. モンキーパッチすっか? じゃあ WITH もホワイトリストに入れたろ def write_query?(sql) read_query = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp( :begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback, :with ) !read_query.match?(sql) end
  35. 35. やったー readonly コネクションで WITH が使えるよう になったぞ res = ActiveRecord::Base.connected_to(database: :readonly) do ActiveRecord::Base.connection.execute( "WITH t AS (SELECT 1 AS n) SELECT * FROM t" ) end res.to_a #=> [{"n"=>1}]
  36. 36. 本当にそれでいいのか? そもそも、なんで WITH がホワイトリストに 入っていなかったか考えないと駄目でしょ WITH は副作用を起こせてしまうのでは?
  37. 37. WITH は副作用を起こせる この機能、めちゃくちゃ便利なんですけど、 今は邪魔ですね res = ActiveRecord::Base.connected_to(database: :readonly) do ActiveRecord::Base.connection.execute( "WITH t AS (UPDATE users SET name = 'hoge' RETURNING *) SELECT * FROM t" ) end res.to_a #=> [{"id"=>1,"name"=>"hoge"},{"id"=>2,"name"=>"hoge"}]
  38. 38. DB ユーザーの権限 しゃーない ちゃんと、DB レベルで readonly なユーザー を作るしかない create user readonlyuser with password 'readonlyuser' NOCREATEDB NOCREATEROLE; GRANT SELECT ON ALL TABLES IN SCHEMA public TO "readonlyuser";
  39. 39. DB ユーザーの権限 development: primary: <<: *default database: sqlql_development readonly: <<: *default database: sqlql_development replica: true username: readonlyuser password: readonlyuser
  40. 40. DB は最後の砦 これで、副作用のあるクエリは DB が弾いて くれるようになった ActiveRecord::Base.connected_to(database: :readonly) do ActiveRecord::Base.connection.execute( "WITH t AS (UPDATE users SET book_name = 'hoge' RETURNING *) SELECT * FROM t" ) end #=> ActiveRecord::StatementInvalid #=> (PG::InsufficientPrivilege: ERROR: permission denied for table users
  41. 41. テーブルホワイトリスト SQL リクエストに含まれるはずのテーブルは 予め決まっている ホワイトリスト方式にすれば、物理テーブル やシステムテーブルなどへのアクセスを防げ る
  42. 42. PgQuery gem pg_query という gem がある $ gem install pg_query PgQuery.parse("select * from t").tables #=> ["t"]
  43. 43. PgQuery gem テーブル名をスキーマ名で修飾している SQL を検知できる pg_user みたいなシステムカタログテーブル へのアクセスも検知できる PgQuery.parse("select * from public.t").tables #=> ["public.t"]
  44. 44. 無限ループ対策 SQLQL の処理をする場合の実行時間に制限を 付ければ、無限には実行されないだろう development: primary: <<: *default database: sqlql_development readonly: <<: *default database: sqlql_development replica: true variables: statement_timeout: 3000
  45. 45. タイムアウトによる無限ループ対策 まぁまぁ良さそう ActiveRecord::Base.connected_to(database: :readonly) do ActiveRecord::Base.connection.execute(<<~SQL) WITH RECURSIVE r AS ( SELECT 1 AS n UNION ALL SELECT n + 1 AS n FROM r ) SELECT * FROM r SQL end #=> ActiveRecord::QueryCanceled #=> (PG::QueryCanceled: ERROR: canceling statement due to statement timeout)
  46. 46. 無限ループ対策 SQL の実行時間を短く制限(3秒とかに)す れば、CTE を使った無限ループを防げる generate_series(1, 10000000) みたいな、膨 大な行数を生成する関数の実行も途中で打ち 切れる とは言え、DOS 攻撃に弱そう…… 今後の課題として残る
  47. 47. SQLQL の核となる考え方 インピーダンスミスマッチ Object から Relation へ SQLQL プログラミング
  48. 48. インピーダンスミスマッチ
  49. 49. インピーダンスミスマッチ 誤解を恐れず言えば、オブジェクトとリレー ション(群)との相互変換が大変という話に 思える これを解決するために OR/M を使って頑張ってるのが 現代
  50. 50. Relation to Object SQL でリレーションをオブジェクトに
  51. 51. Relation to Object JSON_AGG 関数や ROW_TO_JSON 関数を 使えば、SQL でババっと JSON を生成できる WITH users(id, "name") AS (VALUES (1, 'taro'), (2, 'jiro')) SELECT JSON_AGG(users) AS users FROM users; -- users -- --------------------------- -- [{"id":1,"name":"taro"},{"id":2,"name":"jiro"}] -- (1 row)
  52. 52. Relation to Object 欲しい JSON の形でスっと出せる WITH users(id, "name") AS (VALUES (1, 'taro')) , comments(id, content, user_id) AS ( VALUES(1, 'aaaaaaa', 1), (2, 'bbbbbb', 1)) , t AS ( SELECT users.id, users.name, JSON_AGG(comments) as comments FROM users JOIN comments ON users.id = comments.user_id GROUP BY 1, 2) SELECT JSON_AGG(t) AS users FROM t; -- users --------------------------------------------------------------------------------- -- [{"id":1,"name":"taro","comments":[{"id":1,"content":"aaaaaaa","user_id":1}, + -- {"id":2,"content":"bbbbbb","user_id":1}]}] -- (1 row)
  53. 53. Object to Relation 逆にオブジェクトをリレーションに変換は?
  54. 54. Object to Relation JSON_TO_RECORD, JSON_TO_RECORDSET 関数を使う with users as ( SELECT id, "name", comments FROM JSON_TO_RECORDSET( CONCAT('[{"id":1,"name":"taro","comments":', '[{"id":1,"content":"aaaaaaa","user_id":1}', ',{"id":2,"content":"bbbbbb","user_id":1}]}]')::JSON ) AS t(id integer, "name" text, comments json)) select users.id, users."name", comments.id as comment_id, comments.content from users, json_to_recordset(users.comments) AS comments(id integer, content text, user_id integer) -- id | name | comment_id | content ------+------+------------+--------- -- 1 | taro | 1 | aaaaaaa -- 1 | taro | 2 | bbbbbb --(2 rows)
  55. 55. SQLQL プログラミング SQL 処理系の中でプログラミングをすると、 リレーショナル代数計算を手軽に行う事が出 来る 和(UNION)、積(INTERSECT) 差(EXCEPT)、商(一言では言い表せない) 直積(CROSS JOIN)、選択(WHERE) 射影(SELECT)、結合(JOIN)
  56. 56. SQLQL プログラミング オブジェクトとリレーション、相互に変換出 来るのであれば オブジェクト界が得意な処理はオブジェクトに変換し て行う リレーション界が得意な処理はリレーションに変換し て行う RDB に保存したいときも然り
  57. 57. SQLQL プログラミング オブジェクト界が得意な処理と、リレーショ ン界が得意な処理とは…… SQL脳から見たRubyという発表 2015/11/08 by yancya JOIN とか WINDOW 関数の処理をアプリケーション側 で書くのは結構大変 RDB からデータを取ってくるついでに、不得意な処理 も任せてしまいたい
  58. 58. GraphQL の話はどうした GraphQL App 内で SQLQL を呼び出して実行 認可の層として使えるのではないか
  59. 59. まとめ SQLQL のサンドボックスは多少マシになった フロントエンドが持ってるオブジェクトをリ レーションに変換して API の向こうにある DB と JOIN して、欲しいデータとして受け取 れたら楽しい Powered by Rabbit 2.2.2 and COZMIXNG

×