Spring Data JPA Partial Update nasıl yapılır?

Ersan Ceylan
Kod Gemisi
Published in
3 min readMar 4, 2018

Bu blog yazısında, Spring Data Jpa ve Hibernate kullanarak bir entity’nin sadece belirli bölümlerinin nasıl update edilebileceğini göreceğiz.

User classının aşağıdaki gibi olduğunu düşünelim.

@Entity(name = "APP_USER")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName; private String lastName; private String email; private String phone; // getters & setters}

Bir kullanıcının e-mail adresini güncellemek için yapacağımız bir PATCH request aşağıdaki gibi olacaktır:

curl --data '{"email":"ersan@kodgemisi.com"}' -H "Content-Type: application/json" -X PATCH http://localhost:8080/users/1/

UserController:

...
@PatchMapping("/users/{id}/")
public User updatePartial(@PathVariable("id") Long id, User user) {

return userRepository.update(user, id);
}
...

UserRepository:

public class UserRepository {
...
@Transactional
public User update(User user, Long id) {
User persistedUser = entityManager.find(User.class, id);

persistedUser.setFirstName(user.getFirstName());
persistedUser.setLastName(user.getLastName());
persistedUser.setEmail(user.getEmail());
persistedUser.setPhone(user.getPhone());
entityManager.merge(persistedUser);
return persistedUser;

}
...
}

UserController’dakiupdateContactInformation() methodunda Spring, default olarak, PATCH requestinde gönderdiğimiz data objesini User instance’ına deserialize ederken bulamadığı alanları null olarak set edecektir ki bu hiç istenmeyen bir durumdur.

Post etmediğimiz her alanı manuel olarak set etmek, user nesnesinin daha fazla alana sahip olduğunu düşünürsek, en basit biçimde kod karışıklığına ve dolayısıyla ilerleyen zamanlarda hatalar oluşmasına sebep olabilir.

Bu durumda yapmamız gereken şey, User nesnesine null olarak set edilmiş alanları sql sorgusuna dahil etmemek.

BeanUtils.copyProperties()

Spring framework’ünde varolan bu sınıfı kullanarak tam da istediğimiz şeyi kolayca gerçekleştirebiliriz. copyProperties()iki class arasındaki alanları birbirine kopyalamaya yarıyor. Fazla detaya girmeden örnek üzerinde anlatmak daha kolay olacak.

@Transactional
public User updatePartial(User user, Long id) {
User persistedUser = entityManager.find(User.class, id);
String[] ignoredProperties = getNullPropertyNames(user);
BeanUtils.copyProperties(user, persistedUser, ignoredProperties);
entityManager.merge(persistedUser); return persistedUser;
}
private String[] getNullPropertyNames(User user) {
final BeanWrapper wrappedSource = new BeanWrapperImpl(user);
return Stream.of(wrappedSource.getPropertyDescriptors())
.map(FeatureDescriptor::getName)
.filter(propertyName -> wrappedSource.getPropertyValue(propertyName) == null)
.toArray(String[]::new);
}

ignoredProperties, adından da anlaşılacağı gibi kopyalama işlemi sırasında önemsenmemesini, işleme dahil edilmemesini istediğimiz alanlardan oluşan bir String[] arrayi.

BeanUtils.copyProperties(Object source, Object target, String… ignoreProperties)

BeanUtils.copyProperties() javadoc’una buradan ulasabilirsiniz.

Bu arrayi oluşturduğumuz getNullPropertyNames() methodunda yaptığımız şey ise, yine Spring Frameworkü ile gelen sınıfları kullanarak User nesnesindeki null olarak set edilmiş alanları bulmak.

Böylece güncellemek istediğiniz alanları, diğer alanları etkilemeden, güncelleyebilir hale gelirsiniz.

CriteriaBuilder

Aynı işlemi criteria builder api kullanarak yapalım. Şöyle;

public User updatePartial(User user, Long id) {
entityManager.getTransaction().begin();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaUpdate<User> updateCriteria = criteriaBuilder.createCriteriaUpdate(User.class);
Root<User> root = updateCriteria.from(User.class);
updateCriteria = buildCriteriaIgnoringNullValues(updateCriteria, user);
updateCriteria.where(criteriaBuilder.equal(root.get("id"), id));
entityManager.createQuery(updateCriteria).executeUpdate();
User persistedUser = entityManager.find(User.class, id);
log.info(persistedUser.toString());
entityManager.refresh(persistedUser);
entityManager.getTransaction().commit();
return persistedUser;
}
private CriteriaUpdate<User> buildCriteriaIgnoringNullValues(CriteriaUpdate<User> criteriaUpdate, User user) {
final BeanWrapper wrappedSource = new BeanWrapperImpl(user);
Stream.of(wrappedSource.getPropertyDescriptors())
.map(FeatureDescriptor::getName)
.filter(propertyName -> (!propertyName.equals("class") && wrappedSource.getPropertyValue(propertyName) != null))
.forEach(s -> criteriaUpdate.set(s, wrappedSource.getPropertyValue(s)));
return criteriaUpdate;
}

Yukarıdaki buildCriteriaIgnoringNullValues() methodunda yaptığımız şey de neredeyse aynı. User nesnesindeki herbir alanı kontrol edip, null olmayan alanları criteria’ya ekliyoruz.

Nullable & Updatable

Tüm bunlara ek olarak değinilmesi gereken başka bir konu da hiçbir zaman null olmasını ve/veya update edilmesini istemediğiniz alanlar(örneğin username) olabilir.

@Entity(name = "APP_USER")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, updatable = false)
private String username;
private String password; private String email; private String phone; // getters & setters}
  • nullable = false : username alanının hiçbir zaman null değeri almamasını sağlar.
  • updatable = false : hibernate tarafından oluşturulan hiçbir update sorgusuna dahil edilmemesini sağlar. (jpql veya criteria api ile mümkün)

Yukarıdaki örnekleri uygulamalı olarak görebileceğiniz github repo’suna buradan ulaşabilirsiniz.

--

--