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.
Register concrete scalar pairs
Section titled “Register concrete scalar pairs”For a concept such as the TextValue shown in Map JPA concepts, the Kotlin pair is:
@WritingConverterclass TextWrite : Converter<TextValue, String> { override fun convert(source: TextValue): String = source.value()}
@ReadingConverterclass TextRead : Converter<String, TextValue> { override fun convert(source: String): TextValue = TextValue(source)}The ordinary Java equivalent constructs the record explicitly:
@WritingConverterpublic class TextWrite implements Converter<TextValue, String> { @Override public String convert(TextValue source) { return source.value(); }}
@ReadingConverterpublic 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.
MongoDB recipe evidence and limits
Section titled “MongoDB recipe evidence and limits”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.
./gradlew :Integrations:SpringDataMongo:test --tests '*MongoConceptStorageTests'