Advertisement
Advertisement

More Related Content

Slideshows for you(20)

Similar to O/Rマッパーによるトラブルを未然に防ぐ(20)

Advertisement

More from kwatch(20)

Recently uploaded(20)

Advertisement

O/Rマッパーによるトラブルを未然に防ぐ

  1. O/R Mapper による トラブルを未然に防ぐ Makoto Kuwata <kwa@kuwata-lab.com> http://www.kuwata-lab.com/ PostgreSQLカンファレンス 2014 ver 1.1.0
  2. copyright © 2014 kuwata-lab.com all rights reserved まえがき 現在、アプリケーション開発の現場では O/R Mapper (ORM) が普及しています。今後 も ORM を使った開発は、増えることはあっても減ることはないでしょう。 しかし ORM は、アプリケーション開発者にとっては便利でも、DB 管理者 (DBA) か らみたらトラブルの種でもあります。それが特にパフォーマンスに関する問題であるこ とが多いため、開発者と DBA が対立することも珍しくありません。 とはいえ、ORM による問題はすでに解決策が用意されている場合があります。本当の 問題は、すでに存在する解決策があまり知られていないことではないでしょうか。 そこで本発表では、ORM によってどのような問題が起こりやすいか、どう解決・予防 すればいいか、そして ORM とどう「折り合い」をつけるかを説明します。特に、よく トラブルとなる「N+1 問題」については説明を多めにしています。 また本発表を通じて、開発者と DBA が「互いが互いを知らないままに批判しあう」と いう状況を改善し、両者が協調できる「まだ見ぬ丘の向こう」を目指します。
  3. copyright © 2014 kuwata-lab.com all rights reserved まとめ ✓ ORMによるトラブルは (たいてい) 解決策がある ✓ 解決策を知れば嫌悪感は (許容半以内に) 抑えられる ✓ 我々のゴールは「プロジェクトやサービスの
 成功」であることを思い出す (対立することではない)
  4. 発表の背景
  5. copyright © 2014 kuwata-lab.com all rights reserved 背景:ORMによるトラブルが多発 ✓ ORMが生成するSQLがクソ いわゆる「ぐるぐる系SQL」 ✓ SQLをろくに勉強しないアプリ開発者 そのくせOOPやデザインパターンを得意げに語る ;( ✓ 開発効率を上げるためのORMなのに話が違う! ある面では効率が上がっても、別の問題を引き起こしている
  6. copyright © 2014 kuwata-lab.com all rights reserved どこかでみた風景 実はオブジェクト指向って しっくりこないんです… SQLの書けないやつなぞ エンジニアとして三流! 今はORMもKVSもある! SQLよりOOPのほうが重要! Staticおじさんww 老害wwwww OOP信者 自分の得意分野に入り浸って、それ以外の分野に飛び込めない人たちっているよね…
  7. copyright © 2014 kuwata-lab.com all rights reserved 背景:DBAがORMを知らなすぎる ✓ ORMのことをろくに知らずに批判している人 が多すぎ ✓ 「SQLをろくに知らない開発者」
 vs 「ORMをろくに知らないDBA」 ✓ ORMを知らない→トラブルが解決できない→ 「ORMなんかクソ!」
  8. copyright © 2014 kuwata-lab.com all rights reserved わかってない人によるORM批判その1 (ORMは) テーブルから全行をスキャンし、 クライアントアプリケーションへ ネットワーク越しに転送する “ ”『nabokov7; rehash : O/Rマッパーはなぜ悪か』 http://nabokov.blog.jp/archives/1529263.html ちゃんとDB側で絞りこんでから 転送してますよぉ!
  9. copyright © 2014 kuwata-lab.com all rights reserved わかってない人によるORM批判その2 たとえば、SQL を書くことでフィルタリ ングを DB で実行していたのに、O/R を 使うことでフィルタリングは、そのプロ グラミング言語がやることになります。 “ ” where句もhaving句も DB側でやってますよぉ! https://twitter.com/kazu_yamamoto/status/280872190805680129
  10. copyright © 2014 kuwata-lab.com all rights reserved なぜORMを嫌うのか? ✓ 理由:
 ORMがトラブルを引き起こすから? ✓ 本当の理由:
 ORMによるトラブルが解決できないから! トラブルの解決方法を知れば そこまで嫌うことはないはず
  11. copyright © 2014 kuwata-lab.com all rights reserved 理想と現実の間には、妥協点が存在する ・アプリ開発者がSQLを勉強してくれる ・ORMの吐くSQLにDBAが反吐を吐く ・ORMによるトラブルの芽を早期に潰す 【理想】 【現実】
  12. copyright © 2014 kuwata-lab.com all rights reserved 本発表の目的は、DBAの皆さんに… ✓ ORMでよくあるトラブルとその予防策を知っ てもらう 嫌悪感の本当の原因は、ORMがトラブルを起こすからではなく、 トラブルが解決できない (=解決策を知らない) から ✓ そのためにORMの仕組みを知ってもらう 解決方法を知らないのは、DBAがORMを知らなすぎるから ✓ 「O/R Mapperなんか使うな!」とは違う問 題解決方法があることを知ってもらう 「問題があるから禁止」は問題の解決になっていない
  13. O/R Mapperの基礎知識
  14. copyright © 2014 kuwata-lab.com all rights reserved ORMの役目は、大きく3つ • • • SQL RecordSet SQLを組み立てて発行 スキーマを作成・変更 トラブルはここで発生 (今日はここのお話) レコードセットを オブジェクトに変換 Database Application
  15. copyright © 2014 kuwata-lab.com all rights reserved Prepared Statementじゃだめなの? ✓ 要件:実行時に検索条件を変えたい ✓ Prepared Statementではwhere句への追加 などが行えない 性別: 男性 女性 年齢: ∼ 歳歳 無指定 検索条件
  16. copyright © 2014 kuwata-lab.com all rights reserved SQLを動的に組み立てる:文字列結合 var cond = [], args = []; if ( params.gender == "M" || params.gender == "F") { cond.push("gender = ?"); args.push(params.gender); } if (params.max_age) { cond.push("age <= ?"); args.push(params.max_age); } var sql = "select * from users"; if (cond.length) { sql += " where " + cond.join(" and "); } sql += " order by name"; とても面倒なうえに、SQL Injectionを誘発しやすい
  17. copyright © 2014 kuwata-lab.com all rights reserved SQLを動的に組み立てる:ORM SQLテンプレート方式 専用のテンプレートエンジンを 使って、SQLを生成する select * from students where /** if (gender) { **/ gender = :gender /** } **/ order by name クエリオブジェクト方式 SQLを表現するデータを作成し、 それをSQL文字列に変換する { table: "students", where: [["gender=","F"]], orderby: ["name"] } select * from students where gender = 'F' order by name
  18. copyright © 2014 kuwata-lab.com all rights reserved SQLテンプレート方式 select * from students /** if (gender || max_age) { **/ where true /** if (gender) { **/ and gender = :gender /** } **/ /** if (max_age) { **/ and age <= :max_age /** } **/ /** } **/ order by name 文字列結合よりは読みやすい、SQL Injectionも誘発しない この「true」はSQLのoptimizerが 除去してくれる (PostgreSQL)
  19. copyright © 2014 kuwata-lab.com all rights reserved SQLテンプレート方式 select * from students /** if (gender || max_age) { **/ where true /** if (gender) { **/ and gender = :gender /** } **/ /** if (max_age) { **/ and age <= :max_age /** } **/ /** } **/ order by name 不要な "and" や "while" をORMが 自動的に取り除いてくれる SQLを解釈できるORMなら、より簡潔に書けることも
  20. copyright © 2014 kuwata-lab.com all rights reserved SQLテンプレート方式 select st.* from students st where /** if (gender) { **/ st.gender = :gender /** } **/ --------------------- select st.*, cl.* from students st join classes cl on st.class_id = cl.id where /** if (gender) { **/ st.gender = :gender /** } **/ それぞれで条件式が重複している (DRYではない) 似たようなSQLを複数書く必要があり、DRYではない 所属クラスが いらない場合のSQL 所属クラスが 必要な場合のSQL
  21. copyright © 2014 kuwata-lab.com all rights reserved クエリオブジェクト方式 var query = {table: "students", where: [], orderBy: []}; // if (params.gender) query.where.push(["gender=", params.gender]); if (params.max_age) query.where.push(["age<=", params.max_age]); query.orderBy.push("name"); // var sql = generateSQL(query); オブジェクトに各種条件を追加し、最後にSQLへ変換 where や order by を
 データとして操作できる
  22. copyright © 2014 kuwata-lab.com all rights reserved クエリオブジェクト方式 var query = new Query(Student); // if (params.gender) query.where("gender", params.gender); if (params.max_age) query.where("age<=", params.max_age); query.orderBy("name"); // var sql = query.generateSQL(); 通常は専用のクエリクラスを使うので、簡潔に書ける
  23. copyright © 2014 kuwata-lab.com all rights reserved クエリオブジェクト方式 var query = new Query(Student); // if (params.gender) query.where(Student.gender == params.gender); if (params.max_age) query.where(Student.age <= params.age); query.orderBy(Student.name); // var sql = query.generateSQL(); よくできた言語とよくできたORMなら「式」を指定可能 演算子オーバーライドや AST変換を活用
  24. copyright © 2014 kuwata-lab.com all rights reserved クエリオブジェクト方式 == x nil x is nullx == nil 評価 変換 構文解析ではないことに注意! ("==" の評価結果がtrue/falseではなく構文木) (ソースコード) (構文木) (SQL) 「x = null」ではなく 「x is null」になってくれる! 演算子オーバライドを利用した、構文木生成の仕組み
  25. copyright © 2014 kuwata-lab.com all rights reserved 余談:LINQの実態はクエリオブジェクト // SQLのようだが実はC# from st in Student where st.gender == "F" order by st.name select st; LINQ:変換前 // だいたいこんな感じ From(Student) .Where(x => x.gender == "F") .OrderBy(Student.name) .ToArray(); LINQ:変換後
  26. copyright © 2014 kuwata-lab.com all rights reserved 特徴:SQLテンプレート方式 ✓ 仕組みがわかりやすい ORMの学習コストが低い、トラブルに対処しやすい ✓ SQLが予想しやすい つまりチューニングしやすい ✓ SQLの欠点はそのまま DRYにする仕組みがない、'=' と 'is' の使い分けが必要、など ✓ SQL Injectionはほぼ発生しない 文字列結合をするコードを書かなくて済むため
  27. copyright © 2014 kuwata-lab.com all rights reserved 特徴:クエリオブジェクト方式 ✓ 仕組みが複雑 ORMの学習コストが高い、トラブル対応がしにくい ✓ どんなSQLになるかを確認する必要がある 予想外のSQLが生成されることも ✓ SQLではできないことができる 詳細は『なぜORMが必要か?』でggr ✓ SQL Injectionはほぼ発生しない 構文木(or 類似した構造)を作ってからSQLに変換するため
  28. copyright © 2014 kuwata-lab.com all rights reserved ORMのアーキテクチャは「PoEAA」を読め Object-Relational Structural Patterns • Identity Field • Foreign Key Mapping • Association Table Mapping • Dependent Mapping • EmbeddedValue • Serialized LOB • Single Table Inheritance • Class Table Inheritance • Concrete Table Inheritance • Inheritance Mappers Data Source Architectural Patterns • Table Data Gateway • Row Data Gateway • Active Record • Data Mapper Object-Relational Behavioral Patterns • Unit of Work • Identity Map • Lazy Load Object-Relational Metadata Mapping Patterns • Metadata Mapping • Query Object • Repository (注)PoEAA …『Pattern of Enterprise Application Architecture』(Martin Fowler, 2002)
  29. ORMでよくあるトラブルと その対策
  30. copyright © 2014 kuwata-lab.com all rights reserved ORMでよくあるトラブル ✓ N+1 問題 深刻度:極 ✓ クエリ発行箇所が特定できない問題 深刻度:大 ✓ インデックスつけ忘れ問題 深刻度:中 ✓ select * 問題 深刻度:小
  31. copyright © 2014 kuwata-lab.com all rights reserved ORMでよくあるトラブル ✓ N+1 問題 深刻度:極 ✓ クエリ発行箇所が特定できない問題 深刻度:大 ✓ インデックスつけ忘れ問題 深刻度:中 ✓ select * 問題 深刻度:小
  32. copyright © 2014 kuwata-lab.com all rights reserved 「N+1 問題」 とは? 一覧を取得するSQLを発行してから、
 各要素ごとに個別のSQLを発行してしまうこと いわゆる「ぐるぐる系SQL」のこと。パフォーマンスが極端に落ちる users = User.all() for user in users print user.group.name end select * from groups where id = :id をN回発行 select * from users を1回発行 (注)実態をより正確に表すなら「1+N問題」と呼ぶべき (注)
  33. copyright © 2014 kuwata-lab.com all rights reserved ORM側の対策:eager loading 一覧を取得するときに、関連する要素もまとめて取 得するよう指定する 実装は、join だったり id in (…) だったり、まちまち users = User.includes("group").all() for user in users print user.group.name end select文は発行されない select * from users を1回発行してから、 select * from groups where id in (…) を 1回発行する (参考)『3 ways to do eager loading (preloading) in Rails 3 & 4 』 http://blog.arkency.com/2013/12/rails4-preloading/
  34. copyright © 2014 kuwata-lab.com all rights reserved ORM側の対策:strategic eager loading オブジェクトの関連が必要になったら、親となるコ ンテナに通知し、コンテナがまとめて取得する 子であるオブジェクトが個別に取得するのを止める users = User.includes("group").all() for user in users print user.group.name end groups テーブルへのアクセスが必要になると、 親となるコンテナである users へ通知され、 users がまとめて groups テーブルにアクセスする 明示的な指定が必要ない (参考)『Why DataMapper? (Section: Strategic Eager Loading) http://datamapper.org/why.html
  35. copyright © 2014 kuwata-lab.com all rights reserved ORM側の対策:bytecode manipulation bytecodeを解析してeager loadingが必要な箇所 を判定し、それを行うコードを自動的に埋め込む bytecode操作のかわりにAST変換やpreprocessorでもよい List<User> users = query(User).all(); for (User user: users) { System.out.print(user.group.name); } ループの中で関連を取得している ことを検出し、ループ前にまとめ て取得するようbytecodeを変更 (参考)『スケーラブルラピッドプロトタイピングのためのJIT-ORM 』 http://www.ipa.go.jp/files/000007122.pdf
  36. copyright © 2014 kuwata-lab.com all rights reserved insert文における「N+1 問題」 1件ずつinsert文を発行するのをやめて、
 bulk insert機能を使って一括作成する もちろん、大量すぎる場合は copy 文 // Bad:1件ずつinsert for s in names u = User.new(name: s) u.save() end // Good:まとめてinsert users = names.map {|s| User.new(name: s) } User.import(users)
  37. copyright © 2014 kuwata-lab.com all rights reserved update文における「N+1 問題」 1件ずつupdate文を発行するのをやめて、
 条件で指定した集合をまとめて更新する jQueryにおける $("..条件..").attr(key, value) と同じ // Bad:1件ずつの更新 users = db.query(User).all() for u in users: u.value = u.value+1 db.commit() // Good:まとめて更新 db.query(User) .update({ "value": User.value+1 }) db.commit() これは「値」(数値) これは「式」(構文木) (注) (注)まとめて更新するには、「値」ではなく「式」(構文木) を指定できる 必要があり、そのためには演算子オーバーライドなどが使えると便利。
  38. copyright © 2014 kuwata-lab.com all rights reserved DBAが取るべき予防策 ✓ N+1問題について開発側と話しあっておく 使用するORMでの解決方法を事前に確認しておく、
 また親-子だけでなく親-子-孫の場合も解決できるか要確認 ✓ 「N+1問題だけは許さんぞ!」と、開発側に
 しつこく言い続けてプレッシャーをかける N+1問題の名前は知ってても深刻さを分かってない開発者が多い 「N+1問題?何ですかそれ?」 と開発者が言おうものなら「喝!」
  39. copyright © 2014 kuwata-lab.com all rights reserved ORMでよくあるトラブル ✓ N+1 問題 深刻度:極 ✓ クエリ発行箇所が特定できない問題 深刻度:大 ✓ インデックスつけ忘れ問題 深刻度:中 ✓ select * 問題 深刻度:小
  40. copyright © 2014 kuwata-lab.com all rights reserved 「クエリ発行箇所が特定できない問題」とは? スロークエリが検出されても、それがプログラムの どこで発行されたかが分からず手が打てない問題 DBAにとっては、とてもフラストレーションのたまる事案 User.where(gender: "F") .where(deleted: nil) .order_by("name") .all() select * from users where gender = "F" and deleted is null order by name こっち方向は特定できるけど こっち方向が特定できない ;(
  41. copyright © 2014 kuwata-lab.com all rights reserved ORM側の対策:SQL ID、クエリID コメントを使って、SQLごとに一意なIDをつける 1つのSQLが複数個所から呼ばれることがあるので、クエリごとにIDをつ けるのが望ましい (注) -- [sql:fg9xk] select * from users /** if (gender) { **/ where gender = :gender /** } **/ User.where(gender: "F") .where(deleted: nil) .order_by("name") .comment("[q:yxe4m]") .all() 多くのORMで未サポート ;( (注) IDのかわりに、呼び出し元のファイル名と行番号でもよい
  42. copyright © 2014 kuwata-lab.com all rights reserved DBAが取るべき予防策 ✓ SQL IDが付けられるなら、付けてもらう ✓ 呼び出し元のファイル名と行番号がわかるなら、 SQLコメントやログに記録してもらう ✓ Slow QueryがどのAPIで発行されたかを特定 できるような仕組みを用意する 要は、SQLだけでは特定できないなら別の方向から絞り込む
  43. copyright © 2014 kuwata-lab.com all rights reserved ORMでよくあるトラブル ✓ N+1 問題 深刻度:極 ✓ クエリ発行箇所が特定できない問題 深刻度:大 ✓ インデックスつけ忘れ問題 深刻度:中 ✓ select * 問題 深刻度:小
  44. copyright © 2014 kuwata-lab.com all rights reserved 「インデックスつけ忘れ問題」とは? そのままの意味 ORMでなくても発生する問題だが、発行されるSQLが具体的でないと explainを実行できないため、ORMだとより発生しやすいといえる User.where(gender: "F") .where(deleted_at: nil) .order_by("created_at") .all() 実行しないとどんなSQLが分かりにくい → explain で調べにくい   → index つけ忘れに気付かない
  45. copyright © 2014 kuwata-lab.com all rights reserved ORM側の対策:スキーマ定義で指定 カラムごとにインデックス作成を指定する 複合インデックスも指定できることが多い class User(Base): id = Column(Integer, primary_key=True) name = Column(String(255), nullable=False, index=True) email = Column(String(255), nullable=True, index=True) カラムごとにインデックス作成を
 指定できるので忘れにくい (SQLはこれができないので忘れやすい) 残念ながら、本質的な解決策がない ;(
  46. copyright © 2014 kuwata-lab.com all rights reserved DBAが取るべき予防策 ✓ インデックスが必要そうなカラムをリストアッ プし、重点的にチェック ・where で使いそう … 名前, コード, メアド, 生年月日, etc ・order by で使いそう … 名前, 作成日時, 更新日時, etc ・join で使いそう … 外部キー, 多対多の中間テーブル, etc ✓ 大きいテストデータを用意してあげる 小さいデータで開発しているから気付かない、
 大きいデータを用意してあげれば遅いことに気付きやすい
  47. copyright © 2014 kuwata-lab.com all rights reserved ORMでよくあるトラブル ✓ N+1 問題 深刻度:極 ✓ クエリ発行箇所が特定できない問題 深刻度:大 ✓ インデックスつけ忘れ問題 深刻度:中 ✓ select * 問題 深刻度:小
  48. copyright © 2014 kuwata-lab.com all rights reserved 「select * 問題」とは? ORMが、使わないカラムもすべて select * で取得 してしまう問題。 特にBlobや長いtextでは重大な問題 articles = db.query(Article) for x in articles: print(x.id, x.title) ここでは記事のIDとタイトルしか 必要ないのに、記事の本文まで取 得してしまう →負荷増大 (注)たいていのORMではカラム名を指定できるはずだが、外部キー経由で 取得した関連オブジェクトのカラムまでは指定できない。
  49. copyright © 2014 kuwata-lab.com all rights reserved ORM側の対策:遅延フェッチ Blobや長いtextは、デフォルトではselect時に
 除外するよう、ORMのスキーマで指定する 明示的に指定した場合のみ取得する class Article(Base): id = Column(Integer, primary_key=True) title = Column(String, nullable=False, index=True) body = deferred(Column(Text, nullable=False) デフォルトでは select 文で 取得する対象にしない
  50. copyright © 2014 kuwata-lab.com all rights reserved DBAが取るべき予防策 ✓ Blobや長いtextは、別テーブルに分離する かわりに N+1 問題が発生する可能性があるので注意すること ✓ 必要なカラムだけを指定したビューを作る 外部キーで参照しているテーブルまでは変えられないので注意 ✓ カラム名の細かい指定はある程度あきらめる パフォーマンス劣化が深刻でなければ許容する
  51. ORMと折り合いをつける • • • • ための、DBAの心構え
  52. copyright © 2014 kuwata-lab.com all rights reserved 心構えその1:相手を知る ✓ ORMの仕組みを知る 知っていればトラブルに対処しやすい、開発者に提案しやすい
 例:ぐるぐる系SQLが現れた!
 × だからORMは止めよう!
 ◎ eager loadingを使うよう提案しよう! ✓ アプリ開発を知る DBの知識だけで物事を考えない、トラブル対応しようとしない
  53. copyright © 2014 kuwata-lab.com all rights reserved 心構えその2:先手を打つ ✓ 問題が起きるまえに対策を講じる 問題が起きてからどうするか?より、問題を起こさないためには どうするか? (「ORMを使わない」というのはなしで ;) ✓ 開発初期から開発チームに助言する(特にN+1問題) 開発終盤になってから文句を言っても手遅れ
  54. copyright © 2014 kuwata-lab.com all rights reserved 心構えその3:教えてあげる ✓ 開発者がSQLを知らないなら教えてあげる 一見面倒だが、それで重大なトラブルが減らせるなら安上がり ✓ 開発者からのSQLの相談に乗ってあげる 一見面倒だが、下手にORMだけでやられてトラブるより安上がり
  55. copyright © 2014 kuwata-lab.com all rights reserved 心構えその4:心を広く持つ ✓ 最高速度を求めない 必要な速度が出ればそれでよしとする ✓ 最高品質を求めない 必要なサービス品質が提供できればよしとする
  56. copyright © 2014 kuwata-lab.com all rights reserved 心構えその5:金を積む ✓ 「SQLのできる開発者」は金を出せば手に入る 札束で頬をひっぱたけばスキルのある開発者を囲えることは、
 GREEやDeNAやLINEが証明してくれました ✓ 金を惜しんでるなら文句を言うべきではない 月20万30万そこそこの人材に多くを求めすぎない (※) (※)たいていのアプリ開発者にとって、SQLは「多く」に含まれることに注意
  57. まとめ
  58. copyright © 2014 kuwata-lab.com all rights reserved まとめ ✓ ORMによるトラブルは (たいてい) 解決策がある ✓ 解決策を知れば嫌悪感は (許容半以内に) 抑えられる ✓ 我々のゴールは「プロジェクトやサービスの
 成功」であることを思い出す (対立することではない)
  59. copyright © 2014 kuwata-lab.com all rights reserved Q&A:個人的にお勧めなO/Rマッパーは? ✓ DataMapper (ruby) http://datamapper.org/ Strategic Eager Loadingのような秀逸なアイデアを生み出している ✓ Sequel (ruby) http://sequel.jeremyevans.net/ 使いやすさと簡潔さがよく考えられている印象 ✓ SQLAlchemy (python) http://www.sqlalchemy.org/ 教科書 (PoEAA) に忠実に作られている印象 ✓ 自作!自作マジお勧め! 職人が自分の道具作って何が悪い!車輪の再発明なぞ知らんがな! ActiveRecord?あれはあんまり…
  60. copyright © 2014 kuwata-lab.com all rights reserved この資料を読んだ人はこんな資料も読んでます ✓ O/R Mapperを支える技術 http://rubykaigi.org/2011/ja/schedule/details/18S08 ✓ なぜO/Rマッパーが重要か? http://www.slideshare.net/kwatch/sqlor ✓ ORM is an anti-pattern http://seldo.com/weblog/2011/06/15/orm_is_an_antipattern ✓ ORMのパフォーマンス最適化 http://www.infoq.com/jp/articles/optimizing-orm-performance ✓ Patterns Implemented by SQLAlchemy http://techspot.zzzeek.org/2012/02/07/patterns-implemented-by-sqlalchemy/
  61. ORMごときに振り回される 段階はとっとと卒業して、 より高度で本質的な問題に エネルギーを使いましょう
Advertisement