DBIx::ObjectMapperという、PoEAA(Patterns of Enterprise Application Architecture, エンタープライズアプリケーションアーキテクチャパターン)のthe Data Mapper pattern(データマッパーパターン)に則った新手のO/Rマッパーに興味を惹かれました。その一因は、現在進行形で開発しているAmikecoに於いて、the Active Record pattern(アクティブレコードパターン)指向であるDBIx::Classを用いて、ドメインモデルの細かな機微をどこまで表現するか悩んでいることにあります。
本稿では、現在の悩みの詳細、つまりDBIx::Classでテーブル継承系パターンを強引に実現している現状をご紹介しつつ、DBIx::ObjectMapperへ期待を抱くに至った経緯を簡単に記してみます。
追記 : tokuhiromさんに素晴らしいご指摘をいただきました! 本稿でSingle Table Inheritanceを実現できないと嘆いていたのは、Active Recordであることそれ自体が原因ではありません。パターンとモジュールをやや混同して書いてしまいましたので、お詫びします。また、その記事では、DBIx::ClassでもSingle Table Inheritanceを実現していたとのご紹介をいただいているので、(本文中にも弁解を書いていたように(弱気だなぁ))DBIx::Classの実装というよりも、私の使い方が今ひとつであることが原因のようです。愚直に書いただけでパターンを実現できない現状について、何が直接の原因となっているのかということを、落ち着いて・突き詰めてよく考えるべきであることを悟りました。拙い論考に対して鮮やかな反証をご提示いただいて、感激しています。本当にどうもありがとうございました!
追記 : nekokakさんやeiskeoishiさんたちの会話は、示唆に富んでいます。要件や現実世界が複雑だからといって、脊椎反射して腕まくりしてホイホイとモデリングをがっつりやるのは、多くの場合は再考の余地がある働き方だと感じます。いかに無理・無駄を省いてシンプルなものにしていくか、勉強を深めて参りたいです。
モデルを精緻化すればするほどインピーダンスミスマッチが生じる
いわゆるエンタープライズアプリケーションでは、モデリングを精緻に行えば行うほど、、山のような数のクラスが複雑に絡み合うドメインモデルになりがちです。そのこと自体はDDD(ドメイン駆動設計)に鑑みると良い傾向にあります。ところが、それらのオブジェクトをデータストアへ永続化しようとした途端に、めくるめくインピーダンスミスマッチの世界に飛び込むことになります。
それらを解決する方策の一つがO/Rマッパーであり、Perlで最も広く使われているO/Rマッパーの一つがDBIx::Classであるため、AmikecoでもこれまでDBIx::Classを前提として設計・開発を行って来ました。
そうしたこれまでの経験を踏まえた感想は、「O/Rマッパーは銀の弾丸ではない」ということです。その論拠は色々ありますが、本稿ではインピーダンスミスマッチへの対応に絞って述べて参ります。
DBIx::Classでインピーダンスミスマッチを強引に解決する策
まず、現在のAmikecoでの解決法を、ご参考までにご紹介しておきます。これは、DBICでもテーブル継承系パターンを使えないことはない、ということの実証にもなるかも知れません。
PoEAAの第12章で掲げられたObject-Relational Structural patterns(オブジェクトリレーショナル構造パターン)のそれぞれについて、コードとクラス図を用意してみました。それらはgist: 732055で公開してあります(一式をダウンロードすることもできます)。
備考
- 私は
Moose厨なので、以下の具体例ではDBIx::Class::Coreを継承したResultクラスを(よせばいいのに)Mooseに基づいたクラスで記述しています。 - オブジェクトが持つべきデータのバリデーション(検証)を、データストアでのドメイン(この場合は定義域≒型制約の意味)のみに任せず、
Mooseの型制約を使っています。具体的には、列へのアクセッサーに対してbeforeメソッドモディファイヤーを仕込み、それがミューテーター(セッター)として呼ばれた際の引数の型を(MooseX::Params::Validateによって)検証する、ということをしています。さらに、各クラスにそうした処理を仕込んで回る手間を省くべく、それらを(MooseX::Role::Parameterizedによる)parameterized role(変数化したロール)として括り出しています。- 型の検証云々については、
DBIx::Class::MooseColumnsを使うと、アトリビュートを生やすhasのオプションとしてadd_columnを使えるので、より宣言的に記述できると思います。ただ、私がこれを評価した当時のバージョンは(MooseX::DBIC::AddColumnから改名された直後の)0.10でしたので、Moose党lazy派閥でもある私は評価を保留した経緯があります。
- 型の検証云々については、
- コードは5.8.1以上のPerlと、
Moose,DBIx::Class関係のいくつかのCPANモジュールに依存します。bin以下のスクリプトでは5.10.1以上のPerlを要求していますが、これはsayや//(defined-or演算子)で無精をしたかったためです。労を厭わなければ勿論5.8.1でも動きます。
いずれにせよ、これらの備考は本稿の本筋ではないので、この辺で仕舞いにしておきます。
Single Table Inheritance
まずは一番単純なthe Single Table Inheritance pattern(シングルテーブル継承パターン)です。関連する各クラスが一つのテーブルにマッピングされるというものです。

