spring-projects / spring-projects/spring-data-mongodb

Typed patch updates from partial entities on Mongo repositories

Open
#5,229 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
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 aggregate
  • patch(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:

  1. 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.
  2. MongoTemplate + Update works, but pushes teams toward untyped field paths / JSON update strings.
  3. Repository @Update is 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:

  1. A half-filled aggregate as an update stamp conflicts with Spring Data’s DDD-oriented model.
  2. “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)

  1. Repository / operation remains typed to aggregate T (collection + mapping metadata come from T).
  2. P is a partial entity type, not necessarily a subtype of T.
  3. Each writable property in P maps to a Mongo field path via normal Spring Data mapping (@Field, naming strategy, etc.).
  4. Suggested compatibility rule: property names/types in P must be assignable to a property path on T (runtime validation using persistent entity metadata; optional compile-time checks can live outside core).
  5. 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.
  6. Metadata fields such as _id / _class are not $set unless explicitly designed that way (_id identifies the document).

Null / presence semantics

Do not use “null means skip” as the only model.

Proposed options (API detail open for discussion):

  1. Default for plain nullable fields: include $set only for properties considered “present”.
  2. 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
  3. 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 T as a patch stamp
  • Replacing @Update string/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 Update path 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 $set only for mapped partial-entity properties
  • Fields present in the document but absent from the partial entity are preserved
  • Mapping honors @Field and persistent property metadata of T
  • Null/presence behavior is explicit and tested
  • save behavior remains unchanged
  • Imperative and reactive variants (MongoOperations / ReactiveMongoOperations, repository support as chosen)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.