頼りがいのあるORM「Aniki」徹底解説!

F184d9a69a53293895f36730ca0b8289?s=47 karupanerura
December 10, 2016

 頼りがいのあるORM「Aniki」徹底解説!

YAPC::Hokkaido 2016 Sapporo

F184d9a69a53293895f36730ca0b8289?s=128

karupanerura

December 10, 2016
Tweet

Transcript

  1. 頼りがいのある O/R Mapper Aniki徹底解説 id:karupanerura YAPC::Hokkaido 2016

  2. 悪天候により お足元の悪い中 ご足労頂きまして 本当にありがとうございます

  3. ちょっとしたおことわり • 時間の関係上、駆け足での紹介となります • 資料は公開するので安心してください • 質問はリアルタイムに発言してください • 温かみのあるdisも歓迎します •

    細かいことは懇親会とかで話しましょう
  4. $ prove -v about-me.t • 1..8 • ok 1 -

    id: karupanerura • ok 2 - PAUSE ID: KARUPA • ok 3 - lang: Perl5/XS/Go/C/etc.. • ok 4 - love: ♨
  5. $ prove -v about-me.t • ok 5 - Japan Perl

    Association: 理事 • ok 6 - DeNA: Engineer (2016/09-) • ok 7 - Mackerel UG: Organizer • ok 8 - Gotanda.pm: Organizer • All tests successful.
  6. JPA x Kansai.pm http://yapcjapan.org/kansai2017/ YAPC::Kansai 2017 OSAKA

  7. 突然ですが質問です

  8. O/R Mapper について • 使ったことがある(目的問わず) • 知っているがあえて使わない • よく知らないけど興味はある •

    ぜんぜん興味ないです
  9. Perlの O/R Mapper について • 特に不満なく使っている • 不満があり渋々使っている • PerlではDBIで十分

  10. ありがとうございます!

  11. ぼくの場合

  12. ぼくのO/R Mapper事情 • WebアプリをPerlで開発 (at 前職) • ORMとして主にTengを利用 • 基本的に必要十分な機能があり快適

    • 仕様が大きくテーブルの依存関係が複雑 • リレーションシップサポートが欲しくなる
  13. Teng魔改造時代 • Tengにリレーションシップサポートを実装 • 無理やりRowクラスにリレーションシップ 情報を持たせて参照する • めちゃくちゃ複雑になる • DDLとSchema.pmと三重管理化

    • つらい
  14. DBICについて考える • DBICにはリレーションシップサポートがある • やっぱり重い • ソースが巨大で問題を追うのがつらそう • DBIx::Class •

    ほか、CPANを漁るが良さそうなのがない
  15. こまかい不満とか • JOIN句が作られるとちょっと嫌 • MySQLのindexの使われ方が予想しにくい • Schema.pmとDDLの二重管理が嫌 • 外部キー制約とかからよしなにやってくれ •

    しかし外部キー制約を外したいケースも
  16. しゃーない、作るか!

  17. Aniki 爆 誕

  18. こちらが経緯となります

  19. ほんじつのあじぇんだ • コンセプトと設計 • 基本的な使い方 • 高度な使い方 • 今後の展望 •

    まとめ
  20. コンセプトと設計

  21. 構想 • 基本的にTengを踏襲 • なるべく複雑なことはしない • シンプルなものを組み合わせて作る • 独自な実装はなるべく避ける •

    なるべく状態を持たない
  22. “Do it simple stupid!”

  23. 難しいことをしない • DBIx::Handler (DBIx::TransactionManager) • DBIx::Schema::DSL (SQL::Translator) • SQL::Maker (SQL::QueryMaker)

    • SQL::NamedPlaceholder これらのモジュールへの委譲で実現
  24. 驚き最小の法則 • 副作用は明示的に記述する • e.g.) insert • e.g.) insert_and_fetch_id •

    e.g.) insert_and_fetch_row 勝手に余計な副作用を作らない
  25. “Don’t repeat your self”

  26. 必要十分の抽象化レイヤ • Aniki::Handler 接続管理 • Aniki::Reuslt::Collection クエリ結果の集合 • Aniki::Row クエリ結果の行

    • Aniki::QueryBuilder クエリビルダ これら全てを(独自)拡張クラスに置換可能
  27. プラグイン機構 • Mouse::RoleによるTraitにより実現 • プラグインが作りやすくなっている • Mouseは高速でオーバーヘッドも少ない • RoleなのでCollectionやRowにも適用可能 •

    コアでもCollection#pagerはRoleで実装
  28. “YAGNI”

  29. 必要になるまで実装しない • リレーションシップサポート • prefetchせずともアクセサは使えるように • Rowクラス • 各カラムへのアクセサをメソッドで提供 •

    メソッドの上書きも容易
  30. 結果

  31. モジュールの関係性 Aniki 委譲 → Aniki::Filter 委譲 → Aniki::Handler (DBIx::Handler) 委譲

    → Aniki::QueryBuilder (SQL::Maker) 委譲 → Aniki::Schema (SQL::Translator) 結果 → Aniki::Result::Collection 行 → Aniki::Row Aniki::Plugin (Mouse::Role) 拡張 → 各モジュール
  32. コード行数の比較 • Teng: 1908[lines] • Aniki: 2444[lines] • DBIx::Class: 39710[lines]

  33. モジュール数の比較 • Teng: 18[modules] • Aniki: 25[modules] • DBIx::Class: 154[modules]

  34. ベンチマーク (SELECT) • 19% faster than Teng • 148% faster

    than DBIx::Class
  35. ベンチマーク (INSERT) • INSERT(and fetch row): • 18% faster than

    Teng • 40% faster than DBIx::Class • INSERT(and fetch id): • 21% faster than Teng
  36. ベンチマーク (UPDATE) • UPDATE(from row): • 34% slower than Teng

    • 49% faster than DBIx::Class • UPDATE(from where condition): • 5% faster than Teng
  37. ベンチマーク (DELETE) • DELETE(from row): • 2% slower than Teng

    • 422% faster than DBIx::Class • DELETE(from where condition): • 5% faster than Teng
  38. 小さなコードで 多くの機能を実現

  39. なかなか 高いパフォーマンス

  40. 使いたくなってこない? ん?

  41. 基本的な使い方

  42. 基本的にはTengと同じ • setupメソッドでschemaクラス等を登録 • Anikiを継承してnewしてばっちこい • ほとんどのインターフェースに互換性がある • Tengからの移行はとてもかんたん •

    一部違うインターフェースもあるがほぼ同じ
  43. Anikiのはじめかた • install-anikiでAnikiを継承したクラスを生成 • setupメソッドなどから不要なものを消す • つかう

  44. install-aniki % install-aniki --lib=./lib MyApp::DB Creating MyApp::DB ... done lib/MyApp/DB.pm

    syntax OK Creating MyApp::DB::Schema ... done lib/MyApp/DB/Schema.pm syntax OK Creating MyApp::DB::Filter ... done lib/MyApp/DB/Filter.pm syntax OK Creating MyApp::DB::Result ... done lib/MyApp/DB/Result.pm syntax OK Creating MyApp::DB::Row ... done lib/MyApp/DB/Row.pm syntax OK
  45. create table % mysqladmin -uroot create myapp % perl -Ilib

    -MMyApp::DB::Schema -e 'print MyApp::DB::Schema->output' | mysql -uroot myapp
  46. Example: setup package MyApp::DB; use Mouse; extends qw/Aniki/; __PACKAGE__->setup( schema

    => 'MyApp::DB::Schema', # required filter => 'MyApp::DB::Filter', # optional result => 'MyApp::DB::Result', # optional row => 'MyApp::DB::Row', # optional );
  47. Example: schema (1) package MyApp::DB::Schema; use DBIx::Schema::DSL; database 'MySQL'; #

    required create_table 'post' => columns { integer 'id', primary_key, auto_increment; varchar 'subject', size => 255; text 'body'; datetime 'created_at'; datetime 'updated_at'; };
  48. Example: schema (2) package MyApp::DB::Schema; create_table 'comment' => columns {

    integer 'id', primary_key, auto_increment; integer 'post_id'; varchar 'name', size => 255; text 'body'; datetime 'created_at'; datetime 'updated_at'; belongs_to 'post'; };
  49. Example: filter (optional) package MyApp::DB::Filter; use Aniki::Filter::Declare; inflate qr/_at$/ =>

    sub { my $datetime = shift; return Time::Moment->from_string($datetime.'Z', lenient => 1); }; deflate qr/_at$/ => sub { my $datetime = shift; return $datetime->at_utc->strftime('%F %T') if blessed $datetime and $datetime->isa('Time::Moment'); return $datetime; };
  50. Example: filter (optional) package MyApp::DB::Filter; trigger insert => sub {

    my ($row, $next) = @_; $row->{created_at} = Time::Moment->now; return $next->($row); }; trigger update => sub { my ($row, $next) = @_; $row->{updated_at} = Time::Moment->now; return $next->($row); };
  51. Example: connect to database use MyApp::DB; my $connect_info = [

    'dbi:mysql:dbname=myapp', 'root', '', ]; my $db = MyApp::DB->new( connect_info => $connect_info, );
  52. Example: select my $row = $db->select(post => { id =>

    1, }, { limit => 1 })->first; my @rows = $db->select(post => {})->all;
  53. Example: relationship my $row = $db->select(post => { id =>

    1, }, { limit => 1, prefetch => [qw/comment/], })->first; my @rows = $db->select(post => { id => { between => [1, 10] }, }, { prefetch => [qw/comment/], })->all;
  54. Example: row access my $post = $db->select(post => { id

    => 1 }); # post.subject my $subject = $post->subject; # resolve relationship # => SELECT * FROM comments WHERE post_id = 1 my @comments = $post->comments; # You can pre-fetch with select option # But, It's optional. Not required.
  55. Example: insert $db->insert(post => { subject => 'YAPC::Hokkaido 2016 Sapporo',

    body => '北海道だよ', }); my $id = $db->insert_and_fetch_id(post => { ..., }); my $row = $db->insert_and_fetch_row(post => { ..., });
  56. Example: update my $updated_rows_count = $db->update(post => { subject =>

    'YAPC::Kansai 2017 Osaka', body => '大阪だよ', }, { id => 1, }); $db->update($row => { body => '新大阪だよ', });
  57. Example: delete my $deleted_rows_count = $db->delete(post => { id =>

    1, }); $db->delete($row);
  58. Example: transaction $db->txn(sub { my $row = $db->select(post => {

    id => 1 }, { for_update => 1 }); my ($subject, $res) # e.g.) "ぬるぽ (1)" = ($row->subject =~ /^(.+) \((\d+)\)$/); $res++; $db->update($row => { subject => $subject." ($res)", }); $db->insert(comments => { body => "けっこう欠航でてる", }); });
  59. DEMO

  60. 高度な使い方

  61. プラグイン

  62. AnikiではPluginが使える • 実態はMouse::RoleなのでwithすればOK • Anikiのコアでもいくつかパッケージを提供 • Aniki::Plugin::Count • Aniki::Plugin::Pager •

    Aniki::Plugin::SearchJoined などなど
  63. Pluginを作るには • Mouse::Roleを使う • Mouse DSLのwithでapplyする • 以上 • プリインのプラグインの真似するといいです

  64. 例) Aniki::Plugin::Count • countメソッドを提供(定義するだけでOK) • requresで依存するメソッドを列挙 • 関係ないクラスに間違ってapplyしてハマ るのを防ぐ •

    withで適用するとcountメソッドが呼べるよ うになる
  65. 拡張

  66. Anikiのクラスは拡張可能 • handler/row/result/sql_builderのクラスを 独自のクラスに差し替えることが可能 • row/resultクラスは全体で共通となるクラス にも、テーブル毎に違うクラスにもできる • 一部だけ特別扱いとかも可能 •

    今回はよく使うであろうRowクラスを例に
  67. Rowクラスの拡張 • rowクラスにMyApp::DB::Rowを指定してい た場合 • MyApp::DB::Row::${TableName}が存在す ればそれにblessされる • たとえばpostテーブルなら MyApp::DB::Row::Postを拡張すればよい

  68. どんなときにRowを拡張するか • カラム名は変えたいけどアプリのコードの変 更は最小限にしたいとき • データの一部だけをカラムのように参照した いとき • 単行のMD5ハッシュ値などを算出したいとき •

    要するに、苦しいとき、悲しいとき
  69. リレーションシップ

  70. リレーションシップサポート • テーブル間の関係性をもとにDB操作をサポー トするもの • あるテーブルへのSELECTの結果をもとに他 のテーブルのデータもSELECTしたい(JOIN したい)ケースはよくある • オブジェクトとして自然と扱えるとうれしい

  71. Tengの場合 • Rowくらすにアクセサを生やす • 愚直にやると簡単にN+1問題を生む • 自前でJOINなり何なりをする必要がある • 同じようなコードを毎度書くのはだるい

  72. DBICの場合 • Schemaクラスに関係を定義 • 一覧できない • リレーションシップサポート自体は優秀 • 高い完成度

  73. Anikiの場合 • DBIx::Schema::DSLの外部キー定義情報を利 用 • belongs_to 'user'; # $row->user •

    逆向きの関係性も自動判別 • 外部キー制約とは別の定義も可能 • Aniki::Schema::Relationship::Declare
  74. どういうことなのか 時間があればここで サンプルコードを 見ながら解説

  75. マイグレーション

  76. Anego • id:papix作のSQL::Translatorベースのマイ グレーションツール • Anikiで利用するDBIx::Schema::DSLも SQL::Translatorベースなので相性が良い • 手軽で便利っぽい

  77. GitDDL(::Migrator) • DDLはファイルとして存在する必要がある • これもGitと同期するなら使いやすそう

  78. ごめんなさい このへんはまだ調査不足 でもポテンシャルはあるよ

  79. 今後の展望

  80. よくある質問 • ActiveRecordっぽく使いたい • ResultSetほしい つらいとおもうけど まあ、わからなくもない

  81. 機能追加 • Lint機能 • Aniki::Result::Collectionからprefetch • より便利なdeflate • ResultSet/Iteratorの追加 •

    etc..
  82. コントリビューター募集 • ぼくの時間も有限 • Anikiに機能追加したい人を求めてます • pull-reqなげてくれればレビューします • issueを立ててくれてもよいです •

    せめて一度ためしてみてください
  83. ·ͱΊ

  84. まとめ • Anikiはシンプルかつ高機能で、ある程度のハ イパフォーマンスを実現しており、たよりが いがあります • TengとDBICで迷っていて帯に短したすきに 長しと感じていた人にはピッタリでしょう • ぜひ試してみてください

  85. ありがとうございました