Perl Hackers Hub

第47回Anikiで学ぶ実践的なO/Rマッパの作り方(1)

本連載では第一線の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発行が行われる問題のことです。

パフォーマンスチューニングが難しい

実際の開発現場では、より複雑な仕様を限られたデータベースリソースで扱います。ただでさえRDBMSRelational 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にはTMTOWTDIThere's More Then One Way To Do Itという言葉があります。これはPerlのモットーで「やり方は一つじゃない」という意味です。実際CPANには同じ問題を、良し悪しがある異なったやり方で解決するモジュールが複数あります。Perlコミュニティは従来の考え方にとらわれすぎず、新しい考え方を模索することに対して寛容なコミュニティです。上述の問題について悩んだ経験から、これらの問題を軽減あるいは解消できるこれまでにないO/RマッパをPerlで作ろうと考えました。それがAnikiです。

<続きの(2)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.130

2022年8月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-13000-8

  • 特集1
    イミュータブルデータモデルで始める
    実践データモデリング

    業務の複雑さをシンプルに表現!
  • 特集2
    いまはじめるFlutter
    iOS/Android両対応アプリを開発してみよう
  • 特集3
    作って学ぶWeb3
    ブロックチェーン、スマートコントラクト、NFT

おすすめ記事

記事・ニュース一覧