Reputation: 2254
I have a project where I am using Spring MVC + Jackson to build a REST service. Let's say I have the following java entity
public class MyEntity {
private Integer id;
private boolean aBoolean;
private String aVeryBigString;
//getter & setters
}
Sometimes, I just want to update the boolean value, and I don't think that sending the whole object with its big string is a good idea just to update a simple boolean. So, I have considered using the PATCH HTTP method to only send the fields that need to be updated. So, I declare the following method in my controller:
@RequestMapping(method = RequestMethod.PATCH)
public void patch(@RequestBody MyVariable myVariable) {
//calling a service to update the entity
}
The problem is: how do I know which fields need to be updated? For instance, if the client just wants to update the boolean, I will get an object with an empty "aVeryBigString". How am I supposed to know that the user just wants to update the boolean, but does not want to empty the string?
I have "solved" the problem by building custom URLs. For instance, the following URL: POST /myentities/1/aboolean/true will be mapped to a method that allows to only update the boolean. The problem with this solution is that it is not REST compliant. I don't want to be 100% REST compliant, but I do not feel comfortable with providing a custom URL to update each field (especially given that it causes problems when I want to update several fields).
Another solution would be to split "MyEntity" into multiple resources and just update these resources, but I feel like it does not make sense: "MyEntity" is a plain resource, it is not composed of other resources.
So, is there an elegant way of solving this problem?
Upvotes: 67
Views: 53205
Reputation: 516
There are a lot of other great approaches here, but I figured I would add mine since I haven't seen it mentioned and I think it has the added advantage that it can handle nullable fields without having to add a list of updated fields inline with the request. This approach has these properties:
null
in the JSON are updated to null
in the data storeSo, given the following domain object:
public class User {
Integer id;
String firstName;
String lastName;
}
The controller method to incrementally update the user looks like the following, which could easily be extracted out to a static method suitable for any domain object using generics:
public class UserController {
@Autowired ObjectMapper om;
@Autowired
@Qualifier("mvcValidator")
private Validator validator;
// assume this is a JPARepository
@Autowired
private UserRepository userRepo;
@PostMapping(value = "/{userId}", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> incrementalUpdate(@PathVariable("userId") Integer userId,
@RequestBody requestJson) {
final User existingUser = this.userRepo.findById(userId).orElse(null);
if(existingUser == null) {
return ResponseEntity.notFound().build();
}
// OPTIONAL - validate the request, since we can't use @Validated
try {
final User incomingUpdate = om.readValue(updateJson, User.class);
final BeanPropertyBindingResult validationResult
= new BeanPropertyBindingResult(incomingUpdate, "user");
this.validator.validate(incomingUpdate, validationResult);
if (validationResult.hasErrors()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
} catch (JsonProcessingException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
// merge the incoming update into the existing user
try {
this.om.readerForUpdating(existingUser).readValue(updateJson, User.class);
} catch(IOException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
this.userRepo.save(existingUser);
return ResponseEntity.noContent().build();
}
}
Note that if your domain object has any nested objects or collections, they will need to be annotated with @JsonMerge
, otherwise they will just be unconditionally overwritten by the incoming value instead of merged recursively.
Upvotes: 1
Reputation: 6058
After digging around a bit I found an acceptable solution using the same approach currently used by a Spring MVC DomainObjectReader
see also: JsonPatchHandler
import org.springframework.data.rest.webmvc.mapping.Associations
@RepositoryRestController
public class BookCustomRepository {
private final DomainObjectReader domainObjectReader;
private final ObjectMapper mapper;
private final BookRepository repository;
@Autowired
public BookCustomRepository(BookRepository bookRepository,
ObjectMapper mapper,
PersistentEntities persistentEntities,
Associations associationLinks) {
this.repository = bookRepository;
this.mapper = mapper;
this.domainObjectReader = new DomainObjectReader(persistentEntities, associationLinks);
}
@PatchMapping(value = "/book/{id}", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<?> patch(@PathVariable String id, ServletServerHttpRequest request) throws IOException {
Book entityToPatch = repository.findById(id).orElseThrow(ResourceNotFoundException::new);
Book patched = domainObjectReader.read(request.getBody(), entityToPatch, mapper);
repository.save(patched);
return ResponseEntity.noContent().build();
}
}
Upvotes: 11
Reputation: 686
If you will implement JpaRepository then you can use this.
@Modifying
@Query("update Customer u set u.phone = :phone where u.id = :id")
void updatePhone(@Param(value = "id") long id, @Param(value = "phone") String phone);
Upvotes: 0
Reputation: 534
This is an old post, but it was still a problem without a good solution for me. Here is what I am leaning towards.
The idea is to leverage the deserialization phase to track what is sent and what is not and have the entity support a way to interrogate the property change state. Here is the idea.
This interface triggers a custom deserialization and forces the bean to carry its state change information
@JsonDeserialize(using = Deser.class)
interface Changes {
default boolean changed(String name) {
Set<String> changed = changes();
return changed != null && changed.contains(name);
}
void changes(Set<String> changed);
Set<String> changes();
}
Here is the deserializer. Once it's invoked, it reverses the deserialization behavior through a mixin. Note, that it will only work when json properties map directly to bean properties. For anything fancier, I think the bean instance could be proxied and setter calls intercepted.
class Deser extends JsonDeserializer<Object> implements ContextualDeserializer {
private Class<?> targetClass;
public Deser() {}
public Deser(Class<?> targetClass) { this.targetClass = targetClass; }
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {
};
HashMap<String, Object> map = p.readValueAs(typeRef);
ObjectMapper innerMapper = mapper.copy();
innerMapper.addMixIn(targetClass, RevertDefaultDeserialize.class);
Object o = innerMapper.convertValue(map, targetClass);
// this will only work with simple json->bean property mapping
((Changes) o).changes(map.keySet());
return o;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
Class<?> targetClass = ctxt.getContextualType().getRawClass();
return new Deser(targetClass);
}
@JsonDeserialize
interface RevertDefaultDeserialize {
}
}
Here is how the bean from the question would look like. I would split up the JPA entity and the data transfer bean used in the controller interface, but here it's the same bean.
Changes could be supported by a base class if inheritance is possible, but here the interface itself is used directly.
@Data
class MyEntity implements Changes {
private Integer id;
private boolean aBoolean;
private String aVeryBigString;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private Set<String> changes;
@Override
public void changes(Set<String> changed) {
this.changes = changed;
}
@Override
public Set<String> changes() {
return changes;
}
}
and here is how it would be used
class HowToUseIt {
public static void example(MyEntity bean) {
if (bean.changed("id")) {
Integer id = bean.getId();
// ...
}
if (bean.changed("aBoolean")) {
boolean aBoolean = bean.isABoolean();
// ...
}
if (bean.changed("aVeryBigString")) {
String aVeryBigString = bean.getAVeryBigString();
// ...
}
}
}
Upvotes: 0
Reputation: 17980
@Mapper(componentModel = "spring")
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface CustomerMapper {
void updateCustomerFromDto(CustomerDto dto, @MappingTarget Customer entity);
}
public void updateCustomer(CustomerDto dto) {
Customer myCustomer = repo.findById(dto.id);
mapper.updateCustomerFromDto(dto, myCustomer);
repo.save(myCustomer);
}
The drawback of this approach is that we can't pass null values to the database during an update.
See Partial Data Update with Spring Data
See Custom Spring MVC HTTP Patch requests with Spring Data Rest functionality
Upvotes: 3
Reputation: 4007
I use reflection to solve this problem. The client can send object(e.g in javascript) which would contain all the fields with their respected value. The way I capture the new values in controller:
@PatchMapping(value = "{id}")
public HttpEntity<Map<String, Object>> updatePartial(@PathVariable Integer id, @RequestBody Map<String, Object> data) {
return ResponseEntity.ok(questionService.updatePartial(id, data));
}
Then into the service implementation we can use the reflection to find if the requested property exists and if it does then update its value.
public Map<String, Object> updatePartial(@NotNull Long id, @NotNull Map<String, Object> data) {
Post post = postRepository.findById(id);
Field[] postFields = Post.class.getDeclaredFields();
HashMap<String, Object> toReturn = new HashMap<>(1);
for (Field postField : postFields) {
data.forEach((key, value) -> {
if (key.equalsIgnoreCase(postField.getName())) {
try {
final Field declaredField = Post.class.getDeclaredField(key);
declaredField.setAccessible(true);
declaredField.set(post, value);
toReturn.put(key, value);
} catch (NoSuchFieldException | IllegalAccessException e) {
log.error("Unable to do partial update field: " + key + " :: ", e);
throw new BadRequestException("Something went wrong at server while partial updation");
}
}
});
}
postRepository.save(post);
return toReturn;
}
Spring Data JPA is used here for DB operations.
IF you want to see how I handle this at client(javascript).
PATCH
call to whatever endpoint with the data as:
{
voted: true,
reported: true
}
And then in the response client can verify if the response contains the expected properties. E.g I am expecting all the fields(which I passed as params in PATCH
) in response:
if (response.data.hasOwnProperty("voted")){
//do Something
} else{
//do something e.g report it
}
Upvotes: 1
Reputation: 1583
I've noticed that many of the provided answers are all JSON patching or incomplete answers. Below is a full explanation and example of what you need with functioning real world code
A full patch function:
@ApiOperation(value = "Patch an existing claim with partial update")
@RequestMapping(value = CLAIMS_V1 + "/{claimId}", method = RequestMethod.PATCH)
ResponseEntity<Claim> patchClaim(@PathVariable Long claimId, @RequestBody Map<String, Object> fields) {
// Sanitize and validate the data
if (claimId <= 0 || fields == null || fields.isEmpty() || !fields.get("claimId").equals(claimId)){
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); // 400 Invalid claim object received or invalid id or id does not match object
}
Claim claim = claimService.get(claimId);
// Does the object exist?
if( claim == null){
return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 Claim object does not exist
}
// Remove id from request, we don't ever want to change the id.
// This is not necessary,
// loop used below since we checked the id above
fields.remove("claimId");
fields.forEach((k, v) -> {
// use reflection to get field k on object and set it to value v
// Change Claim.class to whatver your object is: Object.class
Field field = ReflectionUtils.findField(Claim.class, k); // find field in the object class
field.setAccessible(true);
ReflectionUtils.setField(field, claim, v); // set given field for defined object to value V
});
claimService.saveOrUpdate(claim);
return new ResponseEntity<>(claim, HttpStatus.OK);
}
The above can be confusing for some people as newer devs don't normally deal with reflection like that. Basically, whatever you pass this function in the body, it will find the associated claim using the given ID, then ONLY update the fields you pass in as a key value pair.
Example body:
PATCH /claims/7
{
"claimId":7,
"claimTypeId": 1,
"claimStatus": null
}
The above will update claimTypeId and claimStatus to the given values for claim 7, leaving all other values untouched.
So the return would be something like:
{
"claimId": 7,
"claimSrcAcctId": 12345678,
"claimTypeId": 1,
"claimDescription": "The vehicle is damaged beyond repair",
"claimDateSubmitted": "2019-01-11 17:43:43",
"claimStatus": null,
"claimDateUpdated": "2019-04-09 13:43:07",
"claimAcctAddress": "123 Sesame St, Charlotte, NC 28282",
"claimContactName": "Steve Smith",
"claimContactPhone": "777-555-1111",
"claimContactEmail": "[email protected]",
"claimWitness": true,
"claimWitnessFirstName": "Stan",
"claimWitnessLastName": "Smith",
"claimWitnessPhone": "777-777-7777",
"claimDate": "2019-01-11 17:43:43",
"claimDateEnd": "2019-01-11 12:43:43",
"claimInvestigation": null,
"scoring": null
}
As you can see, the full object would come back without changing any data other than what you want to change. I know there is a bit of repetition in the explanation here, I just wanted to outline it clearly.
Upvotes: 3
Reputation: 106
My answer might be late but incase if there are people still facing the same problem. I have treid with PATCH with all the possible solutions but couldn't manage to parially update the object's fields. So i switched to POST and with post, i can update specific fields without changing the values of unchanged fields.
Upvotes: -2
Reputation: 7106
You could use Optional<>
for that:
public class MyEntityUpdate {
private Optional<String> aVeryBigString;
}
This way you can inspect the update object as follows:
if(update.getAVeryBigString() != null)
entity.setAVeryBigString(update.getAVeryBigString().get());
If field aVeryBigString
is not in the JSON document, the POJO aVeryBigString
field will be null
. If it is in the JSON document, but with a null
value, the POJO field will be an Optional
with wrapped value null
. This solution allows you to differentiate between "no-update" and "set-to-null" cases.
Upvotes: 5
Reputation: 11
Couldn't you just send an object consisting of the fields that's been updated?
Script call:
var data = JSON.stringify({
aBoolean: true
});
$.ajax({
type: 'patch',
contentType: 'application/json-patch+json',
url: '/myentities/' + entity.id,
data: data
});
Spring MVC controller:
@PatchMapping(value = "/{id}")
public ResponseEntity<?> patch(@RequestBody Map<String, Object> updates, @PathVariable("id") String id)
{
// updates now only contains keys for fields that was updated
return ResponseEntity.ok("resource updated");
}
In the controller's path
member, iterate through the key/value pairs in the updates
map. In the example above, the "aBoolean"
key will hold the value true
. The next step will be to actually assign the values by calling the entity setters. However, that's a different kind of problem.
Upvotes: 1
Reputation: 1
Here is an implementation for a patch command using googles GSON.
package de.tef.service.payment;
import com.google.gson.*;
class JsonHelper {
static <T> T patch(T object, String patch, Class<T> clazz) {
JsonElement o = new Gson().toJsonTree(object);
JsonObject p = new JsonParser().parse(patch).getAsJsonObject();
JsonElement result = patch(o, p);
return new Gson().fromJson(result, clazz);
}
static JsonElement patch(JsonElement object, JsonElement patch) {
if (patch.isJsonArray()) {
JsonArray result = new JsonArray();
object.getAsJsonArray().forEach(result::add);
return result;
} else if (patch.isJsonObject()) {
System.out.println(object + " => " + patch);
JsonObject o = object.getAsJsonObject();
JsonObject p = patch.getAsJsonObject();
JsonObject result = new JsonObject();
o.getAsJsonObject().entrySet().stream().forEach(e -> result.add(e.getKey(), p.get(e.getKey()) == null ? e.getValue() : patch(e.getValue(), p.get(e.getKey()))));
return result;
} else if (patch.isJsonPrimitive()) {
return patch;
} else if (patch.isJsonNull()) {
return patch;
} else {
throw new IllegalStateException();
}
}
}
The implementation is recursiv to take care about nested structures. Arrays are not merged, because they do not have a key for the merge.
The "patch" JSON is directly converted from String to JsonElement and not to a object to keep the not filled fields apart from the fields filled with NULL.
Upvotes: 0
Reputation: 886
This could be very late, but for the sake of newbies and people who encounter the same problem, let me share you my own solution.
In my past projects, to make it simple, I just use the native java Map. It will capture all the new values including the null values that the client explicitly set to null. At this point, it will be easy to determine which java properties needs to be set as null, unlike when you use the same POJO as your domain model, you won't be able to distinguish which fields are set by the client to null and which are just not included in the update but by default will be null.
In addition, you have to require the http request to send the ID of the record you want to update, and do not include it on the patch data structure. What I did, is set the ID in the URL as path variable, and the patch data as a PATCH body.Then with the ID, you would get first the record via a domain model,then with the HashMap, you can just use a mapper service or utility to patch the changes to the concerned domain model.
Update
You can create a abstract superclass for your services with this kind of generic code, you must use Java Generics. This is just a segment of possible implementation, I hope you get the idea.Also it is better to use mapper framework such as Orika or Dozer.
public abstract class AbstractService<Entity extends BaseEntity, DTO extends BaseDto> {
@Autowired
private MapperService mapper;
@Autowired
private BaseRepo<Entity> repo;
private Class<DTO> dtoClass;
private Class<Entity> entityCLass;
public AbstractService(){
entityCLass = (Class<Entity>) SomeReflectionTool.getGenericParameter()[0];
dtoClass = (Class<DTO>) SomeReflectionTool.getGenericParameter()[1];
}
public DTO patch(Long id, Map<String, Object> patchValues) {
Entity entity = repo.get(id);
DTO dto = mapper.map(entity, dtoClass);
mapper.map(patchValues, dto);
Entity updatedEntity = toEntity(dto);
save(updatedEntity);
return dto;
}
}
Upvotes: 25
Reputation: 69
I fixed the Problem like this, because i can't changed the service
public class Test {
void updatePerson(Person person,PersonPatch patch) {
for (PersonPatch.PersonPatchField updatedField : patch.updatedFields) {
switch (updatedField){
case firstname:
person.setFirstname(patch.getFirstname());
continue;
case lastname:
person.setLastname(patch.getLastname());
continue;
case title:
person.setTitle(patch.getTitle());
continue;
}
}
}
public static class PersonPatch {
private final List<PersonPatchField> updatedFields = new ArrayList<PersonPatchField>();
public List<PersonPatchField> updatedFields() {
return updatedFields;
}
public enum PersonPatchField {
firstname,
lastname,
title
}
private String firstname;
private String lastname;
private String title;
public String getFirstname() {
return firstname;
}
public void setFirstname(final String firstname) {
updatedFields.add(PersonPatchField.firstname);
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(final String lastname) {
updatedFields.add(PersonPatchField.lastname);
this.lastname = lastname;
}
public String getTitle() {
return title;
}
public void setTitle(final String title) {
updatedFields.add(PersonPatchField.title);
this.title = title;
}
}
Jackson called only when values exist. So you can save which setter was called.
Upvotes: 1
Reputation: 1876
The correct way to do this is the way proposed in JSON PATCH RFC 6902
A request example would be:
PATCH http://example.com/api/entity/1 HTTP/1.1
Content-Type: application/json-patch+json
[
{ "op": "replace", "path": "aBoolean", "value": true }
]
Upvotes: 11
Reputation: 1993
Spring does/can not use PATCH
to patch your object because of the same problem you already have: The JSON deserializer creates an Java POJO with nulled fields.
That means you have to provide an own logic for patching an entity (i.e. only when using PATCH
but not POST
).
Either you know you use only non primitive types, or some rules (empty String is null
, which does not work for everyone) or you have to provide an additional parameter which defines the overridden values.
Last one works fine for me: The JavaScript application knows which fields have been changed and sent in addition to the JSON body that list to the server. For example if a field description
was named to change (patch) but is not given in the JSON body, it was being nulled.
Upvotes: 4
Reputation: 12766
The whole point of PATCH
is that you are not sending the entire entity representation, so I don't understand your comments about the empty string. You would have to handle some sort of simple JSON such as:
{ aBoolean: true }
and apply that to the specified resource. The idea is that what has been received is a diff of the desired resource state and the current resource state.
Upvotes: 4
Reputation: 1584
You may change boolean to Boolean and assign null value for all fields that you don't want to update. The only one not null value will define you which field client want to update.
Upvotes: -20