Active RecordパターンとData Mapperパターンのメモ
背景
Data Mapperパターンというのに出会った。自分がWebアプリケーションエンジニアになって初めて出会ったので調べる。また、今まで自分が使ってきたものはActive Recordパターンというらしい。せっかくの機会なので両方調べて整理する。今の自分なりの考えも出してみようと思う。
注意事項
すでにいろんな方がまとめているものを再まとめしているだけの可能性は高いです(車輪の再開発)。また、間違った解釈を書いている可能性も高いです。しかし自分の言葉でまとめてみて記録に残したかったのでメモとして残します。
ご指摘あればコメントいただけると非常に助かります🙏(誤字脱字のご指摘も大歓迎です)
事前知識
この記事を読むにあたって知っておくと良いものを列挙しておきます。自分は技術書で見たことがあったり実務の経験があったので調べるにあたり「この単語なんだ?」、「何言ってるかわからないんだけど?」となることは少なかったです。
- ORM
- ドメインオブジェクト
- エンティティ
- リポジトリ
- レイヤードアーキテクチャ
- Laravel, Ruby on Railsなどフルスタックフレームワークの実装経験
Active Recordパターン
Active Record自体はMartin Fowler氏の記事で紹介されています。
An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.
データベースのアクセスとデータに関するドメインロジックをカプセル化したオブジェクトです。
データベース層とドメイン層が同じオブジェクトに内包されているためテーブル構造に引っ張られる可能性があります。
Ruby on Railsの実装が有名(記事執筆時の最新Rails v8.0を参照)
// 例です。
class SampleModel < ApplicationRecord
def change
create_table :articles do |t|
t.text :title
t.text :body
t.timestamps
end
end
end
LaravelのEloquentORMもActive Patternの例
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SampleModel extends Model
{
// ...
}
自分は新卒でアプリケーション作ってから今まで4年はActive Recordパターンしか使ったことがなかったです。データ操作はModelが持つメソッドを使ってました。テーブルのリレーション設定もModelに書き込んでいて「データに関することはとりあえずModelに書こう」という感じでした。
個人的Active Recordパターンの気になるところ
はじめやすい/使いやすい
テーブルとORMが1:1の関係になります。
初学者が勉強するにしても小さくて単純なWebアプリケーションを作成するにしても使いやすいです。学習コストが低いと言っても良いでしょう。
フレームワークにそのまま乗れればなんとかなる
Laravel公式ドキュメントでも「リレーションがある場合は...」と説明があります。attributeもあってModelをカスタマイズする手段が豊富です。
N+1の解消も公式ドキュメントで紹介されていますし、ハマっても検索すれば大体解決できます。
- フレームワークに乗っかることでフレームワークの恩恵を受けられる
- 使ってる人が多いので知見が多い
こんなところでしょうか。
なんでもModelに書いちゃう
これはデメリットと感じています。
前提としてActive Recordパターンはテーブル=ドメインオブジェクトになりやすいです。それによってデータ操作とドメイン知識、テーブル間の関係性.... とデータに関する情報や操作が一緒くたになってしまいます。
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* モデルの肥大化
* 最初は良いが時間が経過すると神クラスになってくことが予想される
**/
class ComplexSampleModel extends Model
{
// データ操作
public function find() {}
public function create() {}
public function update() {}
public function delete() {}
// テーブル間の関係性
public function sampleTableRelation() {}
public function sampleTableRelation2() {}
// ドメインルール
public function sampleDomainRule() {}
public function sampleDomainRule2() {}
public function sampleDomainRule3() {}
// その他
private function otherFunction() {}
// ...
}
最初のうちは気にならないかもしれません。しかし、時間が経つとデータ操作は複雑になり、テーブル間で条件が出てきたりするとリレーションにもロジックが出てきたり... (ここから先は言いません)
フレームワークが元々想定していた利用方法から外れると途端に我々へ牙を剥いてきます。フレームワークの機だけでフレームワークが想定していない処理を無理矢理再現するのは無理があると感じたことが自分はあります。
Active Recordパターンについて簡単にまとめました。次は別の方法、Data Mapperパターンについてまとめます。
Data Mapperパターン
Active Record同様でMartin Fowler氏の記事で紹介されています。
A layer of mappers that moves data between objects and a database while keeping them independent of each other and the mapper itself.
オブジェクトとデータベースの間を取り持つ層。オブジェクトとデータベースは互いに独立している。
ビジネスロジックを持つ部分とデータの出し入れをする部分を一致させず分離することを目指している。
ビジネスロジック層とデータ操作層を分けることでドメインオブジェクトはデータベースが何であるかを知る必要はなくなる。
個人的Data Mapperパターンの気になるところ
ロジックを分離できる
疑似コードでDataMapperを用いてドメインオブジェクトを作ってみる。
namespace App\Domain;
class SampleDomain
{
public function __construct (
private DomainId $id,
private int $hoo,
private int $bar,
private bool $flag,
)
{
}
public static function createNew (
private DomainId $id,
private int $hoo,
private int $bar,
private bool $flag,
): self {
return new self(
$id,
$hoo,
$bar,
$flag,
);
}
public function changeHoo (int $newHoo): void
{
$this->hoo = $newHoo;
}
public function changeHoo (int $newHoo): void
{
$this->hoo = $newHoo;
}
public function changeFlag (): void
{
$this->flag = !$this->flag;
}
}
namespace App\Domain\DataMapper;
use App\Domain\SampleDomain;
use App\Database\SampleDomainRowData;
class SampleDomainMapper
{
public function convert(SampleDomainRowData $sampleDomainRowData)
{
$sampleDomain = SampleDomain::createNew(
$sampleDomainRowData['id'],
$sampleDomainRowData['hoo'],
$sampleDomainRowData['bar'],
$sampleDomainRowData['flag'],
)
}
}
ちょっと複雑なドメインオブジェクトを作ってみる。
namespace App\Domain;
use App\Domain\RelatedDomain1;
use App\Domain\RelatedDomain2;
class ComplexDomain
{
public function __construct (
private ComplexDomainId $id,
private int $hoo,
private int $bar,
private bool $flag,
// 他のデータと関連を持っている
private ?RelatedDomain1 $relatedDomain1,
private ?RelatedDomain2 $relatedDomain2,
)
{
}
public static function createNew (
private ComplexDomainId $id,
private int $hoo,
private int $bar,
private bool $flag,
private ?RelatedDomain1 $relatedDomain1,
private ?RelatedDomain2 $relatedDomain2,
): self {
return new self(
$id,
$hoo,
$bar,
$flag,
$relatedDomain1,
$relatedDomain2,
);
}
public function hasRelatedDomain1(): bool
{
return is_null($relatedDomain1);
}
}
namespace App\Domain\DataMapper;
use App\Domain\SampleDomain;
use App\Database\SampleDomainRowData;
use App\Database\RelatedDomainRowData1;
use App\Database\RelatedDomainRowData2;
class ComplexDomainMapper
{
public function convert(
SampleDomainRowData $sampleDomainRowData,
RelatedDomainRowData1 $relatedDomainRowData1,
RelatedDomainRowData2 $relatedDomainRowData2,
)
{
$sampleDomain = SampleDomain::createNew(
$sampleDomainRowData['id'],
$sampleDomainRowData['hoo'],
$sampleDomainRowData['bar'],
$sampleDomainRowData['flag'],
$relatedDomainRowData1,
$relatedDomainRowData2,
)
}
}
簡単に書いてみました。あくまでDataMapperはデータの変換です。複雑になってもMapperが膨らむだけ。ドメインオブジェクトはドメイン特有の処理のみになります。開発が進んでも責務が明確なので可読性が高そうな気がします(主観)。
純粋なSQLを書ける
テーブルと1:1のActive Recordパターンとは違います。Mapperクラスで最終的にドメインオブジェクトを作れれば良いのでDBのクエリを記述するクラスを用意し、そこにSQLを書きデータを一括で取得することが可能です。Active RecordパターンでもEager Loadでデータの一括取得は可能ですので「DataMapperパターンじゃないとできない。」ということはないです。ただSQLを自分で書くことになるので予期せぬクエリは発行されないし、クエリを追うことが容易になると考えています。
今回は話しませんが、データ取得層のことに今回触れていないので別の機会で考えないといけませんね。
ファイル数が増えます
責務でクラスを分けるのでファイル数は増えるでしょう。それにんともないディレクトリ構成も考えなくてはなりません。クラス間の連携も考えないといけません。
調べてみて
先に言うとActive RecordとData Mapperでどちらが良い、どちらが悪いということを言うつもりは一切ないです。
自分が使ってきた実装の背景を知れた
LaravelのORMが何を元に作られたものなのかがわかっていなかったと反省です。知らないものを知らないままでいるのは良くないです。
今までLaravelのORMを使っていてずっと疑問に感じていました。「なぜ、素直にSQLを書かないんだろうか」と最初は気にならなかったのです。しかし、リレーションを含んだ複雑なデータ処理でビジネスロジックを書こうとした際、「直接SQL書いた方が良くないか?」と思っていました。
自分はActive Recordパターンを知らずにActive Recordパターンを使い、未来へとんでもない負債を残した経験がたくさんあります。一度や二度ではないでしょう。知った上でActive Recordパターンを使うことが最適解と判断できたなら自分は使って良いと思います。他にも言えますが、今後は知らないまま何かを使い続けるのは避けます。
シンプルとイージーは違う
これは自分が好きな言葉です。下記スライドから拝借させていただきます。
今回は簡単なサンプルコードで止めましたが、「DataMapperパターンのようにクラスで責務を分けて実装した方が長期的にみて可読性が高く変更の加えやすい構成なのではないか?」と思いました。もちろんまだ確信はないですし仮実装すらろくにしていないので言い切れません。ただActive Recordパターンで感じた課題感を解決できるかも知れないと考えました。
フレームワークを捨てたいと言ってるわけではありません。「複雑なことを表現するのにはActive RecordパターンよりもData Mapperパターンの方が向いてるかもしれないね」、「責務を分けた方が未来のコードが健全になるかもしれないね」というだけの話です。
別のことについても深掘る必要がありそう
今回触れていませんが、調べる中で「自分、わかってないものがまだまだたくさんあるな」、「自分の整理が不十分で理解できていないものが多い」と感じました。
- エンティティ
- ドメインモデリング
- ドメインオブジェクト、ドメインロジック、ビジネスロジック?
- リポジトリパターン
上記については復習したいです。
おわり
Data Mapperパターンを実際に実装してみようと思います。
参考文献
1.Active Record
2.Design Patterns For Data Persistence
3.Active Record の基礎
4.Active Record: Laravel design pattern.
5.Data Mapper
6.Eager Loadが紹介されています
SimpleとEasyは違う / Simple is not Easy
Discussion