Skip to content

Map MongoDB concepts

Map MongoDB concepts explicitly in the application

Section titled “Map MongoDB concepts explicitly in the application”

If your documents store concepts, MongoDB needs a mapping between each wrapper and its BSON scalar. This is 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 does not register that configuration.

Finish configuring 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.

The writing converter unwraps the concept; the reading converter reconstructs it from the scalar. For a concept such as the TextValue shown in Map JPA concepts, define both directions 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); }
}

View Java snippet source on GitHub

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. These are concrete fixture types, not definitions supplied by Arc or by this excerpt. Do not replace them with an erased Converter<ConceptAs<?>, ?> or infer constructors reflectively.

Defining the pairs is only the first step. Install all six together so Spring Data uses them when reading and writing:

MongoCustomConversions 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());
});

View Java snippet source on GitHub

For a programmatically owned template, apply that same configuration to both the mapping context and the converter before constructing the template. The order matters: repositories must see the finished mappings, not a template that will be reconfigured on lookup. The complete RecipeMongoStore fixture also owns and closes its client:

static TextRows textRows(MongoClient client, String database, MongoCustomConversions conversions) {
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);
return new MongoRepositoryFactory(template).getRepository(TextRows.class);
}

View Java snippet source on GitHub

Configure the client explicitly with MongoClientSettings.builder().uuidRepresentation(UuidRepresentation.STANDARD) before building it. This determines the UUID representation used by the recipe. Its tests read raw BsonDocument values and assert UUID binary subtype 4, BSON string, and BSON int64 for IDs and ordinary fields, rather than 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.

Choose the stored null shape separately from the scalar conversion: Spring bypasses these 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 still 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.

Unlike a JPA managed entity, a loaded MongoDB document has no dirty tracking that will persist an in-memory field change for you. Call repository save explicitly or use a mapped MongoTemplate.updateFirst with a concept-valued query and update. Then read through a fresh template/converter to verify the stored result.

Finish configuring each template before issuing its TenantMongoOperations certificate. For tenant-routed command lookup, that certificate covers tenantId, not JPA’s tenant namespace; it does not establish Mongo namespace isolation. Repository injection alone remains ordinary Spring dependency injection, not tenant routing.

The documented recipe coverage in Integrations/SpringDataMongo comes from io.cratis.arc.persistence.recipes.mongodb.MongoConceptStorageTests and JavaMongoConceptStorageTests. They 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 demonstrates an application omission, not an existing Arc product bug.

Each language stores the same UUID, String, and Long concept keys with different values in two databases. 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.

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'