72

So I have looked at various tutorials about JPA with Spring Data and this has been done different on many occasions and I am no quite sure what the correct approach is.

Assume there is the follwing entity:

package stackoverflowTest.dao;

import javax.persistence.*;

@Entity
@Table(name = "customers")
public class Customer {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private long id;

@Column(name = "name")
private String name;

public Customer(String name) {
    this.name = name;
}

public Customer() {
}

public long getId() {
    return id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}
}

We also have a DTO which is retrieved in the service layer and then handed to the controller/client side.

package stackoverflowTest.dto;

public class CustomerDto {

private long id;
private String name;

public CustomerDto(long id, String name) {
    this.id = id;
    this.name = name;
}

public long getId() {
    return id;
}

public void setId(long id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}
}

So now assume the Customer wants to change his name in the webui - then there will be some controller action, where there will be the updated DTO with the old ID and the new name.

Now I have to save this updated DTO to the database.

Unluckily currently there is no way to update an existing customer (except than deleting the entry in the DB and creating a new Cusomter with a new auto-generated id)

However as this is not feasible (especially considering such an entity could have hundreds of relations potentially) - so there come 2 straight forward solutions to my mind:

  1. make a setter for the id in the Customer class - and thus allow setting of the id and then save the Customer object via the corresponding repository.

or

  1. add the id field to the constructor and whenever you want to update a customer you always create a new object with the old id, but the new values for the other fields (in this case only the name)

So my question is wether there is a general rule how to do this? Any maybe what the drawbacks of the 2 methods I explained are?