bin/lib/MyApp/SingleTableInheritance/Schema.pmSchema/Result/
Role/Schema/Result/
透過的にtypeを扱う
このパターンの特徴の一つであるtype列の制御は素直に行えました。このtype列とは、ただ一つのテーブルであるplayersの各行の出自、つまりどのクラスのオブジェクトを格納したものであるかを表現するものです。Resultクラスで以下のように仕込んでおけば、これは簡単に実現できます。
insertをオーバーライドして、CREATEのタイミングにフックして、type列へ自らの(具象)クラス名を突っ込むupdateをオーバーライドして、UPDATEのタイミングにフックして、type列が妙な値で上書きされないように(引数であるハッシュリファレンスからtypeキーを落とすことで)防衛する
抽象クラスで全アトリビュートを持つという乱暴な実装
ここでは、抽象クラスMyApp::SingleTableInheritance::Schema::Result::Playerにadd_columnsを全て仕込んでおき、これをそれぞれの具象クラスが継承しています。各クラスはすべて__PACKAGE__->table('players');としてマッピングしています。
抽象クラスがplayersテーブルに引き摺られてしまっているので、パターン本来のクラス図からは逸脱しています。そのため、例えばFootballerでbowling_averageを設定することができてしまう問題があります。
初っ端からオレオレルールを発動してしまうと、クラスの使い方を気をつけてこの問題を回避してしまおうという、身も蓋もない方法もあります。さすがに乱暴過ぎるので、具象クラスでinsert, updateをフックして、余所様のクラス用の列については受け付けないようにすると良いかも知れません。
Concrete Table Inheritance
続いて、理想と現実の均衡が取れたthe Concrete Table Inheritance pattern(具象テーブル継承パターン)です。具象クラス毎にテーブルが存在し、各クラスの持つアトリビュート全てが当該テーブルにマッピングされるというものです。

