前回の(1)はこちら から。
O/Rマッパを作る前に
(2)では、O/Rマッパを作る前にどのようなことを検討するべきかを説明します。
既存のO/Rマッパとの違いを明確にする
既存のものとまったく同じ問題を解決したところで、単なる車輪の再発明になってしまいます。既存のものと何が違うのかを明確にする必要があります。
そのためには、前例の調査や検証なども必要になるでしょう。CPANモジュールに含まれるChangesやGitHubの過去のissueなどを見ることで、先人の経験をある程度得ることができます。また、近所のPerl MongersやYAPC(Yet Another Perl Conference )などに参加して、有識者の意見を聞くのもよいでしょう。
本当に作る必要があるのかを検討する
その問題は新しいO/Rマッパを作るに値するほどのものなのかを検討しましょう。既存のO/Rマッパに問題を解決するパッチを送ったり、O/Rマッパを使わないという手段もあります。
CPANモジュールを活用する
CPANのDBIx
ネームスペースやSQLネームスペースには、O/Rマッパの機能のサブセットと言えるようなものがいくつもあります。O/Rマッパを使うことで解決できている問題が局所的であれば、それらを組み合わせてその問題を解決することも容易なはずです。もちろん、それらを組み合わせてO/Rマッパを実装することもできます。
ここでは、Aniki
で実際に利用しているCPANモジュールをいくつか紹介します。ただ、文章だけではわかりにくい点もあるかもしれません。本誌サポートサイトから入手できるサンプルコードや各モジュールのドキュメントも併せて読んでいただくことで、より理解が深まると思います。
DBIx::Handler──データベースとの接続管理
プロセスがfork
しても、ファイルディスクリプタはコピーされません。データベースとの接続でもそれを考慮する必要があります。そうしないと、複数のプロセスから同じセッションでクエリを発行することになってしまいます。
親プロセスが単に子プロセスを待つだけなど、同じコネクションを同時に利用しないことが保証されたコードであれば、DBI
のAutoInactiveDestory
属性を利用すれば問題ありません。しかし、トランザクション中に誤ってfork
した場合のエラー処理や、その再接続のハンドリングまで考えると大変です。
そのようなデータベースベースとの接続のハンドリングを担うのがDBIx::Handler
です[1] 。後述するDBIx::TransactionManager
とうまく連携し、トランザクションの状態を加味したコネクションの一貫した接続管理を行います。
ちなみに、DBIx::Handler
とDBIx::TransactionManager
は、O/RマッパのDBIx::Skinny
やその後継となったTeng
から切り出されたモジュールです。O/Rマッパの実装に利用しやすい設計になっています。
DBIx::TransactionManager──トランザクションの管理
構造化されたコードから適切にトランザクションを扱うためには、いくつか課題があります。その問題の一つに、トランザクションのネストがあります。
複雑なアプリケーションでは、同じようなデータベース操作を別々の場面で行うことがしばしばあります。トランザクションの開始とコミットを含めて共通化すると、それと同じ単位でコミットしたい操作が出てきたときに困ります。「 この処理はトランザクションの中で実行すること」という暗黙の了解をもって共通化した場合も、呼び出し側で正しくトランザクションをかけ忘れると、レースコンディション問題[2] が発生するでしょう。あるいは、リクエスト全体でトランザクションを作るという方法も考えられるかもしれませんが、トランザクションとそのロック範囲が不必要に広くなるためパフォーマンスに難があります。
こういった場面でトランザクションのネストができると、それぞれのコンテキストでトランザクションの範囲を考えればよくなります。ただ、トランザクションのネストの挙動にはRDBMSによって違いがあり、そのまま扱うには少し難があります。
DBIx::TransactionManager
は、ネストしたトランザクションをネストの一番外側の1つのトランザクションにまとめて扱えるようにすることで、トランザクションのネストが扱いにくい問題を解決しています。具体的には、トランザクション内では実際は新たにトランザクションを作らずに仮想的なトランザクションを作り、その結果をもとに実際のトランザクションの操作に作用します。そのため、仮想的なトランザクションでロールバックが発生した場合は、トランザクション全体がその時点でロールバックされる実装になっています。
このロールバックの挙動には、少し気を付ける必要があります。なぜなら、AutoCommit
が有効なセッションで後続の処理を実行してしまうと、それらの処理はトランザクションがかかっていないので、レースコンディション問題が発生するためです。Try::Tiny
などと組み合わせて例外を捕捉してロールバックし、die
を用いて大域脱出をして、一番外側のトランザクションの外側に抜けられるような実装が望ましいです。前述のDBIx::Handler
のtxn
メソッドを利用すれば、まさにそのような挙動にできます。
SQL::Maker──柔軟なSQL生成
従業員の検索機能をイメージしてみましょう。年齢で絞り込みを行いたい場合もあれば、それに加えて性別でも絞り込みたい場合も、名字で絞り込みたい場合もあるでしょう。このように検索条件の組み合わせが多いときに、すべての組み合わせのSQLを別々に定義して利用することは現実的ではありません。
このようなケースでは、プログラム的に文字列結合を行い、SQLを作り上げる方法を思い付くでしょう。しかし、この実装方法では順序ベースで値をバインドする一般的なプレースホルダと組み合わせるのが大変で、かといってSQLに直接値を埋め込むナイーブな実装ではSQLインジェクションの温床になります。何より手続きの連続に冗長なSQLが挟まることで、コードの見通しが悪くなることがよくあります。
今回は、オブジェクト指向の素朴でシンプルなクエリビルダであり、O/Rマッパでも利用されているSQL::Maker
を紹介します。SQL::Maker
は、$maker->select(employee => ['*'], { age => 17 })などといった比較的SQLに近い表現で、SQLとプレースホルダにバインドする値を返す機能を持ちます。また、その一部であるnew_select
メソッドなどを利用することにより、SELECT
句を表現するオブジェクトを操作してSQLを組み立てることができます。たとえば、$age
が真値の場合は検索クエリにそれを含めるSQLを生成するには、new_select
メソッドを使い次のように書けます。
my $select = $maker->new_select;
$select->add_select('*')->add_from('employee');
if ($age) {
$select->add_where(age => $age);
}
my $sql = $select->as_sql;
my @bind = $select->bind;
$dbh->selectall_arrayref($sql, { Slice => {} }, @bind);
文字列結合で同等の処理を実装したコードを想像して比較してみてください。$sql
と@bind
をうまく取り回すのはなかなか骨が折れると思います。これは例なので年齢だけを検索条件にしていますが、実際の現場ではより複雑な条件を扱うことになるでしょう。
SQL::NamedPlaceholder──名前付きプレースホルダ
巨大なSQLを書く際に、printf
風に順序ベースで値をバインドしていく一般的なプレースホルダを利用すると、値を渡すべき順序を間違えやすく、バグの温床になりやすいです。かといってクエリビルダで表現するのが面倒な場合も多いでしょう。
このようなケースでは、SQL::NamedPlaceholder
などの名前付きプレースホルダを利用するのが賢い選択です。名前付きプレースホルダを利用すると、名前ベースで値をバインドできるため説明的なコードになり、間違いに気付きやすくなります。
また、SQL::NamedPlaceholder
をはじめとするPerlの名前付きプレースホルダの実装では、配列リファレンスとして渡した値をカンマ区切りで展開する機能を持つものが多いです。この機能はSQLのIN
句を利用するときに極めて便利なので、よく利用されています。
DBIx::Schema::DSL──DSLによるスキーマ定義
DBIx::Schema::DSL
は、名前のとおりDSL(Domain Specific Language 、ドメイン特化言語)でスキーマを定義できるモジュールです。Perlの内部DSLでスキーマを記述できることで、デフォルト値やカラムのサイズをconstant
プラグマを用いて一元管理できるなどのメリットがあります。
DBIx::Schema::DSL
をO/Rマッパで採用する最大のメリットは、O/Rマッパで使うスキーマ定義と実際のDDL(Data Definition Language )を同一のファイルで管理できることです。従来のO/RマッパはDDLをもとにO/Rマッパ用のスキーマのメタオブジェクトの定義を生成するスタイルのものが多く、データベースに登録したDDLとスキーマのメタオブジェクトの定義を同期する手間が必要でした。DBIx::Schema::DSL
を用いてPerlで記述したスキーマ定義からDDLを生成できるため、その手間が不要になります。
また、DBIx::Schema::DSL
は、SQL::Translator
がバックエンドになっています。SQL::Translator
は、RDBMSごとに文法や型の名前に差異があるDDLを別のRDBMS向けに変換できるモジュールです。SQL::Translator
はそのしくみ上、解釈したDDLをオブジェクトとして表現するスキーマのメタオブジェクトが必要になります。そのスキーマのメタオブジェクトを表現するためのクラスがSQL::Translator::Schema
です。DBIx::Schema::DSL
ではSQL::Translator::Schema
のオブジェクトを直接生成しています。
SQL::Translator::Schema
はDDL文の変換に使えるほど、O/Rマッパで利用するには十分すぎる機能を有しています。Aniki
ではこれを利用して、スキーマ定義を参照するしくみを実装しています。ただし、SQL::Translator::Schema
は頻繁にアクセスされることを想定していないのか、パフォーマンスが良い実装ではありません。Aniki
ではラッパクラスを定義してホットポイントで独自にキャッシュを行い高速化しています。
<続きの(3)はこちら 。>
特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT