JPA: update only specific fields
Asked Answered
N

9

92

Is there a way for updating only some fields of an entity object using the method save from Spring Data JPA?

For example I have a JPA entity like this:

@Entity
public class User {

  @Id
  private Long id;

  @NotNull
  private String login;

  @Id
  private String name;

  // getter / setter
  // ...
}

With its CRUD repo:

public interface UserRepository extends CrudRepository<User, Long> { }

In Spring MVC I have a controller that get an User object for update it:

@RequestMapping(value = "/rest/user", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<?> updateUser(@RequestBody User user) {

   // Assuming that user have its id and it is already stored in the database,
   // and user.login is null since I don't want to change it,
   // while user.name have the new value

   // I would update only its name while the login value should keep the value 
   // in the database
   userRepository.save(user);

   // ...
}

I know that I could load the user using findOne, then change its name and update it using save... But if I have 100 fields and I want to update 50 of them it could be very annoying change each value..

Is there no way to tell something like "skip all null values when save the object"?

Nanci answered 7/1, 2015 at 11:21 Comment(6)
No there isn't. The only correct way is to retrieve the object, update the fields and store it and that could be dynamic. If you want something else you would have to write SQL for it and do it yourself. But assuming the User only has null for the fields you don't want to save what you could do is the other way around, use the incoming User and update the fields you know haven't changed, then update that.Brassiere
Is there a why for which is better to not skip null values updating an object?Nanci
There is no reliable way of knowing what to skip, null can be a valid option for fields, then what? You could probably shoehorn something in place with ann EntityListener or something like that, but I would strongly recommend against it as it will probably lead to more problems than it solves.Brassiere
You use a JPA implementation that does that as default perhaps. The one I use (DataNucleus) only updates the fields I've changed. Would have thought that was the default for all of them ...Athallia
@NeilStockton perhaps I'm wrong, but what I would try to do is not to change values then update only them, since I never retrieve the object but it is like I create a new one setting its id... What I would get is just to skip null values in the updating operation... What you suggest it seems like the @DynamicUpdate(value = true) in Hibernate (that doesn't work in my case)Nanci
User below reference: #53130056Infancy
B
48

I had the same question and as M. Deinum points out, the answer is no, you can't use save. The main problem being that Spring Data wouldn't know what to do with nulls. Is the null value not set or is it set because it needs to be deleted?

Now judging from you question, I assume you also had the same thought that I had, which was that save would allow me to avoid manually setting all the changed values.

So is it possible to avoid all the manuel mapping then? Well, if you choose to adhere to the convention that nulls always means 'not set' and you have the original model id, then yes. You can avoid any mapping yourself by using Springs BeanUtils.

You could do the following:

  1. Read the existing object
  2. Use BeanUtils to copy values
  3. Save the object

Now, Spring's BeanUtils actual doesn't support not copying null values, so it will overwrite any values not set with null on the exiting model object. Luckily, there is a solution here:

How to ignore null values using springframework BeanUtils copyProperties?

So putting it all together you would end up with something like this

