概要
MongoDBが初めてだったRDBMS開発者の小さなヘルパーが、実際のプロジェクトの要求に応じて成長し、1.0.0でDriver-first MongoDBコンビニエンスレイヤーになるまでの歴史と設計基準を説明します。
起源の物語
私は約4年間RDBMSを中心に開発しており、運用環境もほとんどAWS RDSに慣れていました。新しい個人プロジェクトを始める際に初めてMongoDBを選びましたが、学ぶにつれて単なるスキーマが柔軟なデータベースではなく、私の開発生産性を大きく変えるツールだと感じました。
特にMongoDB Atlasは私にとって新世界に近いものでした。従来は別のシステムを付けなければならないと思っていた専門検索とベクター検索を一つのデータプラットフォーム内で使用でき、バックアップ・モニタリング・拡張などの運用作業もAWS RDSだけを使用していた私の基準では非常に便利でした。より専門的な規模と機能が必要であれば、検索エンジンやベクターデータベースを別サービスとして分離することもできますが、一つのプロジェクトを迅速に作成し運用する段階ではAtlas一つで解決できる範囲が非常に魅力的でした。
問題はMongoDBが初めての私にとって動的クエリを書くのが非常に遅かったことです。最初はrepositoryのfindBy...形式だけで十分でしたが、検索条件が複雑になるとQuery、Criteriaと演算子表現を直接組み立てる必要がありました。慣れない状態で繰り返し条件を組み立てるのはタイプミスが起こりやすく、同じ形式のコードを繰り返し書くことも開発速度を大きく落としました。
照会用簡易DSLから始まりました
そこで最初に作ったのはCRUDのRだけを簡単に作るための小さなクラスでした。現在ReactiveMongoDslという名前で残っているクラスの出発点です。当時はaggregation、pipeline、lookupのような機能は全くなく、Spring Data ReactiveMongoTemplateのQuery、Criteria、pagingをもう少し早く組み立てる程度でした。残りの作業は基本ReactiveMongoTemplateやMongoDB Driverを直接使用しても十分だと思っていました。
しかし、一緒に働いていた開発者の考えは異なりました。彼もまたMongoDBが初めてで、upsertやbulkのような作業が必要な時には、私が作ったDSLにその機能があるかどうかをまず尋ねてきました。基本APIを直接使用するように言うこともできましたが、私が初めてMongoDBを学んだときに感じた困難を彼も同じように経験するだろうと思いました。そこで、必要な機能が生まれるたびにReactiveMongoDslに一つずつ追加し始めました。
最初は約600行で、1,000行になった時もわざわざ複数のクラスに分ける必要はないと思っていました。2,000行くらいになった時は少し悩みましたが、分離のための構造が逆に大きくなる感じもあり、何より正直その作業が面倒でした。5,000行を超えた時はもはや先延ばしできず、分離可能な機能の一部を外に移しましたが、すでにコアの流れを細かく再分けするには負担が大きい状態でした。コアクラスが大きい理由は、最初から巨大なDSLを設計したからではなく、実際のプロジェクトで必要だった機能を同じエントリーポイントに積み重ねてきた歴史に近いです。
こうして照会、集計、lookup、原子更新、bulk、履歴、Atlas SearchとVector Searchまで追加した結果、いつの間にかJavaプロジェクトでMongoDBを使用する際に私が頻繁に必要とする機能のほとんどが含まれていました。一つのプロジェクト内の内部ヘルパーとしてだけでなく、複数のプロジェクトで同じ方法で使用できるように分離し、それがreactive-mongo-dslの誕生過程です。
SpringヘルパーからDriver-firstライブラリへ
出発点はReactiveMongoTemplateをより便利に使うためのヘルパーでしたが、機能が増え、複数のプロジェクトで再利用し始めると、特定のフレームワークの実行モデル自体がライブラリの境界になるのは適切ではないと判断しました。1.0.0のコアはMongoExecutionContextを実行契約としてMongoDB Reactive Streams Driverを直接使用します。
この変化はSpring Data MongoDBを排除するためのものではありません。Springアプリケーションでは、ReactiveMongoTemplateのコレクション名付け、MongoConverter、カスタム変換、リアクティブ監査をMongoExecutionContextアダプターで接続できます。コアはフレームワークに独立しておき、必要なアプリケーションだけが既存のSpring設定を引き続き使用する構造です。
同時にMongoDB Driverがすでに提供している機能をDSL内で再実装しない方向もより明確になりました。一般的な集計はBsonステージをそのまま受け取ることができ、Search/VectorにはSearchOperator、VectorSearchQuery、driverOptions(...)、stage(Bson)のようなエスケープハッチを設けています。Driverが新しい機能を先に提供しても、DSLの次の配布を待たずに使用できるようにする選択です。
開発哲学
MongoDBを隠しません
MongoDBの機能を他のデータベース概念に変えるORMを作りたかったわけではありません。MongoDBのクエリ、集計、トランザクション、Atlas SearchとVector Searchをより短く、発見しやすい流れでつなげます。
実際に必要だった機能だけを追加します
機能リストを最初に設計してから実装したライブラリではありません。運用プロジェクトでの参照が必要だったため、参照機能を追加し、upsertとbulkが必要だったため、その機能を追加し、検索機能を使用するためにAtlas SearchとVector Searchを追加しました。
構造的完全性よりも生産性を優先します
コアクラスが大きいことを理想的な構造とは主張しません。チームが慣れていないMongoDBの作業を同じ方法で迅速に処理することが、開始当初はより重要な問題であり、今でも抽象化自体よりも実際の使用フローを優先します。
Driverがすでに行っていることを再構築しません
MongoDB Java Driverにtyped builderがある場合は、可能な限りそのタイプをそのまま使用します。DSLが価値を持って繰り返しの組み合わせを減らせる場合は、便利なAPIとして提供しますが、Driver APIを名前を変えて再度複製することはありません。
基本APIに降りることができる必要があります
DSLがすべての状況を代替するとは考えていません。Bson、Driver filter/sort/operator/options、publisher customizerのようなエスケープハッチを設け、必要に応じてMongoDB Driverを直接使用できます。
現在の文書で扱う4つのフロー
一般的なMongoクエリ
このライブラリが始まったフローです。execute* → fields(...) → end() → find/findAll/count/delete/exists/atomicUpdateの順序で条件と実行を構成します。
Driverネイティブの集約
パイプラインの最初のステージから直接制御する必要がある場合や、Driverが新たに提供した集約機能をそのまま使用する場合は、aggregation().stage(Bson)のフローを使用します。$score、$scoreFusionのように、Driverがすでに提供しているステージをDSLが再実装せずに渡すことができます。
Atlas Search
別の検索サービスを先に導入せずにAtlas Searchをプロジェクトに適用しながら追加したフローです。search(index)の後にtext、compound、score、highlight、sequence tokenなどを構成します。
Vector Search
MongoDB内でembedding検索まで接続するために追加したフローです。query vectorまたはautomated embedding、ANN/ENN、pre/post filterとnested/array embeddingオプションを構成します。
1.0.0の実行モデル
一般的なクエリとSearch/Vectorを分離したのは、MongoDBの実際のパイプライン制約を隠さないためです。$searchと$vectorSearchは最初のステージに制約があり、最初のステージ全体を呼び出し元が制御する必要がある機能はaggregation()に降りることができます。
実際のプロジェクトで使用する範囲
• 条件のないペアをnullで渡して画面ごとの動的検索条件を組み立てます。
• PageStreamでdataとtotalCountを分離し、大量処理フローをreactive publisher状態で維持します。
• atomicUpdate().upsertOne().document().setOnInsert(...)で重複実行に安全な作業を作成します。
• IDまたは業務キーに基づくbulk upsertで収集データと外部連携データを保存します。
• executeLookupAndCountでlookup結果と全体件数を1つのパイプラインで返します。
• Atlas Searchのsequence tokenとVector Searchのautomated embedding queryを使用します。
• Driverが提供する新しい集約ステージは、aggregation().stage(Bson)またはSearch/Vectorのstage(Bson)に直接接続します。
• DSL操作間で原子性が必要な場合は、getTxJob(...)でClientSessionトランザクション範囲を明示します。