bin/lib/MyApp/ConcreteTableInheritance/Schema.pmSchema/Base/Result/
Result/
Role/Schema/Result/
クラス図通りにできた! ......できた ......できた?
今回のクラス図はとても綺麗で、寸分の違いもなくパターンを忠実に実装したように見えます。PK(主キー)のためのid列は抽象クラスMyApp::ConcreteTableInheritance::Schema::Base::Result::Playerにあって然るべきですので、PoEAAにある設計段階のクラス図を実装段階のクラス図として書けば、おそらく十中八九こうなるかと思います。
コードの着目点としては、抽象クラスにある__PACKAGE__->table('__PLACEHOLDER__');というインチキが挙げられます。基底クラスで__PACKAGE__->add_columnsしておけば、派生クラスはそれらの列を同様に使えるのですが、そのadd_columns自体は__PACKAGE__->tableが済んでいることを要求するからです。
具象クラス(派生クラス)側では(それぞれのクラスに紐付くテーブルを定義するために)再び__PACKAGE__->table('footballers');などとするので、怪しげな__PLACEHOLDER__が上書きされるので大丈夫だ、という安易な判断でした。ともあれ求めるスキーマはできたような気がします。
なお、わざわざ具象クラス用の名前空間MyApp::ConcreteTableInheritance::Schema::Resultではなく、MyApp::ConcreteTableInheritance::Schema::Base::Result以下にしたのは、MyApp::ConcreteTableInheritance::Schemaで(他のパターンと同様に)__PACKAGE__->load_namespaces;だけ書きたかったからです。名前空間を混ぜてしまっても、テーブルに紐付くクラスを粒で指定すれば良いので、どちらの道を行くかは好みで決めても構いません。
ともあれ、ほら、なんかうまく行ったっぽいですよ? 一抹の不安はありますけれども!
ゴミindexの恐怖
さて、この辺から雲行きが怪しくなって来ます。「怪しげな」「安易な判断」「できたような気がします」などと不安を煽る文言をちりばめましたが、このパターンの最大の泣き所は、やはり__PLACEHOLDER__にありました。
今回の例では、各パターンともnameに対してUNIQUE制約を付けているのですが、SQL::Translatorを見たり、DDLを見たり、直接DBを開けたりすれば分かる通り、ゴミindexが張られてしまっているのです。
SQLiteの場合では、concrete_table_inheritance.plでも例示していますが、.dumpすると以下のような闇を垣間見られます。
... (snip) ...
CREATE UNIQUE INDEX __PLACEHOLDER___name ON bowlers (name);
CREATE UNIQUE INDEX __PLACEHOLDER___name02 ON cricketers (name);
CREATE UNIQUE INDEX __PLACEHOLDER___name03 ON footballers (name);
CREATE UNIQUE INDEX bowlers_name ON bowlers (name);
CREATE UNIQUE INDEX cricketers_name ON bowlers (name);
CREATE UNIQUE INDEX cricketers_name02 ON cricketers (name);
CREATE UNIQUE INDEX footballers_name ON footballers (name);
なんじゃあこりゃあ!(声の出演:松田優作)
しかしこれを落ち着いて考えれば、当たり前ですが道理が通っていて、例えばFootballerの場合では、
Playerクラスが__PLACEHOLDER__テーブルと結び付こうとするPlayerクラスがadd_unique_constraintsする(つまり__PLACEHOLDER__テーブルにインデックスがあるものとされ、自動で採番されるインデックス名が__PLACEHOLDER__nameなどになる)FootballerクラスがPlayerクラスを継承するFootballerクラスがfootballersテーブルと結び付こうとするFootballerクラスがadd_unique_constraintsを(再度)呼ぶ(今度はfootballersテーブルにインデックスがあるものとされ、インデックスfootballers_nameが追加される)- 継承ツリーの末端であるので、以上で
Footballerクラスの定義がひとまず完了する(SQL::Translatorのデータ構造が安定する) - これを
deployすると、上記の状態がデータストア側へも反映される
......というようにfootballersテーブル用に2回、より深い継承ツリーを辿るbowlersでは都合3回もadd_unique_constraintsが呼ばれることが分かります。
SQL::Translatorをdumpしてみたり、コードを或る程度追った限りでは、これ以外の明白かつ重大な問題は見つけかねましたが、既にこの段階で敗戦気分が蔓延して来ました。
Class Table Inheritance
最後が、理想主義のthe Class Table Inheritance pattern(クラステーブル継承パターン)です。抽象クラスにも具象クラスにも全てテーブルが存在し、派生クラスで特化した(既定クラスに存在しない)アトリビュートのみが当該テーブルにマッピングされるというものです。