@RequestMapping(value = "/rest/user", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<?> updateUser(@RequestBody User user) {

   User existing = userRepository.read(user.getId());
   copyNonNullProperties(user, existing);
   userRepository.save(existing);

   // ...
}

public static void copyNonNullProperties(Object src, Object target) {
    BeanUtils.copyProperties(src, target, getNullPropertyNames(src));
}

public static String[] getNullPropertyNames (Object source) {
    final BeanWrapper src = new BeanWrapperImpl(source);
    java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();

    Set<String> emptyNames = new HashSet<String>();
    for(java.beans.PropertyDescriptor pd : pds) {
        Object srcValue = src.getPropertyValue(pd.getName());
        if (srcValue == null) emptyNames.add(pd.getName());
    }
    String[] result = new String[emptyNames.size()];
    return emptyNames.toArray(result);
}
Bromleigh answered 9/1, 2015 at 13:59 Comment(3)
I beleive it should be userRepository.save(existing); not #save(user);Mystique
if the datatype is integer its default is zero is updated in mysql. Its working fine for string how we can do it for other datatypes like int,char,double.Cordeelia
This solution is prone to concurrency issues or OptimisticLockException if using Version. See @arjun-nayak answer for a correct solutionVibrate
B
33

Using JPA you can do it this way.

CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaUpdate<User> criteria = builder.createCriteriaUpdate(User.class);
Root<User> root = criteria.from(User.class);
criteria.set(root.get("lastSeen"), date);
criteria.where(builder.equal(root.get("id"), user.getId()));
session.createQuery(criteria).executeUpdate();
Badminton answered 17/12, 2016 at 21:9 Comment(4)
Why is this not the accepted answer? Other solutions have possibility of race condition: if one thread updates columns A and C, and another updates B and D, this solutions will work fine, but the other solutions can result in only A+C or B+D having been updated as the second update would overwrite the earlier one's effects. Yes, of course you can add a @Version field and handle the optimisting lock exceptions. But just updating only the column you want to is much simpler.Conventicle
Thank you for your answer. It shocks me that 90% of developers (based on answer votes) do not care about concurrency issues.Vibrate
But how can this be helpful when we need to update more than few columns, lets say 50 out of 100 (and this number can change dynamically i.e. sometimes we need to update only 30 out of 100). It would mess up the code.Aggravate
@JigneshM.Khatri send an array of key value pair which needs to be updated. You can call criteria.set() for all those key and values. Or you can also iterate the JSON object as well.Badminton
G
7

You are able to write something like

@Modifying
    @Query("update StudentXGroup iSxG set iSxG.deleteStatute = 1 where iSxG.groupId = ?1")
    Integer deleteStudnetsFromDeltedGroup(Integer groupId);

Or If you want to update only the fields that were modified you can use annotation

@DynamicUpdate

Code example:

@Entity
@Table(name = "lesson", schema = "oma")
@Where(clause = "delete_statute = 0")
@DynamicUpdate
@SQLDelete(sql = "update oma.lesson set delete_statute = 1, "
        + "delete_date = CURRENT_TIMESTAMP, "
        + "delete_user = '@currentUser' "
        + "where lesson_id = ?")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
Gramme answered 12/10, 2018 at 8:6 Comment(0)
H
4

If you are reading request as JSON String, this could be done using Jackson API. Here is code below. Code compares an existing POJO Elements and create new one with updated fields. Use the new POJO to persist.

public class TestJacksonUpdate {

class Person implements Serializable {
    private static final long serialVersionUID = -7207591780123645266L;
    public String code = "1000";
    public String firstNm = "John";
    public String lastNm;
    public Integer age;
    public String comments = "Old Comments";

    @Override
    public String toString() {
        return "Person [code=" + code + ", firstNm=" + firstNm + ", lastNm=" + lastNm + ", age=" + age
                + ", comments=" + comments + "]";
    }
}

public static void main(String[] args) throws JsonProcessingException, IOException {
    TestJacksonUpdate o = new TestJacksonUpdate();

    String input = "{\"code\":\"1000\",\"lastNm\":\"Smith\",\"comments\":\"Jackson Update WOW\"}";
    Person persist = o.new Person();

    System.out.println("persist: " + persist);

    ObjectMapper mapper = new ObjectMapper();
    Person finalPerson = mapper.readerForUpdating(persist).readValue(input);

    System.out.println("Final: " + finalPerson);
}}

Final output would be, Notice only lastNm and Comments are reflecting changes.

persist: Person [code=1000, firstNm=John, lastNm=null, age=null, comments=Old Comments]
Final: Person [code=1000, firstNm=John, lastNm=Smith, age=null, comments=Jackson Update WOW]
Hardboiled answered 23/3, 2018 at 12:19 Comment(1)
Thanks. It very helpful. Addtional we'd better add @JsonIgnoreProperties(ignoreUnknown = true) on the entity class to ignore the unknow properties in json.Hooded
E
2

skip all null values when save the object

As others have pointed out, there is not straight forward solution in JPA.

But thinking out of the box you can use MapStruct for that.

This means you use the right find() method to get the object to update from the DB, overwrite only the non-null properties and then save the object.

You can use JPA as you know and just use MapStruct like this in Spring to update only the non-null properties of the object from the DB:

@Mapper(componentModel = "spring")
public interface HolidayDTOMapper {

    /**
     * Null values in the fields of the DTO will not be set as null in the target. They will be ignored instead.
     *
     * @return The target Holiday object
     */
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    Holiday updateWithNullAsNoChange(HolidayDTO holidayDTO, @MappingTarget Holiday holiday);

}

See the MapStruct docu on that for details.

You can inject the HolidayDTOMapper the same way you do it with other beans (@Autowired, Lombok,...) .

Enterprise answered 18/11, 2019 at 9:10 Comment(0)
L
1

the problem is not spring data jpa related but the jpa implementation lib that you are using related. In case of hibernate you may have a look at:

http://www.mkyong.com/hibernate/hibernate-dynamic-update-attribute-example/

Lemon answered 7/1, 2015 at 11:26 Comment(3)
Thanks for the reference.. Actually I'm using hibernate. In that post is suggested to change the dynamic-update property: I have just tried to do it using annotations but that property is not supported in JPA's @Entity annotation and Hibernate's @Entity annotation is deprecated.... Do you know the proper way to set it?Nanci
As suggested in another answer the actual way to change dynamic-update is using @DynamicUpdate annotation... I have tried but I didn't saw any change... Any suggestion?Nanci
Please understand the question, it shouldn't work for all request, it will be different for each request from the user. Dynamic Update you can specify few fields they will be skipped in every update.Ruwenzori
H
0

For long,int and other types; you can use the following code;

            if (srcValue == null|(src.getPropertyTypeDescriptor(pd.getName()).getType().equals(long.class) && srcValue.toString().equals("0")))
            emptyNames.add(pd.getName());
Hayleyhayloft answered 9/12, 2018 at 12:47 Comment(0)
G
0

import org.springframework.transaction.annotation.Transactional.

@Transactional //try to add this annotation @Modifying

Grahamgrahame answered 18/2 at 9:12 Comment(0)
E
0

To tell "skip a null value when save the object" you can use something like this:

@Modifying
@Query("""
      UPDATE User target
      SET
          target.name = COALESCE(CAST(:#{#source.name} as string), target.name)
      WHERE target.id = :#{#source.id}
""")
void updateEntity(@Param("source") User source);

the name column will be updated only when the incoming object has the corresponding data.

Eidolon answered 6/6 at 14:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.