1. Unit of Workパターンとは ● DomainModelの変更を都度DBに反映せず、 ビジネスロジック完了後にまとめて反映 ● 変更は監視され、最小のSQLで同期される ● ActiveRecordとUnit of Workは排他的 ● DataMapperはUnit of Workと自然に共存する
2. ActiveRecord vs DataMapper use App\Models\Member; use App\Models\Post; $member = new Member(); $member->name = "Alice"; $member->save(); // この時点でデータベースに保存 $post = new Post(); $post->title = "A post by Alice"; $member->posts()->save($post); // この時点でデータベースに保存 Eloquent (ActiveRecord) アッコちゃん Doctrine (DataMapper) ドクくん
2. ActiveRecord vs DataMapper use App\Models\Member; use App\Models\Post; $member = new Member(); $member->name = "Alice"; $member->save(); // この時点でデータベースに保存 $post = new Post(); $post->title = "A post by Alice"; $member->posts()->save($post); // この時点でデータベースに保存 Eloquent (ActiveRecord) use App\Entities\Member; use App\Entities\Post; $em = app("em"); $member = new Member(); $member->setName("Alice"); $post = new Post(); $post->setTitle("A post by Alice"); $member->addPost($post); $em->persist($member); $em->persist($post); $em->flush(); Doctrine (DataMapper) Eloquent (ActiveRecord) アッコちゃん Doctrine (DataMapper) ドクくん
2. ActiveRecord vs DataMapper use App\Models\Member; use App\Models\Post; $member = new Member(); $member->name = "Alice"; $member->save(); // この時点でデータベースに保存 $post = new Post(); $post->title = "A post by Alice"; $member->posts()->save($post); // この時点でデータベースに保存 Eloquent (ActiveRecord) use App\Entities\Member; use App\Entities\Post; $em = app("em"); // EntityManager $member = new Member(); $member->setName("Alice"); $post = new Post(); $post->setTitle("A post by Alice"); $member->addPost($post); $em->persist($member); $em->persist($post); $em->flush(); Doctrine (DataMapper) EntityManagerを呼ぶ Eloquent (ActiveRecord) アッコちゃん Doctrine (DataMapper) ドクくん
2. ActiveRecord vs DataMapper use App\Models\Member; use App\Models\Post; use Illuminate\Support\Facades\DB; DB::transaction(function () { $member = new Member(); $member->name = "Alice"; $member->save(); $post = new Post(); $post->title = "A post by Alice"; $member->posts()->save($post); }); Eloquent (ActiveRecord) use App\Entities\Member; use App\Entities\Post; $em = app("em"); $member = new Member(); $member->setName("Alice"); $post = new Post(); $post->setTitle("A post by Alice"); $member->addPost($post); $em->persist($member); $em->persist($post); $em->flush(); Doctrine (DataMapper) Eloquent (ActiveRecord) アッコちゃん Doctrine (DataMapper) ドクくん 少し想像してみよう...
2. ActiveRecord vs DataMapper use App\Models\Member; use App\Models\Post; use Illuminate\Support\Facades\DB; $member = new Member(); $member->name = "Alice"; // 長い処理 $post = new Post(); $post->title = "A post by Alice"; DB::transaction(function () use ($member, $post) { $member->save(); $member->posts()->save($post); }); Eloquent (ActiveRecord) use App\Entities\Member; use App\Entities\Post; $em = app("em"); $member = new Member(); $member->setName("Alice"); // ここの間に、なにか長い処理があった場合 // Postの内容をどこか別の場所から取るとか // sleep(3); $post = new Post(); $post->setTitle("A post by Alice"); $member->addPost($post); $em->persist($member); $em->persist($post); $em->flush(); Doctrine (DataMapper) Eloquent (ActiveRecord) アッコちゃん Doctrine (DataMapper) ドクくん トランザクションから脱出しま す
1. (再掲) Unit of Workパターンとは ● DomainModelの変更を都度DBに反映せず、 ビジネスロジック完了後にまとめて反映 ● 変更は監視され、最小のSQLで同期される ● ActiveRecordとUnit of Workは排他的 ● DataMapperはUnit of Workと自然に共存する
こんなインターフェイスもあるし😎 namespace Doctrine\ORM; interface EntityManagerInterface extends ObjectManager { /** * Gets the UnitOfWork used by the EntityManager to coordinate operations. * * @return UnitOfWork */ public function getUnitOfWork(); }
2. ActiveRecord vs DataMapper use App\Entities\Member; interface UowServiceInterface { public function createMembers(Member $entity): void; public function updateMembers(Member $entity): void; public function deleteMembers(Member $entity): void; public function commit(): void; } Interface class UowService implements UowServiceInterface { public function __construct( private readonly EntityManagerInterface $entityManager ) { } public function createMembers(Member $entity): void { $this->entityManager->persist($entity); } public function updateMembers(Member $entity): void { $this->entityManager->persist($entity); } public function deleteMembers(Member $entity): void { $this->entityManager->remove($entity); } public function commit(): void { // 永続化 $this->entityManager->flush(); } } 具象 3. Unit of Workパターン サンプル
2. ActiveRecord vs DataMapper use App\Entities\Member; interface UowServiceInterface { public function createMembers(Member $entity): void; public function updateMembers(Member $entity): void; public function deleteMembers(Member $entity): void; public function commit(): void; } Interface class UowService implements UowServiceInterface { public function __construct( private readonly MemberRepositoryInterface $memberRepository ) { } public function createMembers(Member $entity): void { $this->memberRepository->add($entity); } public function updateMembers(Member $entity): void { $this->memberRepository->update($entity); } public function deleteMembers(Member $entity): void { $this->memberRepository->remove($entity); } public function commit(): void { // 永続化 $this->memberRepository->save(); } } 具象 3. Unit of Workパターン サンプル リポジトリ使ってもいい (こうすればInterfaceでも注入できるはず)
// ServiceProvider... public function register(): void { $this->app->bind( MemberRepositoryInterface::class, fn (Application $app) => $app["em"]->getRepository(\App\Entities\Member::class) ); }
2. ActiveRecord vs DataMapper use App\Entities\Member; interface UowServiceInterface { public function createMembers(Member $entity): void; public function updateMembers(Member $entity): void; public function deleteMembers(Member $entity): void; public function commit(): void; } Interface class UowService implements UowServiceInterface { public function __construct( private readonly MemberRepositoryInterface $memberRepository ) { } public function createMembers(Member $entity): void { $this->memberRepository->add($entity); } public function updateMembers(Member $entity): void { $this->memberRepository->update($entity); } public function deleteMembers(Member $entity): void { $this->memberRepository->remove($entity); } public function commit(): void { // 永続化 $this->memberRepository->save(); } } 具象 3. Unit of Workパターン サンプル リポジトリに処理を委譲できる
2. ActiveRecord vs DataMapper Unit of Workインターフェイス ● UnitOfWorkInterface 4. Unit of Workパターンが解決すること namespace App\Example\UseCase; interface UnitOfWorkInterface { public function markDirty($entity); // Entityを受け取るように public function markNew($entity); public function markDeleted($entity); public function commit(); public function rollback(); } 3. Unit of Workパターン サンプル
2. ActiveRecord vs DataMapper Unit of Workインターフェイス ● UnitOfWorkInterface 4. Unit of Workパターンが解決すること namespace App\Example\UseCase; interface UnitOfWorkInterface { public function markDirty($entity); // Entityを受け取るように public function markNew($entity); public function markDeleted($entity); public function commit(); public function rollback(); } 3. Unit of Workパターン サンプル
2. ActiveRecord vs DataMapper Unit of Work具象 ● UnitOfWork 4. Unit of Workパターンが解決すること namespace App\Example\Service; use App\Example\UseCase\UnitOfWorkInterface; use Doctrine\ORM\EntityManagerInterface; class UnitOfWork implements UnitOfWorkInterface { public function __construct( private readonly EntityManagerInterface $entityManager ) { } public function markDirty($entity) { // EntityManagerはEntityの変更を自動的に監視しているから、 // persistする必要はないけども $this->entityManager->persist($entity); } 3. Unit of Workパターン サンプル public function markNew($entity) { $this->entityManager->persist($entity); } public function markDeleted($entity) { $this->entityManager->remove($entity); } public function commit() { $this->entityManager->flush(); } public function rollback() { $this->entityManager->rollback(); } }
2. ActiveRecord vs DataMapper コマンド処理インターフェイス ● InvoiceCommandInterface 4. Unit of Workパターンが解決すること namespace App\Example\UseCase\Command; use App\Example\Entities\Invoice; use App\Example\UseCase\UnitOfWorkInterface; interface InvoiceCommandInterface { public function execute(Invoice $invoice, UnitOfWorkInterface $unitOfWork); } 3. Unit of Workパターン サンプル
2. ActiveRecord vs DataMapper コマンド処理インターフェイス ● InvoiceCommandInterface 4. Unit of Workパターンが解決すること namespace App\Example\UseCase\Command; use App\Example\Entities\Invoice; use App\Example\UseCase\UnitOfWorkInterface; interface InvoiceCommandInterface { public function execute(Invoice $invoice, UnitOfWorkInterface $unitOfWork); } 3. Unit of Workパターン サンプル UnitOfWorkInterfaceを利用して Invoiceに対する操作を行う何か
2. ActiveRecord vs DataMapper 長期の顧客に対して割引を適用するコマンド ● DiscountForLoyalCustomerCommand
4. Unit of Workパターンが解決すること namespace App\Example\UseCase\Command; use App\Example\Entities\Customer; use App\Example\Entities\Invoice; use App\Example\UseCase\Command\InvoiceCommandInterface; use App\Example\UseCase\UnitOfWorkInterface; /** * 長期の顧客に対して割引を適用する */ class DiscountForLoyalCustomerCommand implements InvoiceCommandInterface { public function execute(Invoice $invoice, UnitOfWorkInterface $unitOfWork) { if ($this->isLoyalCustomer($invoice->getCustomer())) { $invoice->applyDiscount(10); // 10%の割引を適用する $unitOfWork->markDirty($invoice); } } 3. Unit of Workパターン サンプル private function isLoyalCustomer(Customer $customer): bool { // 顧客がロイヤル(長期)顧客であるかの判定ロジックを実装 return true; } }
2. ActiveRecord vs DataMapper 長期の顧客に対して割引を適用するコマンド ● DiscountForLoyalCustomerCommand
4. Unit of Workパターンが解決すること namespace App\Example\UseCase\Command; use App\Example\Entities\Customer; use App\Example\Entities\Invoice; use App\Example\UseCase\Command\InvoiceCommandInterface; use App\Example\UseCase\UnitOfWorkInterface; /** * 長期の顧客に対して割引を適用する */ class DiscountForLoyalCustomerCommand implements InvoiceCommandInterface { public function execute(Invoice $invoice, UnitOfWorkInterface $unitOfWork) { if ($this->isLoyalCustomer($invoice->getCustomer())) { $invoice->applyDiscount(10); // 10%の割引を適用する $unitOfWork->markDirty($invoice); } } 3. Unit of Workパターン サンプル private function isLoyalCustomer(Customer $customer): bool { // 顧客がロイヤル(長期)顧客であるかの判定ロジックを実装 return true; } } Entityの変更を監視
2. ActiveRecord vs DataMapper 一連処理インターフェイス ● InvoiceCommandProcessorInterface
4. Unit of Workパターンが解決すること namespace App\Example\UseCase; use App\Example\Entities\Invoice; use App\Example\UseCase\Command\InvoiceCommandInterface; interface InvoiceCommandProcessorInterface { /** * @param Invoice $invoice * @param InvoiceCommandInterface[] $commands */ public function runCommands(Invoice $invoice, array $commands); } 3. Unit of Workパターン サンプル
2. ActiveRecord vs DataMapper 一連処理インターフェイス ● InvoiceCommandProcessorInterface
4. Unit of Workパターンが解決すること namespace App\Example\UseCase; use App\Example\Entities\Invoice; use App\Example\UseCase\Command\InvoiceCommandInterface; interface InvoiceCommandProcessorInterface { /** * @param Invoice $invoice * @param InvoiceCommandInterface[] $commands */ public function runCommands(Invoice $invoice, array $commands); } 3. Unit of Workパターン サンプル Invoiceに対する操作(コマンド)を持つ ものたちを受け取り、順にそのコマン ドを実行させる何か
2. ActiveRecord vs DataMapper ユースケース ● InvoiceUsecase 4. Unit of Workパターンが解決すること namespace App\Example\UseCase; use App\Example\Entities\Invoice; use App\Example\UseCase\Command\DiscountForLoyalCustomerCommand; use App\Example\UseCase\Command\LateInvoiceAlertCommand; class InvoiceUseCase { public function __construct( private readonly InvoiceCommandProcessorInterface $invoiceCommandProcessor ) { } public function handle(Invoice $invoice) { $commands = [ new DiscountForLoyalCustomerCommand(), new LateInvoiceAlertCommand() ]; $this->invoiceCommandProcessor->runCommands($invoice, $commands); } } 3. Unit of Workパターン サンプル
2. ActiveRecord vs DataMapper ユースケース ● InvoiceUsecase 4. Unit of Workパターンが解決すること namespace App\Example\UseCase; use App\Example\Entities\Invoice; use App\Example\UseCase\Command\DiscountForLoyalCustomerCommand; use App\Example\UseCase\Command\LateInvoiceAlertCommand; class InvoiceUseCase { public function __construct( private readonly InvoiceCommandProcessorInterface $invoiceCommandProcessor ) { } public function handle(Invoice $invoice) { $commands = [ new DiscountForLoyalCustomerCommand(), new LateInvoiceAlertCommand() ]; $this->invoiceCommandProcessor->runCommands($invoice, $commands); } } 3. Unit of Workパターン サンプル Invoiceに対する処理が増えても、コマ ンド処理インターフェイスを満たすも のを渡せばいいだけ
2. ActiveRecord vs DataMapper ユースケース ● InvoiceUsecase 4. Unit of Workパターンが解決すること namespace App\Example\UseCase; use App\Example\Entities\Invoice; use App\Example\UseCase\Command\DiscountForLoyalCustomerCommand; use App\Example\UseCase\Command\LateInvoiceAlertCommand; class InvoiceUseCase { public function __construct( private readonly InvoiceCommandProcessorInterface $invoiceCommandProcessor ) { } public function handle(Invoice $invoice) { $commands = [ new DiscountForLoyalCustomerCommand(), new LateInvoiceAlertCommand() ]; $this->invoiceCommandProcessor->runCommands($invoice, $commands); } } 3. Unit of Workパターン サンプル 拡張に対して開かれている (Open-Closed Principle: 開放閉鎖の 原則)
2. ActiveRecord vs DataMapper ユースケース ● InvoiceUsecase 4. Unit of Workパターンが解決すること namespace App\Example\UseCase; use App\Example\Entities\Invoice; use App\Example\UseCase\Command\DiscountForLoyalCustomerCommand; use App\Example\UseCase\Command\LateInvoiceAlertCommand; class InvoiceUseCase { public function __construct( private readonly InvoiceCommandProcessorInterface $invoiceCommandProcessor ) { } public function handle(Invoice $invoice) { $commands = [ new DiscountForLoyalCustomerCommand(), new LateInvoiceAlertCommand() ]; $this->invoiceCommandProcessor->runCommands($invoice, $commands); } } 3. Unit of Workパターン サンプル Invoiceに対する処理を色々やってく れているんだろうなぁという抽象具 合。 気づけばいい感じに永続化されてい る。
2. ActiveRecord vs DataMapper Unit of Workパターンが解決すること ● データベースとのやり取りを効率的に行ってくれる ● EntityはDBのことを知らないのでビジネスロジックに集中しやすい ● サービス層などでUnit of Workを使って永続化処理を隠蔽 4. 所感
● P of EAA: Unit of Work ○ https://martinfowler.com/eaaCatalog/unitOfWork.html
● Unit of Work パターンと永続性の無視 | Microsoft Learn ○ https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2009/june/the-unit-of-work -pattern-and-persistence-ignorance