Aniki──これまでになかった抽象度を実現したO/Rマッパ
これまでのO/Aniki
です。
既存のO/Rマッパとの違い
Aniki
はこれまでのO/Aniki
のデータベース操作を行うメソッドの名前はselect
やinsert_
などにしています。これにより、
反面として、Aniki
では行オブジェクトを不変オブジェクト
また、
本当に作る必要があるのか
表1に挙げたような既存のO/
もちろん、
また、Aniki
です。
セットアップ
Aniki
について詳しく知るために、
まずはcpanmでAniki
をインストールします。
インストールに成功すると、install-aniki
コマンドが利用可能になります。Aniki
を利用するネームスペースとモジュールの生成先ディレクトリを指定して実行すればセットアップが完了します。
セットアップされるモジュールは表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
を利用して簡単な地理とその間のフライトを管理するスキーマを用意しました
次項以降では、Aniki
の各機能を見ていきましょう。
接続ハンドリング
デフォルトでは、Aniki::Handler
に委譲して接続ハンドリングを管理します。Aniki::Handler
は基本的にはDBIx::Handler
と同じ機能を提供しています。
特筆するべき点としては、MyApp::DB
のsetup
メソッドにhandler
オプションを渡すことで、
具体的なユースケースとして、Aniki::Handler::WeightedRoundRobin
を利用すれば、Data::WeightedRoundRobin
とDBIx::Handler
を組み合わせて重み付けをしつつ、
SQLの発行
Aniki
は大きく分けて2種類のSQL発行方法をサポートしています。クエリビルダによる動的なSQL生成を伴うSQLの発行と、select
などのSQLに寄った名前のメソッドを使います。
これらのメソッドからクエリを発行した結果は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
では外部キー制約から自動的にこの関係を抽出して利用できます。
具体的なコードは次のようになります。
これらのアクセサの生成と命名はスキーマから自動で行われますが、
アクセサの命名規則による関係性の表現
この機能は、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