bin/lib/MyApp/ClassTableInheritance/Schema.pmSchema/Base/Result/
Result/
Role/Schema/Result/
コンポジションによって複数テーブル操作を実現
例えばBowlerを一人追加したい場合にはbowlersとcricketersとplayersをつついて回ることになりますが、これはリレーションで実現するのが手っ取り早いです。ON UPDATE CASCADEやON DELETE CASCADEの違和感もありません。
クラスの存在感が重い
今回も__PLACEHOLDER__の策動があるものの、怪しげなindexは張られません。しかし、テーブルと紐付かない(MyApp::ClassTableInheritance::Schema::Base::Result名前空間の)抽象クラスでビジネスロジック(蹴ったり打ったり投げたりするだけですが)を実装し、テーブルと紐付けるためだけに具象クラスを用意するなど、かなり面倒くさい実装になっています。
面倒くさいというのはすなわちバグの温床であると見ることができるので、これは大いなる不安を引き起こす設計だと断じてしまいたいところです。こんな混沌としたクラス群をちまちまいじっているだけで、気分はたちまち滅入ってしまうことでしょう。
貴様ッ、その手に抱えているnameメソッドは何だ!
「3つもパターンがあれば、さすがにそのうち1つくらいは満足に実装できるだろう」と思いきや、MyApp::ClassTableInheritance::Schema::Base::Result::Bowlerで多くのメソッドが明示的に生えている点が気懸かりです。
実はこのパターンでの落とし穴はここにありました。例えば、なぜnameメソッドをBowlerで実装しなければならないのでしょうか。これもまた落ち着いて考えれば道理は通っていて、
Bowlerに相対するテーブルbowlersにはname列が存在しないname列が存在しているPlayerに手を伸ばさなければならない$self->cricketers->players->nameとして、継承ツリーではなく、リレーションを手繰り寄せる必要がある
......というあんばいです。しかも具合の悪いことに、Cricketerに生えているbatメソッドも(よせばいいのに)内部でnameを使っているため、Bowlerではこれもオーバーライドしなければなりません。
理論上の継承と物理面の制約との取りなしをドメインレイヤーのクラス内部で隠蔽しているので、クラスを使う側(サービスレイヤーのクラス)ではこうした絡み合いを気にする必要はないのですが、それは結局誰に泣いてもらうかというようなジョーカーの押し付け合いでしかないような気がします。
ああ面倒くさい! もう止めだ止めだッ!
結論:欠点に目をつぶる度胸次第
かようにして、Active Record指向のDBIx::Classを馬鹿正直に使っただけでは、
- テーブル継承パターンを「使えないことはない」だけで「大船に乗ったつもりで使える」わけでないこと
- 強引に実装したのであれば、その際に仕込んだ落とし穴に留意しておく必要があること
......などを学んで参りました。
もっと上手な方法はいくらでもありそうですが、少なくとも、愚直に取り組むと悲惨な目に遭うことがわかりました。もっとも、afterなどを使った時点で愚直でも何でもなくて悪足掻きと見ることもできるかも知れません。ともあれPoEAAのクラス図と上述のクラス図を対比してすぐ気付くように、やりたいことのために設計を歪めていることは大変心苦しいです。また、__PACKAGE__->table('__PLACEHOLDER__');の副作用も把握しきれていません。
こうした浮ついた理解でプロダクトにパターンを適用することは、何としても避けなければなりません。
実際のところ、本業ではとてもそんなことはしていないものの、Amikecoではがっつり適用してしまっているわけですが......。
Data Mapperは銀の弾丸になるか
クラスに生えているアトリビュートとテーブルの列との整合性を力尽くで合わせることが諸悪の根源である(というかO/Rマッパーの使い方を間違えている)のであれば、現状に拘泥せずに、それらが完全にぶった切られた世界のことを知る必要がありそうです。
その世界、つまりData Mapperは必ずしも理想郷ではないかも知れませんが、PoEAAのData Source Architectural Patterns(データソースのアーキテクチャーに関するパターン)でActive Recordと並立して紹介されていることからも分かるように、少なくとも実現可能性評価をしておかなければならない存在にあることは確かでしょう。
実はごく最近に.NETでNHibernate(Javaで最も有名なO/Rマッパーの一つであるHibernateの.NET版)を使う案件があったので、エンタープライズアプリケーションに於けるData Mapperの威力の片鱗は肌感覚として感じています。
新星、DBIx::ObjectMapper
そんな折、色々な意味でリハビリ中の折に、YAPC::Asia 2010での@eiskeoishiさんによるDBIx::ObjectMapperの発表スライドを拝見して、このData Mapper指向のORMがPerlにもあることを知ったのです。
それではここで、「Object-Relational Structural patterns、どんと来いです」(意訳)としているDBIx::ObjectMapper選手が、こうした問題をたちどころに解決してくれる(らしい)ことを見て回りましょう。
パターン通りのクラス!
クラスとテーブルとの紐付けをmapperに任せ、かつ、その紐付けが「全テーブルの列と全クラスのアトリビュートを一旦ご破算にして、それらを粒で紐付けて回る」ものである以上、データストアのことを全く気にせず、クラスをパターン通りに書けることは当然です。POPO(Plain Old Perl Object)は、今こそデータストアの桎梏から逃れ、大空を自由に飛び回ることができるのです。
これだけでも上述の問題の過半は解消しているのですが、そのほかにも各パターン固有の問題についても確認しておきます。
Single Table Inheritanceではtypeを宣言的に記述可能!
スライド(pp. 64-69)を拝見すると、typeも宣言的に記述できます。
Concrete Table Inheritanceからはゴミindexが一掃!
変なことをしなければ変な結果は生まれないわけで、ゴミindexの生まれる余地がないことが分かります。
Class Table Inheritanceでは無駄なメソッド記述が不要に!
テーブルの列が同時に複数のクラスのアトリビュートと結び付いても良いため、例えばBowlerでnameを生で呼んでも、mapperが面倒を見てくれる結果、cricketersテーブルのname列を触りに行ってくれます。手作り感溢れるマッピングは不要になるわけです。
全国のDBIx::ObjectMapper使用者の喜びの声(使用者の感想であり効果を保証するものではありません)
素晴らしい、DBIx::ObjectMapper! 後は、
DBIx::ObjectMapperで栄転の辞令を貰いました!DBIx::ObjectMapperでTOEICのスコアが300点も上がりました!DBIx::ObjectMapperで身長が10cm伸びました!DBIx::ObjectMapperで宝くじの2等を当てました!DBIx::ObjectMapperで彼女ができました!
......などという体験談を仕込むと完璧です。最後のものは割と本気で期待しているのですが、誠に遺憾ながら、今年のクリスマスも中止とすべきようです。
苦悩からの解放とその代償
簡素かつ愚直にすべき......なのだけれども
閑話休題。そろそろテーブル継承が辛くなってきたので、この苦悩から解放してくれるのなら、神でも悪魔でも頼りたいという気分でありました。しかし計算機上の魔術の基本も等価交換であって、学術的に素晴らしいものでも、それが現実と遊離していると使えないので注意が必要です。その端的な例が、「テーブルをどこまで正規化するか」という命題でしょう。徹底的に正規化されたことによる洗練度の高さを学術的に評価されるスキーマであっても、更新頻度と実行時性能と開発・保守生産性を勘案して、ある程度までで留めておくというのは、大変よくある事例です。
他にも『モダンPerl入門』のpp.97-101辺りで言及されているような、
JOINが必要となるような複雑なテーブルはORMではできるだけ触らない- 読み込み用のキャッシュ的テーブルと、書き込み用のテーブルを分ける
......などの技巧は地に足の着いたもので、大いに納得するところです。「そもそもテーブル継承が必要な作りにすること自体が間違いなのだ」という論には、強い説得力があります。つまりはKeep It Simple and Stupidということですね。
今回の場合、Data Mapperはマッパーというレイヤーを新たに一つ設ける営みであるため、抽象度が増す、つまり複雑度が増すということは厳然たる事実です。
富豪的プログラミング万歳
一方で、4Gbpsを超えるようなシステムを業務として開発・運用する場合と、日曜プログラミングの場合とを比較すると、問題への接近方法が少なからず異なってくるようにも思えます。言い換えるならば、大規模なサービスやミッションクリティカルなサービスでない限り、いわゆる「富豪的プログラミング」が許される場面が少なからずあるはずだ、と考えます。
自分で複雑な物を作り上げてしまうのではなく、複雑なことを隠蔽してくれる便利な道具を使うのであれば、市井のエンドユーザーであり間抜けでもある自分が「複雑なことをして自滅する」ことは回避できるはずです。
それは例えば、
Data::ModelやDBIx::Skinnyなども素敵だけれども、敢えてDBIx::ClassをClass::Accessor::Fastなども質実剛健だけれども、敢えてMooseをAmonなども巧妙だけれども、敢えてCatalystやArkを
......選択するという、つまり小気味良い軽薄短小ではなく、鈍重かもしれなくても重厚長大なフレームワークにお世話になるという戦略の一環と見なすこともできるでしょう。
徒労感溢れる作業をいかに低減するか、あるいは甘受するか
マッピング地獄
光にばかり目を奪われずに、影についても検討してみましょう。Data Mapperの鬼門は、やはりマッピング地獄にあります。全ての列と全てのクラスアトリビュートがばらばらになった後、誰がその紐付けを定義するのか。それは自分なのです。こうして、いわゆるXML地獄がその大きな口を開けているという素敵な構図が浮かび上がります。全国のHibernate使用者の喜びの声にもあるように、私も痛い目に遭いました。
前述の通り.NETでNHibernateを使った際に、3桁になんなんとするテーブルを取り扱う都合上、マッピング用の設定ファイルやらドメインレイヤーのクラスやらを手作りではなく自動生成で出そうとしました。関係がぶった切られるとはいえども、テーブルの列とクラスのアトリビュートが1:1で結び付く割合はかなり高いからです。そして、インピーダンスミスマッチが起こる箇所や、追加で何かをしたい箇所にのみ、生成後のファイルやクラスを修正するという手順を考えていました。
しかし、スキーマを読み取ってそれらを生成する既存のツールには一長一短がありました。結局Perl + SQL::Translator + Text::Xslateでオレオレジェネレーターを作ってしまったという、仕様もない落ちもあったりします。NHibernateはHibernate然として使えますが、周辺ツール類までHibernate級の存在感を期待するのはお門違いだったのかも知れません。大いに反省しています。
自動生成という「隣の青い芝」
さて、これは一つの示唆であるように思えます。「ためにする作業」は徒労感が募りますし、コード量の増加はバグの増加に直結します。そもそも私がPerlへ惹かれている理由の一つは、「簡単に書けること」であったはずです。
例えばDBIx::Classを使う場合、データストア側かドメインレイヤーのクラス側のどちらかの作業を自動化することが少なくありません。
- MySQL WorkbenchなどでERモデリングを行い、DDLを自動生成し、
DBIx::Class::Schema::Loaderでそれを読み込む(DDLとドメインレイヤーのクラスを自動生成する) - ドメインレイヤーのクラスを自分で書き上げ、
DBIx::Class::Schema::Deployでそれを反映する(DDLを自動生成する)
もしそうでないとしたら? ERモデリングにも全力で取り組んで、ドメインモデリングにも全力で取り組むというだけの熱意・根気・義務感・勤勉さ・緻密さ・夢・希望・愛・その他諸々は、今の私のポケットのどこをはたいても出て来ません。いや、誇張がありましたので軌道修正すると、「隣の青い芝」が気になってしまいます。
スライドを拝見した限りでは、$mapper->metadata->tableの引数たるハッシュで、値をautoloadとすれば、1:1マッピングにかかる仕事が終わるようにお見受け致しました。しかしドメインレイヤーのクラス群を書く仕事は残っています。例えばDBIx::ObjectMapper::Metadata::Loader(仮称)のようなものが、SQL::Translatorでスキーマを読み取りつつ、その結果を回して(Mooseベースのクラスであれば)hasを吐いて回るようだと、かなり無精になれそうです。
NHibernate用ジェネレーターのアドホックな実装経験を活かせれば良いのですが、オレオレイカサマジェネレーターで大火傷を負う未来絵図も脳裏に浮かぶので、ここは慎重になりたいところです。
冒険すべき時と場合・冒険すべきでない時と場合
自動生成という名の青い鳥を追い求めるのは、果たして是か非か。自動生成がないからといってこれまで通りの混沌とした設計を貫き通すのか。これはなかなか深刻な命題ですが、ヴェイパーウェア疑惑が付いて回るAmikecoでは、もはや冒険をすべき時期ではなくなっていると考えます。技術的負債を償却するための良い機会ではありますが、そこでさらに償却方法についても背伸びをしてしまうのはやり過ぎだと悟りました。
手作りして回ったとしても、Active Recordで突き進んで破滅するよりは遙かに優れた結果を得ることができるはずなので、まずは手作りで頑張ってみようと思います。
まとめ ~ 適用可否を本気で検討したい
......などと、冒頭に「簡単に記してみます」などと書いておきながら、延々と自問自答しないと新しいものに手を出せない私は、もう少し素直に生きた方が良いと思います(だから彼女ができないんですね)。
もとい。選択肢があるなら試してみたいし、開発・保守の生産性や、実行性能を比較してみたい。これが現在の正直な気持ちです。ERモデリングもドメインモデリングも行うかわりに、混沌とした設計を脱する方が生産性が高いのか。抽象化をさらに推し進めたData Mapperで、それなりに満足の行く実行性能を叩き出せるのか。今から楽しみでなりません。
今後はAmikecoへの適用可否の検証を通してDBIx::ObjectMapperを評価し、その結果を別稿としてまとめていきたい、と考えています。その際には、(DBIx::ObjectMapperのスライドで語り尽くされた感もありますが)DBIx::ObjectMapperで仕上げた場合の実例も(今回の例示法に基づいて)載せるつもりです。
コメントする