Reputation: 657
I'm trying to implement a partial update of the Manager entity based in the following:
Entity
public class Manager {
private int id;
private String firstname;
private String lastname;
private String username;
private String password;
// getters and setters omitted
}
SaveManager method in Controller
@RequestMapping(value = "/save", method = RequestMethod.PATCH)
public @ResponseBody void saveManager(@RequestBody Manager manager){
managerService.saveManager(manager);
}
Save object manager in Dao impl.
@Override
public void saveManager(Manager manager) {
sessionFactory.getCurrentSession().saveOrUpdate(manager);
}
When I save the object the username and password has changed correctly but the others values are empty.
So what I need to do is update the username and password and keep all the remaining data.
Upvotes: 21
Views: 57248
Reputation: 1839
As was mentioned, you can use beanutils, which supports arrays/lists as well. Example code below defines dot-notation based whitelist of fields for updates. Adapt for the web, injecting properties, saving to DB, etc.
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.PropertyUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
@Slf4j
public class PatchProcessor {
private final PatchProperties patchProperties;
public PatchProcessor() {
this.patchProperties = new PatchProperties();
}
public void patch(Object sourceObject, Object targetObject) throws Exception {
for (Map.Entry<String, Object> allowedField : patchProperties.fieldsAllowedMap.entrySet()) {
processFieldUpdate(sourceObject, targetObject, allowedField, "");
}
}
private void processFieldUpdate(Object sourceObject, Object targetObject, Map.Entry<String, Object> subField, String parentField) throws Exception {
String targetObjectKey = subField.getKey();
String fullField = ("".equals(parentField)? "" : parentField + ".") + targetObjectKey;
Object sourceSubObject = PropertyUtils.getProperty(sourceObject, targetObjectKey);
if (sourceSubObject instanceof Collection) {
String lookupKey = patchProperties.listLookupField.get(fullField);
boolean hasListId = patchProperties.listLookupField.containsKey(fullField);
List sourceList = (List) sourceSubObject;
List targetList = (List) PropertyUtils.getProperty(targetObject, targetObjectKey);
if (null == targetList) {
targetList = sourceList.getClass().newInstance();
PropertyUtils.setProperty(targetObject, targetObjectKey, targetList);
}
int index = 0;
for (Object sourceListObject : sourceList) {
Object targetListObject = null;
if (hasListId) {
targetListObject = findTargetListObject(sourceListObject, targetList, lookupKey);
} else {
targetListObject = targetList.size() >= index + 1? targetList.get(index) : null;
}
if (null == targetListObject) {
targetListObject = sourceListObject.getClass().newInstance();
if (hasListId) {
Object sourceKeyValue = PropertyUtils.getProperty(sourceListObject, lookupKey);
if (sourceKeyValue == null && PropertyUtils.getPropertyDescriptor(sourceListObject, lookupKey).getPropertyType() == UUID.class) {
sourceKeyValue = UUID.randomUUID();
PropertyUtils.setProperty(sourceListObject, lookupKey, sourceKeyValue);
} else if (sourceKeyValue == null) {
log.warn("unable to set id due to unknown type");
}
PropertyUtils.setProperty(targetListObject, lookupKey, sourceKeyValue);
}
targetList.add(targetListObject);
}
if (subField.getValue() instanceof Map) {
Map<String, Object> listObjectSubFields = (Map<String, Object>) subField.getValue();
for (Map.Entry<String, Object> listObjectSubField : listObjectSubFields.entrySet()) {
processFieldUpdate(sourceListObject, targetListObject, listObjectSubField, fullField);
}
} else if (!hasListId) {
targetList.set(index, sourceListObject);
} else {
targetList.set(targetList.indexOf(targetListObject), sourceListObject);
}
index++;
}
} else {
Object targetSubObject = PropertyUtils.getProperty(targetObject, targetObjectKey);
if (null == sourceSubObject) {
log.debug("not setting field {} to null", targetObjectKey);
} else if (null != targetSubObject) {
if (subField.getValue() instanceof Map) {
Map<String, Object> listObjectSubFields = (Map<String, Object>) subField.getValue();
for (Map.Entry<String, Object> listObjectSubField : listObjectSubFields.entrySet()) {
processFieldUpdate(sourceSubObject, targetSubObject, listObjectSubField, fullField);
}
} else {
Object rightValue = PropertyUtils.getProperty(sourceObject, targetObjectKey);
if (rightValue != null) {
PropertyUtils.setProperty(targetObject, targetObjectKey, rightValue);
} else {
//ignore null
}
}
} else {
if (subField.getValue() instanceof Map) {
targetSubObject = sourceSubObject.getClass().newInstance();
PropertyUtils.setProperty(targetObject, targetObjectKey, targetSubObject);
Map<String, Object> listObjectSubFields = (Map<String, Object>) subField.getValue();
for (Map.Entry<String, Object> listObjectSubField : listObjectSubFields.entrySet()) {
processFieldUpdate(sourceSubObject, targetSubObject, listObjectSubField, fullField);
}
} else {
Object rightValue = PropertyUtils.getProperty(sourceObject, targetObjectKey);
if (rightValue != null) {
PropertyUtils.setProperty(targetObject, targetObjectKey, rightValue);
} else {
//ignore null
}
}
}
}
}
public Object findTargetListObject(Object sourceListObject, List targetList, String lookupKey) throws Exception {
Object sourceKeyValue = PropertyUtils.getProperty(sourceListObject, lookupKey);
return targetList.stream().filter(obj -> {
try {
Object targetKeyValue = PropertyUtils.getProperty(obj, lookupKey);
return Objects.equals(sourceKeyValue, targetKeyValue);
} catch (Exception e) {
throw new RuntimeException(e);
}
}).findFirst().orElse(null);
}
private class PatchProperties {
List<String> fieldsAllowed = new ArrayList<>();
Map<String, String> listLookupField = new HashMap<>();
Map<String, Object> fieldsAllowedMap;
PatchProperties() {
fieldsAllowed.add("name");
fieldsAllowed.add("phoneNumber");
fieldsAllowed.add("jobs.location");
fieldsAllowed.add("jobs.emailAddresses.privateUse");
fieldsAllowed.add("jobs.emailAddresses.emailAddress");
fieldsAllowed.add("jobs.emailAddresses.siteUsedList.siteName");
fieldsAllowed.add("jobs.emailAddresses.siteUsedList.pinCode.oldDigits");
fieldsAllowed.add("jobs.emailAddresses.siteUsedList.pinCode.pinDigits");
fieldsAllowed.add("jobs.emailAddresses.siteUsedList.pinCode.pinCodeType");
fieldsAllowed.add("jobs.emailAddresses.siteUsedList.pinCode.maxAttempts");
fieldsAllowed.add("jobs.emailAddresses.siteUsedList.pinCode.pinAttempts.status");
fieldsAllowed.add("websites.dateAdded");
fieldsAllowed.add("websites.url");
fieldsAllowed.add("websites.visits.asUser");
fieldsAllowed.add("websites.visits.timeOfVisit");
fieldsAllowedMap = setupFieldsAllowedMap(fieldsAllowed);
listLookupField.put("jobs", "subId");
listLookupField.put("jobs.emailAddresses", "emailId");
listLookupField.put("jobs.emailAddresses.siteUsedList", "siteUsedId");
listLookupField.put("jobs.emailAddresses.siteUsedList.pinCode.pinAttempts", "pinAttemptId");
}
}
private static Map<String, Object> setupFieldsAllowedMap(Collection<String> fieldsAllowed) {
Map<String, Object> fieldsAllowedMap = new HashMap<>();
fieldsAllowed.stream().forEach(fieldStr -> {
if (fieldStr.contains(".")) {
String key = fieldStr.substring(0, fieldStr.indexOf('.'));
if (!fieldsAllowedMap.containsKey(key)) {
fieldsAllowedMap.put(key, new HashSet<String>());
}
Set<String> subValues = (Set<String>) fieldsAllowedMap.get(key);
subValues.add(fieldStr.substring(fieldStr.indexOf('.') + 1));
} else {
fieldsAllowedMap.put(fieldStr, 1);
}
});
fieldsAllowedMap.keySet().stream().forEach(key -> {
if (fieldsAllowedMap.get(key) instanceof Collection) {
fieldsAllowedMap.put(key, setupFieldsAllowedMap((Collection<String>) fieldsAllowedMap.get(key)));
}
});
return fieldsAllowedMap;
}
}
Domain classes:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Email {
private UUID emailId;
private Boolean privateUse;
private String emailAddress;
private List<SiteUsed> siteUsedList;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Job {
private UUID subId;
private String location;
private String title;
private List<Email> emailAddresses;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private UUID resourceId;
private LocalDateTime createdDateTime;
private String name;
private String title;
private Integer age;
private Phone phoneNumber;
private List<Job> jobs;
private List<Website> websites;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Phone {
private String phoneNumber;
private Integer id;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PinAttempt {
private UUID pinAttemptId;
private String status;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PinCode {
private String pinDigits;
private int maxAttempts;
private List<PinAttempt> pinAttempts;
private List<String> oldDigits;
private PinCodeType pinCodeType;
}
public enum PinCodeType {
LONG, SHORT
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SiteUsed {
private UUID siteUsedId;
private String siteName;
private PinCode pinCode;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Visit {
private LocalDateTime timeOfVisit;
private String asUser;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Website {
private String url;
private LocalDate dateAdded;
private List<Visit> visits;
}
Then the unit test:
class PatchProcessorTest {
@Test
void patch() throws Exception {
UUID matchJobId = UUID.randomUUID();
Person person = new Person();
person.setTitle("original title");
person.setName("Joe");
List<Website> websites = new ArrayList<>();
websites.add(new Website("url1", LocalDate.of(2022, 12, 10), null));
List<Visit> visits = new ArrayList<>();
visits.add(new Visit(LocalDateTime.MIN, "asUser1"));
websites.add(new Website("url1", LocalDate.of(2022, 12, 11), visits));
websites.add(new Website());
person.setWebsites(websites);
List<PinAttempt> pinAttempts = new ArrayList<>();
UUID pinAttemptId1 = UUID.randomUUID();
pinAttempts.add(new PinAttempt(pinAttemptId1, "success"));
List<SiteUsed> siteUsedList = new ArrayList<>();
UUID siteUsedId1 = UUID.randomUUID();
List<String> oldDigits = new ArrayList<>();
oldDigits.add("oldigit1");
oldDigits.add("oldigit2");
siteUsedList.add(new SiteUsed(siteUsedId1, "siteOne", new PinCode("1234", 3, pinAttempts, oldDigits, null)));
List<Email> emailAddresses = new ArrayList<>();
UUID emailAddy1 = UUID.randomUUID();
emailAddresses.add(new Email(emailAddy1, true, "email1", siteUsedList));
person.setJobs(new ArrayList<>());
person.getJobs().add(new Job(UUID.randomUUID(), "location 1", "job title 1", null));
person.getJobs().add(new Job(matchJobId, "location 2", "job title 2", emailAddresses));
person.setAge(99);
Person update = new Person();
update.setName("Joseph");
update.setTitle("new title");
List<Website> websites2 = new ArrayList<>();
List<Visit> visits1 = new ArrayList<>();
visits1.add(new Visit(LocalDateTime.now(), "asUser1"));
websites2.add(new Website("url2", LocalDate.of(2022, 12, 12), visits1));
List<Visit> visits2 = new ArrayList<>();
visits2.add(new Visit(LocalDateTime.MAX, "asUser2"));
visits2.add(new Visit(LocalDateTime.now(), "asUser3"));
websites2.add(new Website("url3", LocalDate.of(2022, 12, 13), visits2));
update.setWebsites(websites2);
List<PinAttempt> pinAttempts1 = new ArrayList<>();
pinAttempts1.add(new PinAttempt(UUID.randomUUID(), "sucfail"));
pinAttempts1.add(new PinAttempt(pinAttemptId1, "fail"));
List<SiteUsed> siteUsedList2 = new ArrayList<>();
List<String> oldDigits2 = new ArrayList<>();
oldDigits2.add("olddigit1");
oldDigits2.add("olddigit2");
oldDigits2.add("olddigit3");
siteUsedList2.add(new SiteUsed(siteUsedId1, "siteOne1", new PinCode("12345", 2, pinAttempts1, oldDigits2, PinCodeType.LONG)));
UUID siteUsedId2 = UUID.randomUUID();
siteUsedList2.add(new SiteUsed(siteUsedId2, "siteOne1", new PinCode("123456", 2, pinAttempts1, oldDigits2, PinCodeType.SHORT)));
List<Email> updatedEmailAddresses = new ArrayList<>();
updatedEmailAddresses.add(new Email(emailAddy1, false, "email2", siteUsedList2));
updatedEmailAddresses.add(new Email(null, true, "email3", null));
update.setJobs(new ArrayList<>(Arrays.asList(new Job(matchJobId, "new location 2", "new job title 2", updatedEmailAddresses),
new Job(UUID.randomUUID(), "new location 3", "job title 3", null))));
update.setAge(66);
PatchProcessor processor = new PatchProcessor();
processor.patch(update, person);
assertEquals("Joseph", person.getName());
assertNotEquals("new title", person.getTitle());
assertTrue(person.getJobs().size() == 3);
assertEquals("new location 2", person.getJobs().get(1).getLocation());
assertEquals("siteOne1", person.getJobs().get(1).getEmailAddresses().get(0).getSiteUsedList().get(0).getSiteName());
assertEquals("12345", person.getJobs().get(1).getEmailAddresses().get(0).getSiteUsedList().get(0).getPinCode().getPinDigits());
assertEquals("fail", person.getJobs().get(1).getEmailAddresses().get(0).getSiteUsedList().get(0).getPinCode().getPinAttempts().get(0).getStatus());
assertEquals("url2", person.getWebsites().get(0).getUrl());
assertEquals("asUser1", person.getWebsites().get(0).getVisits().get(0).getAsUser());
assertEquals(2, person.getWebsites().get(1).getVisits().size());
assertEquals(3, person.getWebsites().size());
assertEquals(siteUsedId2, person.getJobs().get(1).getEmailAddresses().get(0).getSiteUsedList().get(1).getSiteUsedId());
assertEquals("123456", person.getJobs().get(1).getEmailAddresses().get(0).getSiteUsedList().get(1).getPinCode().getPinDigits());
assertEquals(PinCodeType.LONG, person.getJobs().get(1).getEmailAddresses().get(0).getSiteUsedList().get(0).getPinCode().getPinCodeType());
assertEquals(99, person.getAge());
}
}
Upvotes: 0
Reputation: 195
You can use Mapstruct to achievie this goal OOTB. Configuration NullValuePropertyMappingStrategy.IGNORE
is the key.
See this example:
@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UpdateMapper {
void update(@MappingTarget MyClass target, MyClass source);
}
And use it:
UpdateMapper mapper = new UpdateMapperImpl();
mapper.update(target, source);
repository.save(target);
Here you can read Baeldung reference
Upvotes: 0
Reputation: 5893
If you are truly using a PATCH, then you should use RequestMethod.PATCH, not RequestMethod.POST.
Your patch mapping should contain the id with which you can retrieve the Manager object to be patched. Also, it should only include the fields with which you want to change. In your example you are sending the entire entity, so you can't discern the fields that are actually changing (does empty mean leave this field alone or actually change its value to empty).
Perhaps an implementation as such is what you're after?
@RequestMapping(value = "/manager/{id}", method = RequestMethod.PATCH)
public @ResponseBody void saveManager(@PathVariable Long id, @RequestBody Map<Object, Object> fields) {
Manager manager = someServiceToLoadManager(id);
// Map key is field name, v is value
fields.forEach((k, v) -> {
// use reflection to get field k on manager and set it to value v
Field field = ReflectionUtils.findField(Manager.class, k);
field.setAccessible(true);
ReflectionUtils.setField(field, manager, v);
});
managerService.saveManager(manager);
}
I want to provide an update to this post as there is now a project that simplifies the patching process.
The artifact is
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.13</version>
</dependency>
The implementation to patch the Manager object in the OP would look like this:
@Operation(summary = "Patch a Manager")
@PatchMapping("/{managerId}")
public Task patchManager(@PathVariable Long managerId, @RequestBody JsonPatch jsonPatch)
throws JsonPatchException, JsonProcessingException {
return managerService.patch(managerId, jsonPatch);
}
...
@Autowired
private ObjectMapper objectMapper;
...
public Manager patch(Long managerId, JsonPatch jsonPatch) throws JsonPatchException, JsonProcessingException {
Manager manager = managerRepository.findById(managerId).orElseThrow(EntityNotFoundException::new);
JsonNode patched = jsonPatch.apply(objectMapper.convertValue(manager, JsonNode.class));
return managerRepository.save(objectMapper.treeToValue(patched, Manager.class));
}
The patch request follows the specifications in RFC 6092, so this is a true PATCH implementation. Details can be found here
Upvotes: 49
Reputation: 41
ObjectMapper.updateValue
provides all you need to partially map your entity with values from dto.
As an addition, you can use either of two here: Map<String, Object> fields
or String json
, so your service method may look like this:
@Autowired
private ObjectMapper objectMapper;
@Override
@Transactional
public Foo save(long id, Map<String, Object> fields) throws JsonMappingException {
Foo foo = fooRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
return objectMapper.updateValue(foo , fields);
}
As a second solution and addition to Lane Maxwell's answer you could use Reflection
to map only properties that exist in a Map of values that was sent, so your service method may look like this:
@Override
@Transactional
public Foo save(long id, Map<String, Object> fields) {
Foo foo = fooRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
fields.keySet()
.forEach(k -> {
Method method = ReflectionUtils.findMethod(LocationProduct.class, "set" + StringUtils.capitalize(k));
if (method != null) {
ReflectionUtils.invokeMethod(method, foo, fields.get(k));
}
});
return foo;
}
Second solution allows you to insert some additional business logic into mapping process, might be conversions or calculations ect.
Also unlike finding reflection field Field field = ReflectionUtils.findField(Foo.class, k);
by name and than making it accessible, finding property's setter actually calls setter method that might contain additional logic to be executed and prevents from setting value to private properties.
Upvotes: 1
Reputation: 4597
With this, you can patch your changes
1. Autowire `ObjectMapper` in controller;
2. @PatchMapping("/manager/{id}")
ResponseEntity<?> saveManager(@RequestBody Map<String, String> manager) {
Manager toBePatchedManager = objectMapper.convertValue(manager, Manager.class);
managerService.patch(toBePatchedManager);
}
3. Create new method `patch` in `ManagerService`
4. Autowire `NullAwareBeanUtilsBean` in `ManagerService`
5. public void patch(Manager toBePatched) {
Optional<Manager> optionalManager = managerRepository.findOne(toBePatched.getId());
if (optionalManager.isPresent()) {
Manager fromDb = optionalManager.get();
// bean utils will copy non null values from toBePatched to fromDb manager.
beanUtils.copyProperties(fromDb, toBePatched);
updateManager(fromDb);
}
}
You will have to extend BeanUtilsBean
to implement copying of non null values behaviour.
public class NullAwareBeanUtilsBean extends BeanUtilsBean {
@Override
public void copyProperty(Object dest, String name, Object value)
throws IllegalAccessException, InvocationTargetException {
if (value == null)
return;
super.copyProperty(dest, name, value);
}
}
and finally, mark NullAwareBeanUtilsBean as @Component
or
register NullAwareBeanUtilsBean
as bean
@Bean
public NullAwareBeanUtilsBean nullAwareBeanUtilsBean() {
return new NullAwareBeanUtilsBean();
}
Upvotes: 22
Reputation: 5371
You can write custom update query which updates only particular fields:
@Override
public void saveManager(Manager manager) {
Query query = sessionFactory.getCurrentSession().createQuery("update Manager set username = :username, password = :password where id = :id");
query.setParameter("username", manager.getUsername());
query.setParameter("password", manager.getPassword());
query.setParameter("id", manager.getId());
query.executeUpdate();
}
Upvotes: 1
Reputation: 2923
First, you need to know if you are doing an insert or an update. Insert is straightforward. On update, use get() to retrieve the entity. Then update whatever fields. At the end of the transaction, Hibernate will flush the changes and commit.
Upvotes: 2