Apex における Repository パターンの試行錯誤と内蔵化

Apex Stem ドキュメント
ApexRepository PatternArchitectureApexEloquent
Apex で Repository パターンを愚直に書くと何が起きるか、Salesforce 公式の Selector Pattern との関係、そして ApexEloquent が IEloquent として「内蔵 Repository」を提供する理由を解説します。

Repository パターンに関する情報はネット上に豊富にあり、実装例も多くの言語で見つかります。しかし、これらをそのまま Apex (Salesforce) に持ち込もうとすると、ほかの言語にはない事情にぶつかります。

ここでは、Apex で実際に試した Repository クラスの構成パターンと、そこから ApexEloquent が 「内蔵 Repository」 という形に行き着いた経緯を整理します。詳しい設計哲学は Query Delegation Pattern と合わせて読むと、なぜこの形に着地したのかが繋がります。

パターン 1: ユースケース別 Repository

ユースケースごとに個別の Repository クラスを定義していくスタイルです。ユースケースが増えるたびに 1 つずつ Repository を切り出していけば、各処理の意図はコードに残ります。

一方で、インターフェースと実装クラスが量産される という重い課題があります。Apex は namespace を持たないため、クラスを大量に作るとすぐに名前衝突や命名の枯渇に陥ります。これは Apex 固有の設計上の制約として軽視できません。

パターン 2: オブジェクト別 Repository (Selector Pattern)

クラス数を抑える方針として、SObject 単位で Repository (Apex の文脈では Selector) を定義するスタイルがあります。Salesforce 公式の Selector Pattern がこの形に当たります。

📚 Selector Pattern 公式ガイド (Trailhead)

公式が掲げるメリット

公式ガイドは、Selector Pattern が解こうとしている問題として 3 つの「不整合」を挙げています。これらはいずれも、SOQL をビジネスロジックのあちこちに散らかしたときに実際に起こる問題で、Selector に集約する動機としては妥当です。

  • クエリの不整合: 似たような SOQL がコードのあちこちにコピペされ、ある場所だけ条件が抜けるといった不一致が起きる
  • クエリデータの不整合: SOQL で取得した SObject を引き回すと、取得していない項目に触れた呼び出し先が System.SObjectException: SObject row was retrieved via SOQL without querying the requested field で落ちる
  • セキュリティの不整合: 各実行コンテキストでのオブジェクトセキュリティチェックを散在させると見落としが起きやすい。Selector に寄せると一元管理できる

公式が触れていない、長期運用での副作用

ここからが、公式ガイドではあまり言及されない領域です。Selector Pattern を中規模以上のプロジェクトで何年か運用すると、メリットを得ながら同時に別の副作用が積み上がっていきます。Opportunity のように複数のユースケースから使われる頻用オブジェクトで、特に顕著に現れます。

  • 細かな絞り込み条件のためのメソッドが量産される (getById / getByIdAndStage / getByIdAndStageWithClosedDate ...)
  • フラグ引数によるクエリ制御 が増え、呼び出し側の意図が見えにくくなる (getById(id, includeLineItems, lockForUpdate, ...))
  • メソッドの文脈が曖昧になり、安全に修正できなくなる (修正したら、どのユースケースに影響するか追えない)
  • 「すべてのフィールドを取得する」圧力 がかかる: 未取得フィールドへのアクセスで SObjectException が出る恐怖を避けるため、Selector メソッドは「念のため全フィールドを SELECT に入れておく」方向に膨らみがち。結果として SOQL コストの増加、ガバナ制限への圧迫、依存関係の不明瞭化 (SELECT 句から「どのユースケースが何を使っているか」が読めなくなる) を招く

特に 4 つ目は、公式メリットの 2 番目 (未取得フィールド対策) を実現する手段が「Selector に集約する」だけだと、その裏返しとして自然に発生します。「未取得フィールド恐怖」を避けるための一番楽な対処が「全部 SELECT に入れる」なので、Selector が複数ユースケースから共有されるほど、SELECT 句は最大公約数として肥大化します。

「良かれと思って踏んだ道が、そのまま技術的負債への道だった」という、Selector Pattern の典型的な崩れ方です。公式の 3 つのメリットは事実ですが、それを実現する手段が「メソッドを足し続ける」「SELECT 句を膨らませる」になっている限り、長期運用ではこの副作用とセットになります。

ApexEloquent が向き合いたいのは、公式が挙げる 3 つのメリットを維持しつつ、長期運用の副作用を別の設計で回避すること です。

解決策: Built-in Repository

これらの試行錯誤を経て、ApexEloquent は Query Delegation Pattern に基づく Built-in Repository を採用しました。

設計の核は、クエリの構築をドメイン層 (Usecase) に置き、実行だけを Repository に委ねる ことです。これによって、アプリケーションごとに Selector を量産する必要がなくなり、共通の「内蔵 Repository」1 つで済みます。ApexEloquent ではそれが IEloquent (本番は Eloquent、テストは MockEloquent) として提供されます。

このパターンの利点

  • ✅ Selector クラスの過剰な量産を防げる
  • ✅ 「こういうデータが欲しい」という意図とクエリの組み立てが、ドメイン層の同じ場所に残る
  • ✅ 細かなクエリの差分はドメイン層で吸収でき、Repository の肥大化が起きない
  • ✅ アプリ側で Repository インターフェースを定義する必要がなく、テストが書きやすい

実際にこのスタイルを採用してからは、テストコードを書く際の心理的負担が大きく下がりました。生 SOQL を量産する書き味からも、Selector のメソッドを継ぎ足し続ける運用からも解放され、Usecase のそばで Scribe を組み立て、IEloquentMockEloquent に差し替えるだけで単体テストが書けます。

クエリ共通化の余地は残してある

「内蔵 Repository」にしたからといって、共通化したいクエリを置く場所が無くなるわけではありません。共通化したい Scribe の組み立てを集約したいときは、Selector 派生としての クエリ保管庫 (Vault) スタイルを併用できます。詳しくは Query Delegation Pattern の Query Reusability を参照してください。

従来パターンとの比較

観点Traditional Repository / SelectorBuilt-in Repository
クラス数ユースケースやオブジェクトごとに増える共通の 1 つ (IEloquent)
テストの煩雑さモックの準備が複雑DI で MockEloquent に差し替えるだけ
クエリの置き場所Selector に散らばるドメイン層 (Usecase) に集約
保守性インターフェースが増えて辛くなる単一インターフェースで完結
Apex 適性namespace 制約にぶつかりやすいApex の特性に合わせて設計済み

まとめ

「Query Delegation Pattern + Built-in Repository」は、単なる選択肢の 1 つというより、Apex 開発における設計の見方そのものを切り替えるアプローチ です。

生 SOQL を量産するスタイルや、DB に依存したテストの面倒さから開発者を解放し、Salesforce プラットフォーム上でも、ほかのプラットフォームと同じ感覚でクリーンなテスト駆動の開発を実践できます。結果として、技術的負債を抑え、開発スピードを保ちながら、より保守しやすいコードを残すチーム文化を育てていけます。

関連ドキュメント