--- title: MongoDB互換のFerretDBにSpring Boot + Testcontainersでアクセスするメモ tags: ["Spring Boot", "FerretDB", "MongoDB", "Testcontainers", "DocumentDB", "Java"] categories: ["Programming", "Java", "org", "springframework", "data", "mongodb"] date: 2025-07-18T03:43:12Z updated: 2025-07-18T03:49:24Z --- [FerretDB](https://docs.ferretdb.io/)はMongoDB互換のOSSデータベースで、[DocumentDB](https://github.com/microsoft/documentdb)拡張を使ったPostgreSQLをバックエンドに使用します。 MongoDBがSSPL(Server Side Public License)になったのに対し、FerretDBはApache 2.0でライセンスです(DocumentDB拡張はMITライセンス)。 > [!TIP] > ちなみにTanzu for PostgreSQLもFerretDBをサポートしています。 > https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres/17-5/tnz-postgres/index.html 本稿では、Spring Boot + Spring Data MongoDBを使ってFerretDBにアクセスする方法をメモします。 といってもアプリケーション観点ではMongoDBと同じで、Testcontainersを使った設定だけが変わります。 ### プロジェクトの雛形の作成 Spring Initializrを使って、Spring Bootプロジェクトの雛形を作成します。 ```bash curl -s https://start.spring.io/starter.tgz \ -d artifactId=demo-ferretdb \ -d name=demo-ferretdb \ -d baseDir=demo-ferretdb \ -d packageName=com.example \ -d dependencies=web,data-mongodb,actuator,configuration-processor,prometheus,native,testcontainers \ -d type=maven-project \ -d applicationName=DemoFerretApplication | tar -xzvf - cd demo-ferretdb ``` ### サンプルアプリの作成 非常にシンプルな、メッセージを保存・取得するアプリケーションを作成します。 ```java cat < src/main/java/com/example/Message.java package com.example; public record Message(String id, String text) { } EOF ``` ```java cat < src/main/java/com/example/HelloController.java package com.example; import java.util.List; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { private final MongoTemplate mongoTemplate; public HelloController(MongoTemplate mongoTemplate) { this.mongoTemplate = mongoTemplate; } @PostMapping(path = "/messages") public Message postMessage(@RequestBody String text) { return mongoTemplate.save(new Message(null, text)); } @GetMapping(path = "/messages") public List getMessages() { return mongoTemplate.findAll(Message.class); } } EOF ``` Spring InitializrでTestcontainersを追加しているので、次のファイルがプロジェクトに含まれます。 ```java package com.example; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.context.annotation.Bean; import org.testcontainers.containers.MongoDBContainer; import org.testcontainers.utility.DockerImageName; @TestConfiguration(proxyBeanMethods = false) class TestcontainersConfiguration { @Bean @ServiceConnection MongoDBContainer mongoDbContainer() { return new MongoDBContainer(DockerImageName.parse("mongo:latest")); } } ``` Testcontainersを使ってアプリを起動しましょう。`src/test/java/com/example/TestDemoFerretApplication.java`を実行するか、次のコマンドを実行します: ```bash ./mvnw spring-boot:test-run ``` アプリケーションおよびMongoDBが起動したら、次のようにメッセージをPOST & GETしてみましょう。 ```bash $ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello MongoDB\!" {"id":"6879a6f3ba99e4ec5c9419fd","text":"Hello MongoDB!"} $ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello FerretDB\!" {"id":"6879a6f8ba99e4ec5c9419fe","text":"Hello FerretDB!"} $ curl -s http://localhost:8080/messages | jq . [ { "id": "6879a6f3ba99e4ec5c9419fd", "text": "Hello MongoDB!" }, { "id": "6879a6f8ba99e4ec5c9419fe", "text": "Hello FerretDB!" } ] ``` この動作確認に相当するテストコードは次のようになります。 ```java package com.example; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestClient; import static org.assertj.core.api.Assertions.assertThat; @Import(TestcontainersConfiguration.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class DemoFerretApplicationTests { RestClient restClient; @BeforeEach void setUp(@LocalServerPort int port, @Autowired RestClient.Builder restClientBuilder) { this.restClient = restClientBuilder.defaultStatusHandler(statusCode -> true, (req, res) -> { /* NO-OP */}).baseUrl("http://localhost:" + port).build(); } @Test void contextLoads() { { ResponseEntity res = this.restClient.post() .uri("/messages") .contentType(MediaType.TEXT_PLAIN) .body("Hello MongoDB!") .retrieve() .toEntity(Message.class); assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); Message message = res.getBody(); assertThat(message).isNotNull(); assertThat(message.text()).isEqualTo("Hello MongoDB!"); assertThat(message.id()).isNotNull(); } { ResponseEntity res = this.restClient.post() .uri("/messages") .contentType(MediaType.TEXT_PLAIN) .body("Hello FerretDB!") .retrieve() .toEntity(Message.class); assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); Message message = res.getBody(); assertThat(message).isNotNull(); assertThat(message.text()).isEqualTo("Hello FerretDB!"); assertThat(message.id()).isNotNull(); } { ResponseEntity> res = this.restClient.get() .uri("/messages") .retrieve() .toEntity(new ParameterizedTypeReference<>() { }); assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); List messages = res.getBody(); assertThat(messages).isNotNull(); assertThat(messages).hasSize(2); assertThat(messages).map(Message::id).allSatisfy(id -> assertThat(id).isNotNull()); assertThat(messages).map(Message::text).containsExactly("Hello MongoDB!", "Hello FerretDB!"); } } } ``` テストでもTestcontainersでMongoDBが起動します。次のコマンドでテストを実行できます: ```bash ./mvnw clean test ``` ### FerretDBへの入れ替え MongoDBからFerretDBに入れ替えます。アプリケーションのコードはそのままでよく、Testcontainersの設定だけを変更します。 FerretDBのコンテナイメージは`MongoDBContainer`との互換性がなかったので、`GenericContainer`を使います。 そのため、ServiceConnectionは使わず、`DynamicPropertyRegistrar`を使ってMongoDBの接続情報を動的に登録します。 FerretDBはデフォルトで認証が有効になっているので、ユーザー名とパスワードも含めて`spring.data.mongodb.uri`を設定します。 `TestcontainersConfiguration`を次のように変更します: ```java cat < src/test/java/com/example/TestcontainersConfiguration.java package com.example; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.test.context.DynamicPropertyRegistrar; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; import org.testcontainers.utility.DockerImageName; @TestConfiguration(proxyBeanMethods = false) class TestcontainersConfiguration { @Bean GenericContainer ferretDbContainer() { return new GenericContainer<>(DockerImageName.parse("ghcr.io/ferretdb/ferretdb-eval:2")) .withExposedPorts(27017, 5432) .withEnv("POSTGRES_USER", "user") .withEnv("POSTGRES_PASSWORD", "password") .withEnv("FERRETDB_TELEMETRY", "false") .waitingFor(new HostPortWaitStrategy().forPorts(27017, 5432)); } @Bean DynamicPropertyRegistrar dynamicPropertyRegistrar(GenericContainer ferretDbContainer) { return registry -> registry.add("spring.data.mongodb.uri", () -> "mongodb://user:password@%s:%d/test" .formatted(ferretDbContainer.getHost(), ferretDbContainer.getMappedPort(27017))); } } EOF ``` `TestcontainersConfiguration`変更後も同じテストが通るはずです: ```bash ./mvnw clean test ``` `src/test/java/com/example/TestDemoFerretApplication.java`を再実行するか、次のコマンドを再実行します: ```bash ./mvnw spring-boot:test-run ``` 先ほどと同じように、メッセージをPOST & GETしてみましょう。 ```bash $ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello MongoDB\!" {"id":"6879af654c503243968ecba0","text":"Hello MongoDB!"} $ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello FerretDB\!" {"id":"6879af6a4c503243968ecba1","text":"Hello FerretDB!"} $ curl -s http://localhost:8080/messages | jq . [ { "id": "6879af654c503243968ecba0", "text": "Hello MongoDB!" }, { "id": "6879af6a4c503243968ecba1", "text": "Hello FerretDB!" } ] ``` 特に問題なくMongoDBからFerretDBに切り替わることが確認できました。 ### スタンドアローンで実行 Testcontainersを使わずに、アプリケーションをスタンドアローンで実行してみます。 FerretDBは次の`docker run`コマンドで起動します: ```bash docker run --rm --name ferretdb -p 27017:27017 -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e FERRETDB_TELEMETRY=false ghcr.io/ferretdb/ferretdb-eval:2 ``` 次のコマンドで実行可能なjarファイルを作成します: ```bash ./mvnw clean package ``` 実行時に`spring.data.mongodb.uri`を指定して、FerretDBに接続します。 ```bash java -jar target/demo-ferretdb-0.0.1-SNAPSHOT.jar --spring.data.mongodb.uri=mongodb://user:password@localhost:27017/test ``` 先ほどと同じように、メッセージをPOST & GETできるでしょう。 動作確認したソースコードは[こちら](https://github.com/making/demo-ferretdb)です。 --- FerretDBをSpring Boot + Spring Data MongoDB + Testcontainersで使う方法を紹介しました。 SSPLなMongoDBの代替としてFerretDBを使うことで、ライセンスの問題を回避しつつ、MongoDB互換の機能を利用できそうです。 詳細な互換性は[ドキュメント](https://docs.ferretdb.io/migration/compatibility/)を参照してください。