Aniki──これまでになかった抽象度を実現したO/Rマッパ
これまでのO/Anikiです。
既存のO/Rマッパとの違い
AnikiはこれまでのO/Anikiのデータベース操作を行うメソッドの名前はselectやinsert_などにしています。これにより、
反面として、Anikiでは行オブジェクトを不変オブジェクト
また、
本当に作る必要があるのか
表1に挙げたような既存のO/
もちろん、
また、Anikiです。
セットアップ
Anikiについて詳しく知るために、
まずはcpanmでAnikiをインストールします。
$ cpanm Anikiインストールに成功すると、install-anikiコマンドが利用可能になります。Anikiを利用するネームスペースとモジュールの生成先ディレクトリを指定して実行すればセットアップが完了します。
$ install-aniki --lib=./lib MyApp::DBセットアップされるモジュールは表2のとおりです。
| 名前 | 機能 |
|---|---|
| MyApp::DB | データベースを表現する |
| MyApp::DB::Schema | スキーマ定義を定義する |
| MyApp::DB::Filter | inflate/ |
| MyApp::DB::Result | SQLの実行結果を表現する |
| MyApp::DB::Row | データベースの行を表現する |
スキーマ定義
install-anikiで生成したMyApp::DB::Schemaでは、DBIx::Schema::DSLを利用してスキーマを定義します。
サンプルとして、DBIx::Schema::DSLを利用して簡単な地理とその間のフライトを管理するスキーマを用意しました
create_table country => columns {
integer 'id', primary_key;
varchar 'name', size => 32;
varchar 'region', size => 32;
};
create_table city => columns {
integer 'id', primary_key;
varchar 'name', size => 32;
integer 'country_id';
belongs_to 'country';
};
create_table flight => columns {
integer 'id', primary_key;
varchar 'name', size => 32;
integer 'departure_city_id';
integer 'arrival_city_id';
fk 'departure_city_id' => 'city', 'id';
fk 'arrival_city_id' => 'city', 'id';
};次項以降では、Anikiの各機能を見ていきましょう。
接続ハンドリング
デフォルトでは、Aniki::Handlerに委譲して接続ハンドリングを管理します。Aniki::Handlerは基本的にはDBIx::Handlerと同じ機能を提供しています。
特筆するべき点としては、MyApp::DBのsetupメソッドにhandlerオプションを渡すことで、
package MyApp::DB;
...
__PACKAGE__->setup(
...
handler => 'MyApp::DB::Handler',
);具体的なユースケースとして、Aniki::Handler::WeightedRoundRobinを利用すれば、Data::WeightedRoundRobinとDBIx::Handlerを組み合わせて重み付けをしつつ、
SQLの発行
Anikiは大きく分けて2種類のSQL発行方法をサポートしています。クエリビルダによる動的なSQL生成を伴うSQLの発行と、selectなどのSQLに寄った名前のメソッドを使います。
# クエリビルダによる動的なSQL生成を伴うSQLの発行
my @countries = $db->select(country => {
region => 'Asia',
})->all;
# 静的に記述したSQLの発行
my @countries = $db->select_by_sql(q{
SELECT * FROM country WHERE region = ?
}, ['Asia'])->all;これらのメソッドからクエリを発行した結果はMyApp::DB::Resultのインスタンスです。このクラスはひな型でAniki::Result::Collectionを継承しています。コレクションとして振る舞うためallメソッドを呼び出すことですべての行を得ることができます。デフォルトでは各行はMyApp::DB::Rowのインスタンスとしてマッピングされますが、suppress_オプションでマッピングを抑制して純粋なハッシュリファレンスで行を表現することもできます。
RowとResultはどちらもそのネームスペースに、countryテーブルに対応するRowクラスを定義したい場合は、MyApp::DB::Rowを継承するMyApp::DB::Row::Countryという名前のクラスを定義すればよいでしょう。
このように行オブジェクトなどはテーブルごとにクラスを分けることができますが、FROM句からテーブル名を抽出して利用することもできます。table_オプションで明示的に指定することもできます。
クエリビルダ
AnikiではクエリビルダとしてSQL::Makerを利用しています。Anikiではデフォルトでプリペアドステートメントのためにprepare_を利用します。
キャッシュヒット率を向上させるため、WHERE句のカラムの順序が同じ順番になるように、
リレーションシップサポート
Anikiでは、Aniki::Schema::Relationship::Declareを利用することで外部キー制約に依存せずにリレーションシップを定義することもできます。外部キー制約を利用したほうが実態に即した状態に維持しやすいため、
ここでは主に外部キー制約の解釈について説明します。
スキーマにはbelongs_によって外部キー制約が定義されています。これはcity.にはcountryテーブルに存在するidの値しか入らないという制約を簡潔に定義するためのエイリアスです。プライマリキー制約やユニーク制約を見てみると、countryとcityは1対多の関係にあることがわかります。つまり、countryは複数のcityを持つ可能性があり、cityは単一のcountryしか持たないということです。Anikiでは外部キー制約から自動的にこの関係を抽出して利用できます。
具体的なコードは次のようになります。
my $country = $db->select(country => {
id => 1,
}, {
prefetch => [qw/cities/],
})->first;
$country->cities; # Array[city]これらのアクセサの生成と命名はスキーマから自動で行われますが、
アクセサの命名規則による関係性の表現
この機能は、cityのcountry_からcountryのidに対して外部キー制約が定義されているため、countryという名前で関連レコードへのアクセサが作られます。
関連レコードへのアクセサはテーブル間の関係によって得られるべき情報が違います。たとえば、countryのレコードに対して紐付くcityのレコードは複数ある可能性があります。しかし、cityのレコードに対して紐付くcountryのレコードは1つしかあり得ません。つまり、countryのレコードに紐付くのはcityの集合であり、cityのレコードに紐付くのは単一のcountryです。
集合をデータ構造で表現するためには配列などを利用する必要がありますが、Anikiでは複数個以上のレコードに対するアクセサの場合は複数形の名前でアクセサが作られるしくみにしています。
例を出すと、countryの行オブジェクトからはcityテーブルのレコードに対して複数形の名前であるcitiesでアクセサが作られます。逆に、cityの行オブジェクトからはcountryテーブルのレコードに対して単数形の名前であるcountryでアクセサが作られます。このように、
Perlで英語の単語を単数形/Lingua::EN::Inflectモジュールが利用できます。Anikiでは後述する接頭辞を無視するために、departure_という名前であれば最後にあたるcityを複数形にすることで、departure_にして自然な名前にできます。
接頭辞の解釈
テーブル名を解釈するだけでは不十分な場合もあるでしょう。具体的には、flightテーブルのdeparture_やarrival_カラムのように、
たとえば、flightテーブルのdeparture_カラムに対する外部キー制約からは、departureという接頭辞が抜き出されます。そして、departure_として、cityを複数形にしたdeparture_という名前でアクセサが作られます。これは一見ややこしいですが、
プリフェッチ機能
selectメソッドにprefetchオプションを指定することで、JOINが用いられますが、AnikiではトランザクションとIN句を使ってプリフェッチを実現しています。その結果、
プラグイン
Anikiではプラグインとして、COUNT関数を利用したクエリを手軽に発行するためのAniki::Plugin::Countや、JOIN句を利用するためのAniki::Plugin::SelectJoined、Aniki::Plugin::Pagerなどを標準で用意しています。AnikiのプラグインはすべてMouse::Roleとして実装されているため、Mouseのwith句を利用してプラグインを適用できます。
特筆するべきは、Mouse::Roleの定義を利用して簡単に問題を解決できます。
さらに、Mouse::Roleのrequiresを利用すれば特定のメソッドを持つクラスだけにプラグインを適用させることもできるので、requiresに指定することで特定のカラムが存在するテーブルのみにプラグインを適用させることもできます。また、Mouse::Roleは別のMouse::Roleを内包できるため、Aniki::Plugin::PagerとAniki::Plugin::SQLPagerがありますが、Aniki::Plugin::PagerInjectorとして共通化しています。
まとめ
本稿では、Anikiのような実践的なO/
筆者が挙げた問題は、DBIx::ClassもTengも帯に短し襷Anikiもご検討頂けると幸いです。
さて、
本誌最新号をチェック!
WEB+DB PRESS Vol.130
2022年8月24日発売
B5判/
定価1,628円
ISBN978-4-297-13000-8
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現! - 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう - 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、 NFT

