本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはkarupaneruraこと佐藤健太さんで、テーマは「O/Rマッパ」です。
本稿のサンプルコードは、WEB+DB PRESS Vol.101のサポートサイト から入手できます。
O/Rマッパを使う良し悪し
データベースを利用した開発現場では、しばしばO/Rマッパを使うか使わないかという議論が発生します。O/Rマッパを使うと、データベースの各要素をオブジェクトとして表現できることで、本質的でない手続きを隠蔽(いんぺい)しやすくなるなどの恩恵が受けられます。その反面、データベースの具体的な操作を隠蔽してしまうことにより、誘発されやすくなる問題が存在します。
本稿では、既存のO/Rマッパにどのような問題があるのか、O/Rマッパを新たに作ることによってどのように解決できるのかを、拙作のAniki
というO/Rマッパを例に説明します。
O/Rマッパを使ったコードの特徴
同等の処理を、汎用的なデータベースインタフェースであるDBI
で実装した例と、メジャーなO/Rマッパの一つであるDBIx::Class
で実装した例を用意しました。これらのサンプルコードを比較して、O/Rマッパを使ったコードに見られる特徴とその良し悪しについて考えていきます。
DBIで実装した例
以下は、counter
テーブルのid=1
のレコードのcount
カラムをインクリメントする処理を、DBI
で実装したサンプルコードの抜粋です。
$dbh->begin_work();
my $counter = $dbh->selectrow_hashref(q{
SELECT * FROM counter
WHERE id = ? LIMIT 1 FOR UPDATE
}, undef,
1, # id = ?
);
$dbh->do(q{
UPDATE counter SET `count` = ?
WHERE id = ?
}, undef,
$counter->{count} + 1, # count = ?
$counter->{id}, # id = ?
);
$dbh->commit();
データベースに発行するSQLが明確で素朴な実装です。しかし、どのレコードに対してどのような処理を行うのかが手続きを追わなければわかりません。また、テーブル名やカラム名が頻繁に登場するのでタイプミスも犯しやすいです。
DBIx::Classで実装した例
では、O/Rマッパを使って同様の処理を実装するとどうなるでしょうか。上記と同様の部分をDBIx::Classを使って書くと、次のようになります。
$schema->txn_do(sub {
my $rs = $schema->resultset('Counter');
my $counter = $rs->find(1, { for => 'update' });
$counter->increment(1);
$counter->update();
});
counter
テーブルのレコードを表現する行オブジェクトである$counter
が振る舞いを持つことで、カウンタを1増やす処理であることがコードで簡潔に表現できています。また、テーブル名などを書く機会が減ったことにより、タイプミスなどの心配も少なくなりました。しかし、どのタイミングでどのようなSQLが発行されるのかが少しわかりにくくなりました。つまり、O/Rマッパを使うことで抽象度が高まった反面、具象が見えにくくなったということです。
実際の開発現場で起こりがちな問題
O/Rマッパの特徴とそれが持つ良し悪しを表1 にまとめました。これをもとに、実際の開発現場で問題になりやすい事柄について説明していきます。
表1 O/Rマッパの特徴ごとの良し悪し
特徴 利点 欠点
関連テーブルのレコードの一括取得 N+1問題(※ )を防ぎつつも抽象的に処理を記述できる パフォーマンスチューニングが難しい
行オブジェクトなどの拡張性 コードを抽象化しやすい 責務過多を誘発しやすい
データベース操作の抽象化 直感的なオブジェクト操作ができる N+1問題を誘発しやすい
※ ループ内でSQL発行などの副作用を伴うメソッドを呼び出してしまうことで、ループの回数分のSQL発行が行われる問題のことです。
パフォーマンスチューニングが難しい
実際の開発現場では、より複雑な仕様を限られたデータベースリソースで扱います。ただでさえRDBMS(Relational Database Management System )はスケールアウトが難しく、パフォーマンス上のボトルネックになりがちです。スケールアップにも限界があるため、パフォーマンスチューニングのしやすさは重要です。
パフォーマンスチューニングのためには、いつどこでどのようなSQLが発行されているのかという具象がわからなければなりません。そのため、データベース操作の具象の見えにくさは、開発現場で問題として顕著に現れます。
SQLのコメントとしてそのSQLの発行箇所を挿入することで、RDBMSのスロークエリログからSQLの発行箇所を特定するアプローチも一部のO/Rマッパで見られます。たとえば、SQLを発行するO/Rマッパのメソッドの呼び出し元を/* path/to/Src.pm line 32 */
などとコメントで表現してSQLに含めるという方法です。これは良いアプローチですが、SQLから離れすぎた抽象的なインタフェースの場合、それをどのように書き換えれば適切なSQLを生成できるのかがわかりにくい場合があります。
責務過多を誘発しやすい
行オブジェクトが拡張しやすいことから、ついあらゆる処理を行オブジェクトに寄せすぎて責務過多を引き起こしてしまうことも多いと思います。この問題は、行オブジェクトをそのままモデル層として扱う、いわゆるActiveRecordパターンを採用している実装でよく起こります。
実際のアプリケーションでは、複雑な関係性を持ったデータを扱うために、正規化されたテーブルに対して処理を実装していくことになります。実際の規模のアプリケーションで扱いたい問題はテーブルの単位よりもう少し抽象度が高い問題が多いはずですが、行オブジェクトを拡張して処理を実装していくことが楽であることから、行オブジェクトが濫用されがちです。
N+1問題を誘発しやすい
また、データベース操作が抽象化されることでN+1クエリ問題も引き起こしやすく、気付きにくくなります。多くのO/RマッパはN+1クエリ問題を防ぐために関連がある複数のテーブルから検索する機能を提供していますが、ほとんどの実装では自動生成されるJOIN
句を含む複雑なSQLを発行します。複雑なSQLにパフォーマンス上の問題があった場合、問題を分解して適切な実行計画を持ったSQLを発行するコードに変更することは容易ではありません。
これまでにないO/Rマッパで解決を試みる
総じて、O/Rマッパは抽象的にデータベースを扱える反面、抽象度が高まるほど具体的なデータベース操作が見えにくくなり、さまざまな問題を誘発すると考えられます。これをO/Rマッパの使い方が悪いと一蹴することは簡単ですが、今の一般的なO/Rマッパが必ずしもベストなのでしょうか。
筆者は、O/Rマッパが必要以上に抽象度を高めていることも問題だと考えました。たとえば、もう少しテーブルの行を抽象化することに特化したO/Rマッパがあってもよいのではないでしょうか。
PerlにはTMTOWTDI(There's More Then One Way To Do It )という言葉があります。これはPerlのモットーで「やり方は一つじゃない」という意味です。実際CPANには同じ問題を、良し悪しがある異なったやり方で解決するモジュールが複数あります。Perlコミュニティは従来の考え方にとらわれすぎず、新しい考え方を模索することに対して寛容なコミュニティです。上述の問題について悩んだ経験から、これらの問題を軽減あるいは解消できるこれまでにないO/RマッパをPerlで作ろうと考えました。それがAniki
です。
<続きの(2)はこちら 。>
特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT