MongoDB互換のFerretDBにSpring Boot + Testcontainersでアクセスするメモ

FerretDBはMongoDB互換のOSSデータベースで、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プロジェクトの雛形を作成します。

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 

サンプルアプリの作成

非常にシンプルな、メッセージを保存・取得するアプリケーションを作成します。

cat <<EOF > src/main/java/com/example/Message.java
package com.example;

public record Message(String id, String text) {
}
EOF
cat <<EOF > 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<Message> getMessages() {
		return mongoTemplate.findAll(Message.class);
	}

}
EOF

Spring InitializrでTestcontainersを追加しているので、次のファイルがプロジェクトに含まれます。

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を実行するか、次のコマンドを実行します:

./mvnw spring-boot:test-run

アプリケーションおよびMongoDBが起動したら、次のようにメッセージをPOST & GETしてみましょう。

$ 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!"
  }
]

この動作確認に相当するテストコードは次のようになります。

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<Message> 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<Message> 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<List<Message>> res = this.restClient.get()
				.uri("/messages")
				.retrieve()
				.toEntity(new ParameterizedTypeReference<>() {
				});
			assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
			List<Message> 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が起動します。次のコマンドでテストを実行できます:

./mvnw clean test

FerretDBへの入れ替え

MongoDBからFerretDBに入れ替えます。アプリケーションのコードはそのままでよく、Testcontainersの設定だけを変更します。 FerretDBのコンテナイメージはMongoDBContainerとの互換性がなかったので、GenericContainerを使います。 そのため、ServiceConnectionは使わず、DynamicPropertyRegistrarを使ってMongoDBの接続情報を動的に登録します。 FerretDBはデフォルトで認証が有効になっているので、ユーザー名とパスワードも含めてspring.data.mongodb.uriを設定します。

TestcontainersConfigurationを次のように変更します:

cat <<EOF > 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変更後も同じテストが通るはずです:

./mvnw clean test

src/test/java/com/example/TestDemoFerretApplication.javaを再実行するか、次のコマンドを再実行します:

./mvnw spring-boot:test-run

先ほどと同じように、メッセージをPOST & GETしてみましょう。

$ 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コマンドで起動します:

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ファイルを作成します:

./mvnw clean package

実行時にspring.data.mongodb.uriを指定して、FerretDBに接続します。

java -jar target/demo-ferretdb-0.0.1-SNAPSHOT.jar --spring.data.mongodb.uri=mongodb://user:password@localhost:27017/test

先ほどと同じように、メッセージをPOST & GETできるでしょう。

動作確認したソースコードはこちらです。


FerretDBをSpring Boot + Spring Data MongoDB + Testcontainersで使う方法を紹介しました。 SSPLなMongoDBの代替としてFerretDBを使うことで、ライセンスの問題を回避しつつ、MongoDB互換の機能を利用できそうです。

詳細な互換性はドキュメントを参照してください。