Skip to content

Map MongoDB concepts

Map MongoDB concepts explicitly in the application

Section titled “Map MongoDB concepts explicitly in the application”

MongoDB concept storage is also an application-owned recipe, not Arc autodiscovery. Register concrete, stateless @WritingConverter/@ReadingConverter pairs for each concept and scalar in one authoritative MongoCustomConversions.create configuration. A bare Spring Converter bean is not this registration. Configure the application’s mapping context and converter before creating templates, repositories, or tenant certificates; Arc must not mutate an application-owned custom MongoConverter during lookup.

For a concept such as the TextValue shown in Map JPA concepts, the Kotlin pair is:

@WritingConverter
class TextWrite : Converter<TextValue, String> {
override fun convert(source: TextValue): String = source.value()
}
@ReadingConverter
class TextRead : Converter<String, TextValue> {
override fun convert(source: String): TextValue = TextValue(source)
}

The ordinary Java equivalent constructs the record explicitly:

@WritingConverter
public class TextWrite implements Converter<TextValue, String> {
@Override
public String convert(TextValue source) { return source.value(); }
}
@ReadingConverter
public class TextRead implements Converter<String, TextValue> {
@Override
public TextValue convert(String source) { return new TextValue(source); }
}

Here Converter means Spring’s org.springframework.core.convert.converter.Converter, not the JPA annotation. The executable recipes provide equivalent concrete UUID-to-UUID and Long-to-Long pairs named UuidWrite/UuidRead and LongWrite/LongRead. Do not replace them with an erased Converter<ConceptAs<?>, ?> or infer constructors reflectively. Install all six together:

val conversions = MongoCustomConversions.create { adapter ->
adapter.registerConverter(UuidWrite())
adapter.registerConverter(UuidRead())
adapter.registerConverter(TextWrite())
adapter.registerConverter(TextRead())
adapter.registerConverter(LongWrite())
adapter.registerConverter(LongRead())
}
var conversions = MongoCustomConversions.create(adapter -> {
adapter.registerConverter(new UuidWrite());
adapter.registerConverter(new UuidRead());
adapter.registerConverter(new TextWrite());
adapter.registerConverter(new TextRead());
adapter.registerConverter(new LongWrite());
adapter.registerConverter(new LongRead());
});

For programmatically owned templates, use that same configuration in both places, in this order (the complete RecipeMongoStore fixture also owns and closes its client):

var mappingContext = new MongoMappingContext();
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
mappingContext.setInitialEntitySet(Set.of(UuidRow.class, TextRow.class, LongRow.class));
mappingContext.afterPropertiesSet();
var factory = new SimpleMongoClientDatabaseFactory(client, database);
var converter = new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext);
converter.setCustomConversions(conversions);
converter.afterPropertiesSet();
var template = new MongoTemplate(factory, converter);
var repository = new MongoRepositoryFactory(template).getRepository(TextRows.class);

Configure the client explicitly with MongoClientSettings.builder().uuidRepresentation(UuidRepresentation.STANDARD) before building it. The tests read raw BsonDocument values and assert UUID binary subtype 4, BSON string, and BSON int64 for IDs and ordinary fields, not nested wrappers or numeric strings.

Choose identifier and null mappings deliberately

Section titled “Choose identifier and null mappings deliberately”

The recipes use Spring Data @Id for UUID and Long concept identifiers, with the concrete pairs above. For the String concept identifier, use @MongoId(FieldType.STRING) explicitly: the tested 24-hex value 507f1f77bcf86cd799439011 stays a BSON string instead of becoming an ObjectId. Do not substitute an unqualified @MongoId for these mappings; its implicit target is not the same conversion policy. Declare repository IDs as the concrete concept type, for example MongoRepository<TextRow, TextValue>.

Kotlin documents use constructor-bound concept IDs and mutable ordinary fields. Ordinary Java documents use no-argument construction and field hydration, with immutable concept records as their values; this avoids relying on Java constructor parameter-name compiler metadata. Do not change an identifier after saving a document.

Spring bypasses these scalar converters for null. Apply @Field(write = Field.Write.ALWAYS) (@field:Field(write = Field.Write.ALWAYS) in Kotlin) when a nullable concept field must be written as explicit BSON null. Without it, the tested null field is absent. Both read as null; the tests separately remove an explicitly written field and verify absent-field hydration. Non-null blank text fails in the concept constructor. A stored String in the Long field fails with ConverterNotFoundException rather than becoming a default Long concept; the configured reading pair accepts Long, not arbitrary malformed storage types.

MongoDB documents are not JPA managed entities: changing a loaded field alone does not persist it. Call repository save explicitly or use a mapped MongoTemplate.updateFirst with a concept-valued query and update. Verify with another read through a fresh template/converter.

Integrations/SpringDataMongo tests io.cratis.arc.persistence.recipes.mongodb.MongoConceptStorageTests and JavaMongoConceptStorageTests execute configured repositories, concept findById and equality predicates, fresh class reconstruction, explicit save/update, distinct null/absent BSON shapes, malformed-value failures, and exact signed Long boundaries including values beyond JavaScript’s exact integer range. The missing-registration control stores a nested concept document instead of a scalar; adding the application registration produces the asserted scalar. This is an application omission control, not an existing Arc product bug.

Each language stores the same UUID, String, and Long concept keys with different values in two databases. Templates are fully configured before issuing TenantMongoOperations certificates. The existing contextual resolver rejects unknown tenants, mismatched certificates, missing/blank tenants, scalar keys, and a different concept key class. Exact resolver-call and driver find command counts prove no retry or fallback database read. Mongo certificates certify tenantId, not JPA’s tenant namespace; no Mongo namespace-isolation claim follows. Repository injection alone remains ordinary Spring dependency injection, not tenant routing.

This evidence uses Spring Data MongoDB 5.1.1 and Mongo Java driver 5.8.1 against the existing mongo-java-server 1.47.0 MemoryBackend emulator, not a real MongoDB server, Testcontainers, or an external database. It does not certify production MongoDB, other providers or BSON types, change streams, replica sets, transactions, optimistic locking, or Arc .NET parity. No provider dependency or Arc API is added by the recipes.

Terminal window
./gradlew :Integrations:SpringDataMongo:test --tests '*MongoConceptStorageTests'