Lukas Makor
  • 1,947
  • 2
  • 17
  • 24
  • 7
    "So currently there is no way to update an existing customer (except than deleting the entry in the DB and createing a new Cusomter with a new auto-generated id)" [Say... what?](http://www.objectdb.com/java/jpa/persistence/update). How did you get to this idea? – Adrian Colomitchi Sep 28 '16 at 07:49
  • if you are calling `save()` with the same ID and you havent overridden it, it will give you a Duplicate ID exception. If you have configured Spring Transactions properly , then whenever you have fetched a customer from the DB and simply setting the values you want, you could see that some queries will be written out in the logs. But still lets consider it without JPA , in your scenarios generally how you could update a customer row with simple sql ???? if you dont have the ID???? – AntJavaDev Sep 28 '16 at 07:55
  • I will add some clarfications in a moment, thanks for your comments – Lukas Makor Sep 28 '16 at 07:57
  • I updated the question to make it somewhat clearer – Lukas Makor Sep 28 '16 at 08:04
  • You can remove `strategy = GenerationType.AUTO`as it is by default. The same for `@Column(name = "id")` as it is exactly the same as `id` attribute. – belgoros Aug 17 '20 at 14:14

8 Answers8

138

T getById(ID id) has been deprecated since 2.7. Please use T getReferenceById(ID id) instead. The idea is the same, it returns a reference only. But it is more self-explained, as "getById" maybe unclear and "getReferenceById" would be less ambiguous.

Customer customerToUpdate = customerRepository.getReferenceById(id);
customerToUpdate.setName(customerDto.getName);
customerRepository.save(customerToUpdate);

Even better then @Tanjim Rahman answer you can using Spring Data JPA use the method T getOne(ID id)

Customer customerToUpdate = customerRepository.getOne(id);
customerToUpdate.setName(customerDto.getName);
customerRepository.save(customerToUpdate);

Is's better because getOne(ID id) gets you only a reference (proxy) object and does not fetch it from the DB. On this reference you can set what you want and on save() it will do just an SQL UPDATE statement like you expect it. In comparsion when you call find() like in @Tanjim Rahmans answer spring data JPA will do an SQL SELECT to physically fetch the entity from the DB, which you dont need, when you are just updating.

siumi
  • 3
  • 4
Robert Niestroj
  • 15,299
  • 14
  • 76
  • 119
  • 2
    Robert Niestroj and Lukas Makor, I have checked your answer. It will definitely works fine than find for performance because no read from DB. But also it has a security concern. What if you don't want to update a column of Customer table. Imagine you want to update customer name but you don't wanna update customer creation date(As it will do only by insert and never update). In these cases it would be better to fetch DB current row before update. Also if you want to use JPA version for optimistic locking, you may face problem. – Esty Oct 02 '16 at 06:27
  • 3
    This will issue an update statements against all fields `update tbl set f1=v1, f2=v2, etc... where id=?` https://dba.stackexchange.com/q/176582/148282 – Muhammad Hewedy Apr 03 '18 at 11:00
  • 3
    Throws LazyInitializationException in my test case. – PeMa Nov 14 '18 at 14:02
  • 3
    How can a false answer have the "correct answer" set? you have the id field and the destination columen+value? Why don't you issue an update? – gorefest Feb 14 '19 at 12:18
  • 5
    This isn't correct. The reference is used for lazy loading (or if reference is linked to an entity, it doesn't need to be loaded at all). It has nothing to do with lazily updating values. Unless someone can show a JPA implementation that works like this, there's no evidence of this happening, and it's not according to JPA spec (although I guess it doesn't expressly forbid it either). – Kayaman Mar 10 '21 at 18:48
  • 1
    As Kayaman said, this isn't correct. It is true that getOne() will fetch a proxy first, but as soon as you call a set on this proxy it will load the entity from database. So the question remains, when should you use getOne()? A good case is when you are creating child on a @ManytoOne side of relation. In this case you can fetch only a proxy of parent and set it on child. – runtime_expection May 06 '21 at 11:58
  • 8
    GetOne is now deprecated – anegru Jul 28 '21 at 11:29
  • 1
    getOne() got replaced by getReferenceById() [Spring Docs - JpaRepository](https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html#getOne-ID-) – anme98 Nov 10 '22 at 07:52
14

In Spring Data you simply define an update query if you have the ID

  @Repository
  public interface CustomerRepository extends JpaRepository<Customer , Long> {

     @Query("update Customer c set c.name = :name WHERE c.id = :customerId")
     void setCustomerName(@Param("customerId") Long id, @Param("name") String name);

  }

Some solutions claim to use Spring data and do JPA oldschool (even in a manner with lost updates) instead.

Amir Kirsh
  • 12,564
  • 41
  • 74
gorefest
  • 849
  • 8
  • 22
  • I have an issue with this solution, if I query the object again after this update I get it from Hiberate Session Cache. even with the `@Modifying` annotation – TecHunter Apr 01 '19 at 14:44
  • Does your calling service carry the @Transactional Annotation? – gorefest Apr 03 '19 at 05:44
  • yes :/ in the end, I saw that commit is done properly but Hibernate has a session cache which is refreshed when saving/deleting. But here it doesn't get refreshed and I can clearly see with the debug logs the get entity from session cache – TecHunter Apr 03 '19 at 20:25
  • The first level cache is flushed on commit and all modifications are applied on the database - within the container transaction -, if an entity is queried, whose modifications have not been synced with the db yet. Are you aware of this? Can be a solid source of side effects. – gorefest Apr 05 '19 at 05:58
  • the problem is there, I know. but it doesn't answer the question about how a `@Modifying` query within a `@Transactional` from Spring Data repo can tell hibernate to consider entities 'dirty'? – TecHunter Apr 05 '19 at 07:58
  • 1
    Maybe you have hit a fringe situation. Normally hibernate detects this "under the hood", since Hibernate will keep track over all entities in the first level cache. If you are going update an entity, it won't be flushed until a) the transaction is over or b) the entity is queried. Are you able to isolate this behaviour in a test? Then you could hand it over to the Spring Data community and ask for assistance. Oh, and does you Application carry the @EnableJpaRepositories annotation? – gorefest Apr 12 '19 at 06:50
  • as we are short on time I stopped investigating this and moved to a hibernate save entity solution so I cannot provide a test case. But yes we have a `@EnableJpaRepository` – TecHunter Apr 12 '19 at 07:55
  • When using @Query annotation, first level cache is not updated (hibernate is not able to link your query to the modified entity). You can detach it first to force hibernate not using the first level cache and get it from DB instead. – Matt Jul 15 '19 at 12:14
2

Simple JPA update..

Customer customer = em.find(id, Customer.class); //Consider em as JPA EntityManager
customer.setName(customerDto.getName);
em.merge(customer);
Esty
  • 1,882
  • 3
  • 17
  • 36
2

There is a method in JpaRepository

getOne

It is deprecated at the moment in favor of

getById

So correct approach would be

Customer customerToUpdate = customerRepository.getById(id);
customerToUpdate.setName(customerDto.getName);
customerRepository.save(customerToUpdate);
santik
  • 346
  • 1
  • 12
1

This is more an object initialzation question more than a jpa question, both methods work and you can have both of them at the same time , usually if the data member value is ready before the instantiation you use the constructor parameters, if this value could be updated after the instantiation you should have a setter.

Amer Qarabsa
  • 6,412
  • 3
  • 20
  • 43
  • you are right, however I was wondering if there are some special best practices when working with JPA – Lukas Makor Sep 28 '16 at 08:01
  • Hibernate is a collection of API which enables you to work with java models instead of sql, you need to have your models ready so you can use these API's – Amer Qarabsa Sep 28 '16 at 08:05
  • Yes i know this, but I was wondering wether there are some downsides to one or another approach, or maybe even some hidden pitfalls. – Lukas Makor Sep 28 '16 at 08:08
  • Mainly the difference between using setters or constructors as i mentioned , yet you can search for more details answers regarding this – Amer Qarabsa Sep 28 '16 at 08:14
1

If you need to work with DTOs rather than entities directly then you should retrieve the existing Customer instance and map the updated fields from the DTO to that.

Customer entity = //load from DB
//map fields from DTO to entity
Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • so you suggest loading the entity from the DB and then updating the fields via setters? – Lukas Makor Sep 28 '16 at 08:09
  • just one more question: so you would say that the overhead of doing another query against the DB is negligible in general? – Lukas Makor Sep 28 '16 at 08:12
  • yes, this is how JPA/Hibernate works.. You inevitably load the whole entity before updates, regardless getOne() or findById(). This is by design to ensure that L2C is updated properly. Surely, if you do not use cache, updating with a native query or Spring Data @Modifying is the way to go, but add you some extra job. – Andreas Gelever Jul 26 '19 at 06:33
1

So now assume the Customer wants to change his name in the webui - then there will be some controller action, where there will be the updated DTO with the old ID and the new name.

Normally, you have the following workflow:

  1. User requests his data from server and obtains them in UI;
  2. User corrects his data and sends it back to server with already present ID;
  3. On server you obtain DTO with updated data by user, find it in DB by ID (otherwise throw exception) and transform DTO -> Entity with all given data, foreign keys, etc...
  4. Then you just merge it, or if using Spring Data invoke save(), which in turn will merge it (see this thread);

P.S. This operation will inevitably issue 2 queries: select and update. Again, 2 queries, even if you wanna update a single field. However, if you utilize Hibernate's proprietary @DynamicUpdate annotation on top of entity class, it will help you not to include into update statement all the fields, but only those that actually changed.

P.S. If you do not wanna pay for first select statement and prefer to use Spring Data's @Modifying query, be prepared to lose L2C cache region related to modifiable entity; even worse situation with native update queries (see this thread) and also of course be prepared to write those queries manually, test them and support them in the future.

Andreas Gelever
  • 1,736
  • 3
  • 19
  • 25
  • When you say "transform DTO -> Entity with all given data, foreign keys, etc...". Are you updating the entity you fetched? – Toast Apr 01 '20 at 20:26
  • At step 3, yes you can update (within an active transaction of course) the entity you fetched by ID, another option is not to fetch manually, but create a new entity with new operator and set all the fields from DTO (including ID) and then merge it as step 4 describes; – Andreas Gelever Apr 02 '20 at 05:04
1

I have encountered this issue!
Luckily, I determine 2 ways and understand some things but the rest is not clear.
Hope someone discuss or support if you know.

  1. Use RepositoryExtendJPA.save(entity).
    Example:
    List<Person> person = this.PersonRepository.findById(0) person.setName("Neo"); This.PersonReository.save(person);
    this block code updated new name for record which has id = 0;
  2. Use @Transactional from javax or spring framework.
    Let put @Transactional upon your class or specified function, both are ok.
    I read at somewhere that this annotation do a "commit" action at the end your function flow. So, every things you modified at entity would be updated to database.
Neo_
  • 107
  • 1
  • 4