spring-projects / spring-projects/spring-data-mongodb
Typed patch updates from partial entities on Mongo repositories
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 1.7k
- Forks
- 1.1k
- PR merge metrics
- No merged PRs in 30d
Description
Summary
Add a first-class, explicitly named patch API on Mongo repositories / MongoOperations that applies a typed partial entity as a $set-only update against the repository’s aggregate type T, without loading the full document and without replacing unmapped fields.
Naming parallel with the existing API:
save(entity)— persist / replace the aggregatepatch(partialEntity)— write only the fields mapped by the partial type
This is not a request to change save() semantics and not a request to treat a half-populated instance of T as an update stamp. A partial entity is a dedicated type whose mapped properties are a subset of T.
Motivation
In systems where multiple services share a MongoDB collection (or where a service only owns a subset of fields), the current options are awkward:
CrudRepository.save(entity)replaces the whole document. When the Java type maps only a subset of fields, properties that exist in MongoDB but are absent from that type are not preserved across the write.MongoTemplate+Updateworks, but pushes teams toward untyped field paths / JSON update strings.- Repository
@Updateis useful for fixed updates, but does not scale well for “apply this typed slice of fields” use cases.
Teams often invent local helpers (converter.write → $set). A supported patch would make the intent explicit and keep that pattern consistent.
Concrete scenario
- Collection
customers - Billing service maps
{ id, billing.plan, billing.status } - Shipping service maps
{ id, shipping.address } - Billing needs to update its fields while leaving
shipping.*unchanged
With the current API, persisting a billing-only mapped type through save(...) performs a full document replace, so fields outside that type are not retained. A dedicated patch(billingPartial) would express “write only these mapped fields” without requiring a full-document read/merge.
Why this is different from #2642
#2642 asked for partial updates from a partially filled domain POJO, and was declined mainly because:
- A half-filled aggregate as an update stamp conflicts with Spring Data’s DDD-oriented model.
- “Non-null fields are written” cannot express “set this field to
null”.
This proposal avoids both issues:
| Concern in #2642 | This proposal |
|---|---|
Half-filled aggregate root (T with some nulls) |
Separate partial entity type, validated against T |
Overloaded / implicit save |
Explicit patch API; save unchanged |
| Null ambiguity | Explicit null policy (see below) |
Proposed API
The core idea is an explicit patch operation. Where it lives can be decided by the team; a few options:
Option A — extend the existing repository type
Expose patch on MongoRepository (or a shared Spring Data abstraction, if that fits better):
<P> long patch(P partialEntity);
<P> long patch(ID id, P partialEntity);
Existing repositories would gain the method without introducing a new interface.
Option B — dedicated repository type / fragment
Keep MongoRepository unchanged and offer an opt-in type for apps that need patching:
@NoRepositoryBean
public interface PatchMongoRepository<T, ID> extends MongoRepository<T, ID> {
<P> long patch(P partialEntity);
<P> long patch(ID id, P partialEntity);
}
Or the same methods via a repository fragment, so only selected repositories opt in.
Option C — MongoOperations first
Start at the template level, then decide whether/how to surface it on repositories:
<T, P> UpdateResult patch(P partialEntity, Class<T> entityClass);
<T, P> UpdateResult patch(Object id, P partialEntity, Class<T> entityClass);
<T, P> UpdateResult patch(Query query, P partialEntity, Class<T> entityClass);
Example (shape of the call)
@Document("customers")
class Customer {
@Id String id;
Billing billing;
Shipping shipping;
}
record BillingPlanPartial(
@Id String id,
@Field("billing.plan") String plan,
@Field("billing.status") String status
) {}
// Depending on the chosen option:
// - CustomerRepository extends MongoRepository<Customer, String>
// - or CustomerRepository extends PatchMongoRepository<Customer, String>
customerRepository.patch(new BillingPlanPartial(id, "pro", "ACTIVE"));
// => db.customers.updateOne({_id: id}, {$set: {"billing.plan": "pro", "billing.status": "ACTIVE"}})
Unmapped fields such as shipping remain untouched. No full-document read is required.
Mapping rules (partial entity → $set)
- Repository / operation remains typed to aggregate
T(collection + mapping metadata come fromT). Pis a partial entity type, not necessarily a subtype ofT.- Each writable property in
Pmaps to a Mongo field path via normal Spring Data mapping (@Field, naming strategy, etc.). - Suggested compatibility rule: property names/types in
Pmust be assignable to a property path onT(runtime validation using persistent entity metadata; optional compile-time checks can live outside core). - Nested documents should prefer dotted paths (
billing.plan) so patching does not replace an entire subdocument unless the partial entity intentionally maps the whole subdocument. - Metadata fields such as
_id/_classare not$setunless explicitly designed that way (_ididentifies the document).
Null / presence semantics
Do not use “null means skip” as the only model.
Proposed options (API detail open for discussion):
- Default for plain nullable fields: include
$setonly for properties considered “present”. - Explicit absent vs null: support wrapper types already common in the ecosystem, e.g.:
Optional<T>/ dedicated patch wrapper- or an annotation such as
@PatchNull/ unset marker
- Explicit unset: allow mapping a present sentinel / annotation to
$unset.
The important part: null handling must be documented and opt-in/clear, not inferred only from Java null on a partially constructed aggregate.
Non-goals
- Changing
CrudRepository.save/ replace semantics - Treating a partially populated instance of
Tas a patch stamp - Replacing
@Updatestring/pipeline updates for fixed derived queries - Requiring a Lombok-like annotation processor in spring-data-mongodb core
(compile-time partial-entity validation can be a separate community module later)
Benefits
- Strongly typed patches without untyped
Updatepath strings everywhere - Fits shared collections / field ownership across services
- Clear write-side counterpart to read projections: projections for read, partial entities for
patch - Preserves DDD purity of
save(entity)while offering an explicit escape hatch for PATCH-like persistence
Suggested acceptance criteria
-
patch(partialEntity)/patch(id, partialEntity)emit$setonly for mapped partial-entity properties - Fields present in the document but absent from the partial entity are preserved
- Mapping honors
@Fieldand persistent property metadata ofT - Null/presence behavior is explicit and tested
-
savebehavior remains unchanged - Imperative and reactive variants (
MongoOperations/ReactiveMongoOperations, repository support as chosen)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by comparing the existing MongoRepository, MongoOperations, ReactiveMongoOperations, and repository-support entry points to determine where the proposed API could fit. Review the acceptance criteria for typed partial-entity mapping, preservation of unmapped fields, explicit null behavior, unchanged save semantics, and imperative/reactive coverage.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, mongodb, spring
- Domain
- databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100