Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

$ 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: ♨

Slide 5

Slide 5 text

$ 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.

Slide 6

Slide 6 text

JPA x Kansai.pm http://yapcjapan.org/kansai2017/ YAPC::Kansai 2017 OSAKA

Slide 7

Slide 7 text

突然ですが質問です

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

ありがとうございます!

Slide 11

Slide 11 text

ぼくの場合

Slide 12

Slide 12 text

ぼくのO/R Mapper事情 • WebアプリをPerlで開発 (at 前職) • ORMとして主にTengを利用 • 基本的に必要十分な機能があり快適 • 仕様が大きくテーブルの依存関係が複雑 • リレーションシップサポートが欲しくなる

Slide 13

Slide 13 text

Teng魔改造時代 • Tengにリレーションシップサポートを実装 • 無理やりRowクラスにリレーションシップ 情報を持たせて参照する • めちゃくちゃ複雑になる • DDLとSchema.pmと三重管理化 • つらい

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

こまかい不満とか • JOIN句が作られるとちょっと嫌 • MySQLのindexの使われ方が予想しにくい • Schema.pmとDDLの二重管理が嫌 • 外部キー制約とかからよしなにやってくれ • しかし外部キー制約を外したいケースも

Slide 16

Slide 16 text

しゃーない、作るか!

Slide 17

Slide 17 text

Aniki 爆 誕

Slide 18

Slide 18 text

こちらが経緯となります

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

コンセプトと設計

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

“Do it simple stupid!”

Slide 23

Slide 23 text

難しいことをしない • DBIx::Handler (DBIx::TransactionManager) • DBIx::Schema::DSL (SQL::Translator) • SQL::Maker (SQL::QueryMaker) • SQL::NamedPlaceholder これらのモジュールへの委譲で実現

Slide 24

Slide 24 text

驚き最小の法則 • 副作用は明示的に記述する • e.g.) insert • e.g.) insert_and_fetch_id • e.g.) insert_and_fetch_row 勝手に余計な副作用を作らない

Slide 25

Slide 25 text

“Don’t repeat your self”

Slide 26

Slide 26 text

必要十分の抽象化レイヤ • Aniki::Handler 接続管理 • Aniki::Reuslt::Collection クエリ結果の集合 • Aniki::Row クエリ結果の行 • Aniki::QueryBuilder クエリビルダ これら全てを(独自)拡張クラスに置換可能

Slide 27

Slide 27 text

プラグイン機構 • Mouse::RoleによるTraitにより実現 • プラグインが作りやすくなっている • Mouseは高速でオーバーヘッドも少ない • RoleなのでCollectionやRowにも適用可能 • コアでもCollection#pagerはRoleで実装

Slide 28

Slide 28 text

“YAGNI”

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

結果

Slide 31

Slide 31 text

モジュールの関係性 Aniki 委譲 → Aniki::Filter 委譲 → Aniki::Handler (DBIx::Handler) 委譲 → Aniki::QueryBuilder (SQL::Maker) 委譲 → Aniki::Schema (SQL::Translator) 結果 → Aniki::Result::Collection 行 → Aniki::Row Aniki::Plugin (Mouse::Role) 拡張 → 各モジュール

Slide 32

Slide 32 text

コード行数の比較 • Teng: 1908[lines] • Aniki: 2444[lines] • DBIx::Class: 39710[lines]

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

ベンチマーク (SELECT) • 19% faster than Teng • 148% faster than DBIx::Class

Slide 35

Slide 35 text

ベンチマーク (INSERT) • INSERT(and fetch row): • 18% faster than Teng • 40% faster than DBIx::Class • INSERT(and fetch id): • 21% faster than Teng

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

小さなコードで 多くの機能を実現

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

基本的な使い方

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Anikiのはじめかた • install-anikiでAnikiを継承したクラスを生成 • setupメソッドなどから不要なものを消す • つかう

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

create table % mysqladmin -uroot create myapp % perl -Ilib -MMyApp::DB::Schema -e 'print MyApp::DB::Schema->output' | mysql -uroot myapp

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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'; };

Slide 48

Slide 48 text

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'; };

Slide 49

Slide 49 text

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; };

Slide 50

Slide 50 text

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); };

Slide 51

Slide 51 text

Example: connect to database use MyApp::DB; my $connect_info = [ 'dbi:mysql:dbname=myapp', 'root', '', ]; my $db = MyApp::DB->new( connect_info => $connect_info, );

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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;

Slide 54

Slide 54 text

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.

Slide 55

Slide 55 text

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 => { ..., });

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Example: delete my $deleted_rows_count = $db->delete(post => { id => 1, }); $db->delete($row);

Slide 58

Slide 58 text

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 => "けっこう欠航でてる", }); });

Slide 59

Slide 59 text

DEMO

Slide 60

Slide 60 text

高度な使い方

Slide 61

Slide 61 text

プラグイン

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

拡張

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Rowクラスの拡張 • rowクラスにMyApp::DB::Rowを指定してい た場合 • MyApp::DB::Row::${TableName}が存在す ればそれにblessされる • たとえばpostテーブルなら MyApp::DB::Row::Postを拡張すればよい

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

リレーションシップ

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

Anikiの場合 • DBIx::Schema::DSLの外部キー定義情報を利 用 • belongs_to 'user'; # $row->user • 逆向きの関係性も自動判別 • 外部キー制約とは別の定義も可能 • Aniki::Schema::Relationship::Declare

Slide 74

Slide 74 text

どういうことなのか 時間があればここで サンプルコードを 見ながら解説

Slide 75

Slide 75 text

マイグレーション

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

今後の展望

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

·ͱΊ

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

ありがとうございました