JPAに慣れていると、MongoDBを使おうとしたときに「何から始めればいいの?」と戸惑いますよね。アノテーションもリポジトリも見た目は似ているけど、概念が微妙に違う。

この記事では Spring Boot 3.x(Java 17以上) を対象に、MongoDBを接続して基本的なCRUD・クエリ・集計まで動かせるようになることを目標にします。

MongoDBとRDBMSの概念対比

まず頭を切り替えましょう。MongoDBはテーブルではなく Collection にデータを格納し、行の代わりに Document (JSON形式)を扱います。

RDBMSMongoDB
TableCollection
RowDocument
Primary Key_id
Schemaスキーマレス(柔軟)

スキーマレスというのは、同じコレクション内のドキュメントがバラバラなフィールドを持てるということです。スキーマが頻繁に変わるデータや、深いネスト構造を持つデータに向いています。逆に、複雑なトランザクションや厳格な整合性が必要な場面はRDBMSの方が安心です。

ローカル環境をDocker Composeで準備する

MongoDBをローカルにインストールしなくても、Docker Composeで簡単に起動できます。

# docker-compose.yml
services:
  mongo:
    image: mongo:latest  # ローカル開発用。本番では特定バージョン(例: mongo:7.0)を指定すること
    ports:
      - "27017:27017"
    volumes:
      - mongo_data:/data/db

volumes:
  mongo_data:

docker compose up -d で起動するだけです。認証なしのシンプルな設定ですが、ローカル開発では十分です。Dockerを使った環境構築については Spring BootをDockerでコンテナ化する方法 も参考にしてください。

依存関係の追加と接続設定

build.gradle に以下を追加します。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
}

application.properties の接続設定はこれだけです。

spring.data.mongodb.uri=mongodb://localhost:27017/mydb

Spring Bootの自動設定が動いて、MongoClient の生成からコネクションプールの設定まで全部やってくれます。JPAのときと同じ感覚ですね。

@DocumentでエンティティクラスとMongoDBを紐付ける

@Entity の代わりに @Document を使います。

@Document(collection = "products")
public class Product {

    @Id
    private String id;  // ObjectIdはString型で受け取れる

    private String name;

    private int price;

    @Field("category_name")
    private String categoryName;  // DBのフィールド名をカスタマイズ

    // コンストラクタ・getters・setters 省略
}

ネストしたオブジェクトは別のクラスをそのままフィールドに持てば、埋め込みドキュメントとして自動的に扱われます。@Embedded のようなアノテーションは不要です。

MongoRepositoryでCRUD操作を実装する

JpaRepository と同じように、MongoRepository を継承するだけで基本的なCRUDが使えます。

public interface ProductRepository extends MongoRepository<Product, String> {
    // save / findById / findAll / deleteByIdはそのまま使える
}

サービス層での使い方もJPAとほぼ同じです。

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Product create(Product product) {
        return productRepository.save(product);
    }

    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }

    public void delete(String id) {
        productRepository.deleteById(id);
    }
}

REST APIとの組み合わせ方は Spring BootでREST APIを作るチュートリアル が参考になります。

クエリメソッドで条件検索を書く

Spring Data JPAのクエリメソッドと 全く同じ命名規則 が使えます。

public interface ProductRepository extends MongoRepository<Product, String> {

    List<Product> findByName(String name);

    List<Product> findByPriceLessThan(int price);

    List<Product> findByNameAndCategoryName(String name, String categoryName);

    @Query("{ 'price': { $gte: ?0, $lte: ?1 } }")
    List<Product> findByPriceRange(int min, int max);
}

JPAのクエリメソッド命名規則については Spring Data JPAのクエリメソッド解説 も合わせて読んでみてください。

MongoTemplateで動的クエリを書く

クエリメソッドは静的な条件には便利ですが、「条件がある場合だけフィルタする」といった動的クエリは MongoTemplate の方が書きやすいです。

@Service
public class ProductSearchService {

    private final MongoTemplate mongoTemplate;

    public ProductSearchService(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    public List<Product> searchProducts(String name, Integer maxPrice) {
        Query query = new Query();

        if (name != null) {
            query.addCriteria(Criteria.where("name").regex(name, "i"));
        }
        if (maxPrice != null) {
            query.addCriteria(Criteria.where("price").lte(maxPrice));
        }

        return mongoTemplate.find(query, Product.class);
    }
}

Criteria.where("field").is(value) で条件を組み立て、Query オブジェクトに追加していくスタイルです。なお addCriteria()異なるフィールドに対して 呼ぶ必要があります。同じフィールドに2回呼ぶと InvalidMongoDbApiUsageException が発生するので注意してください。

Aggregation Pipelineで集計する

MongoDBのAggregation Pipelineは、Spring Data APIでも表現できます。まず集計結果を受け取るクラスを定義します。

public class CategorySummary {

    @Id
    private String id;      // $groupのキー(categoryName)が入る

    private int productCount;

    private long totalPrice;

    // getters省略
}

次に、パイプラインを組み立てて実行します。

public List<CategorySummary> aggregateByCategory() {
    Aggregation aggregation = Aggregation.newAggregation(
        Aggregation.match(Criteria.where("price").gt(0)),          // $match
        Aggregation.group("categoryName")
            .count().as("productCount")
            .sum("price").as("totalPrice"),                         // $group
        Aggregation.sort(Sort.Direction.DESC, "productCount")       // $sort
    );

    AggregationResults<CategorySummary> results =
        mongoTemplate.aggregate(aggregation, "products", CategorySummary.class);

    return results.getMappedResults();
}

$match で絞り込み、$group で集計、$sort で並び替えというMongoDBのパイプライン構造がそのままJavaコードで表現できます。AggregationResults#getMappedResults()CategorySummary のリストとして受け取れます。

RDBMSとMongoDBの使い分け

どちらを選ぶかは、データの性質と要件次第です。

MongoDBが向く場面

  • スキーマが頻繁に変化するデータ(設定情報、ユーザー属性など)
  • ネスト構造が自然なデータ(注文+明細、記事+コメントなど)
  • 水平スケーリングが前提の大規模データ

RDBMSが向く場面

  • 複雑なトランザクションや厳格な整合性が必要
  • 正規化されたデータで複雑なJOINが多い
  • 既存のJPAベースのコードが大量にある

一つのシステムでRDBMSとMongoDBを用途に応じて使い分ける「ポリグロット構成」も現実的な選択肢です。RDB中心のアーキテクチャとの比較は Spring BootのMyBatis vs JPA比較記事 も参考にしてみてください。

まとめ

Spring Data MongoDBはJPAと似た使い心地になるよう設計されているので、JPA経験者であれば比較的すんなり入れます。まとめると以下の5点を押さえるだけで、Spring BootアプリにMongoDBを組み込めます。

  • @Document@Id でエンティティ定義
  • MongoRepository で基本CRUD
  • クエリメソッドはJPAと同じ命名規則
  • 動的クエリは MongoTemplateCriteria
  • 集計は Aggregation.newAggregation()

テストについては TestcontainersでSpring Bootの統合テストを書く方法 でMongoDBコンテナを使った統合テストも書けるので、合わせて確認してみてください。