如何在Spring Data中优雅地更新JPA实体?

72

所以我看了很多关于JPA与Spring Data的教程,这些教程在许多场合下都有不同的实现方式,我不太确定正确的方法是什么。

假设存在以下实体:

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;
}
}

我们还有一个DTO,在服务层中检索并传递给控制器/客户端。

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;
}
}

现在假设客户想要在WebUI中更改他的名称 - 那么将会有一些控制器操作,其中将更新DTO与旧ID和新名称。

现在我需要将此更新后的DTO保存到数据库中。

不幸的是,目前没有办法更新现有的客户(除非删除DB中的条目并创建一个新的带有新自动生成的ID的客户)

然而,这不可行(尤其是考虑到这样的实体可能有数百个关系),所以我脑海中有两个直接的解决方案:

  1. 为Customer类添加一个id setter- 允许设置id,然后通过相应的存储库保存Customer对象。

或者

  1. 将id字段添加到构造函数中,并且每当您想要更新客户时,始终使用旧ID创建一个新对象,但使用其他字段的新值(在本例中仅为名称)

所以我的问题是是否有一般规则如何处理这个问题?也许我解释的2种方法的缺点是什么?


8
目前没有办法更新现有客户的信息(除非在数据库中删除该条目并创建一个新的带有新自动生成的ID的客户)。此想法来源于 Say... what? - Adrian Colomitchi
如果您使用相同的ID调用save(),并且没有覆盖它,则会出现重复ID异常。如果您已正确配置Spring事务,则每当您从数据库中获取客户并仅设置所需值时,您可能会看到一些查询记录在日志中。但是,即使没有JPA,在您的场景中通常如何使用简单的SQL更新客户行?如果您没有ID,该怎么办? - AntJavaDev
我马上会添加一些澄清,感谢您的评论。 - Lukas Makor
我更新了问题,使其更加清晰明了。 - Lukas Makor
你可以删除strategy = GenerationType.AUTO,因为它是默认的。同样地,@Column(name = "id")也可以删除,因为它与id属性完全相同。 - belgoros
8个回答

138

T getById(ID id)自2.7版本起已被弃用。请改用T getReferenceById(ID id)这个方法的思想是相同的,它只返回一个引用。但是它更加自解释,因为"getById"可能不够清晰,而"getReferenceById"则更少歧义。

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

比起@Tanjim Rahman的回答,你可以使用Spring Data JPA更好地使用方法T getOne(ID id)
Customer customerToUpdate = customerRepository.getOne(id);
customerToUpdate.setName(customerDto.getName);
customerRepository.save(customerToUpdate);

更好的原因是 getOne(ID id) 只获取一个引用(代理)对象,并不从数据库中获取。在这个引用上,你可以设置任何你想要的东西,并且在save()时它将只执行一个像你期望的 SQL UPDATE 语句。相比之下,当你调用find()像在 @Tanjim Rahmans 的回答中使用 spring data JPA,它会执行一次 SQL SELECT 来从数据库中物理获取实体,而当你只进行更新操作时这是不必要的。


3
这将针对所有字段发出更新语句 update tbl set f1=v1, f2=v2, etc... where id=? - Muhammad Hewedy
3
我的测试用例中抛出了LazyInitializationException异常。 - PeMa
3
一个错误的答案怎么可能有“正确答案”?你需要id字段和目标列+值吗?那为什么不执行一次更新呢? - gorefest
5
这不正确。引用用于延迟加载(或者如果引用链接到实体,则完全不需要加载)。它与延迟更新值无关。除非有人能展示一种像这样工作的JPA实现,否则就没有证据表明会发生这种情况,而且这也不符合JPA规范(尽管我猜它也没有明确禁止这样做)。 - Kayaman
8
GetOne现已被弃用。 - anegru
显示剩余3条评论

14

如果你有ID,那么在Spring Data中你只需要定义一个更新查询

  @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);

  }

有些解决方案声称使用Spring数据并且以老派的JPA方式实现(甚至以一种带有丢失更新的方式)。


我对这个解决方案有问题,即使使用@Modifying注解,在更新后如果我再次查询对象,我仍然从Hibernate Session Cache中获取它。 - TecHunter
你的调用服务是否带有 @Transactional 注解? - gorefest
第一级缓存在提交时被刷新,并且所有修改都会在数据库中应用 - 在容器事务内 - 如果查询了一个实体,其修改尚未与数据库同步。您是否知道这一点?这可能是副作用的可靠来源。 - gorefest
问题在那里,我知道。但这并不能回答关于Spring Data仓库中的@Transactional内的@Modifying查询如何告诉Hibernate将实体视为“脏”的问题。 - TecHunter
1
也许你遇到了一个边缘情况。通常情况下,Hibernate会在“引擎盖下”检测到这一点,因为Hibernate将跟踪第一级缓存中的所有实体。如果您要更新实体,则不会刷新该实体,直到a)事务结束或b)查询该实体。您能否在测试中隔离此行为?然后,您可以将其交给Spring Data社区并请求帮助。哦,还有,您的应用程序是否带有@EnableJpaRepositories注释? - gorefest
显示剩余3条评论

2

简单的JPA更新..

Customer customer = em.find(id, Customer.class); //Consider em as JPA EntityManager
customer.setName(customerDto.getName);
em.merge(customer);

1
em.getReference会表现更好,因为它只获取ID。 - Journeycorner
4
这个问题涉及Spring Data。你的回答并没有真正帮助到问题解决。 - ichalos
@Journeycorner 这会删除在客户对象中不存在的现有数据库记录吗?我无法实现这一点。 - Pratik Ambani

2

JpaRepository 中有一个方法

getOne

目前已被弃用,建议使用

getById

所以正确的方法应该是

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

1
这更多是一个对象初始化问题,而不是JPA问题,两种方法都可以使用,你可以同时拥有它们。通常情况下,如果在实例化之前数据成员值已经准备好了,就使用构造函数参数;如果该值可以在实例化后更新,则应该使用setter。

你说得没错,不过我在想,在使用JPA时是否有一些特殊的最佳实践呢? - Lukas Makor
Hibernate是一组API的集合,它使您能够使用Java模型而不是SQL进行工作。您需要准备好您的模型,以便可以使用这些API。 - Amer Qarabsa
是的,我知道这一点,但我想知道使用其中一种方法是否存在一些缺点,或者甚至可能存在一些隐藏的陷阱。 - Lukas Makor
主要是我提到的使用setter或构造函数之间的区别,但你可以搜索更多详细答案。 - Amer Qarabsa

1
如果您需要使用DTO而不是直接使用实体,则应检索“现有”的Customer实例,并将更新的字段从DTO映射到该实例。
Customer entity = //load from DB
//map fields from DTO to entity

那么您建议从数据库加载实体,然后通过setter更新字段? - Lukas Makor
还有一个问题: 那么你会说在一般情况下,对数据库进行另一个查询的开销是可以忽略不计的吗? - Lukas Makor
是的,这就是 JPA/Hibernate 的工作方式。无论是 getOne() 还是 findById(),在更新之前你不可避免地要先加载整个实体。这是有意设计的,以确保 L2C 能够得到正确更新。当然,如果你不使用缓存,则使用本地查询或 Spring Data @Modifying 进行更新是更好的方法,但这会增加一些额外的工作量。 - Andreas Gelever

1
我遇到了这个问题!幸运的是,我找到了2种方法并理解了一些东西,但其他部分还不清楚。如果你知道,请希望有人讨论或支持。
  1. 使用RepositoryExtendJPA.save(entity)。示例:
    List<Person> person = this.PersonRepository.findById(0); person.setName("Neo"); This.PersonReository.save(person);
    这段代码更新了id=0的记录的新名称;
  2. 使用javax或spring框架中的@Transactional。将@Transactional放在类或指定函数上都可以。我在某个地方读到过,这个注释会在函数流程结束时执行“提交”操作。因此,您修改实体的所有内容都将更新到数据库。

2
如果一个人是List<Person>,那么你不能在person对象上调用setName。 - Yazan Khalaileh
是的,这是一个错误。让我们获取 per = person.get(0) << 如果 person.length !== 0 >>然后使用 setName 设置 per 的名称。此致敬礼 - Neo_

1
假设客户想在WebUI中更改名称-那么就会有一些控制器操作,其中将包含具有旧ID和新名称的更新后的DTO。
通常,您有以下工作流程:
1. 用户从服务器请求其数据并在UI中获取它们; 2. 用户更正其数据并使用已经存在的ID将其发送回服务器; 3. 在服务器上,通过用户更新的数据获得更新后的DTO,通过ID在DB中查找它(否则抛出异常),并将DTO转换为带有所有给定数据、外键等的实体; 4. 然后只需合并它,或者如果使用Spring Data,则调用save(),这将合并它(请参见此thread)。 附言:这个操作不可避免地会发出2个查询:select和update。即使您只想更新一个字段,也需要进行2个查询。但是,如果您在实体类顶部使用Hibernate专有的@DynamicUpdate注释,则可以帮助您仅将实际更改的字段包含在更新语句中,而不是所有字段。 附言:如果您不想支付第一个select语句的费用,并且希望使用Spring Data的@Modifying查询,请准备好失去与可修改实体相关的L2C缓存区域;原生更新查询的情况更糟(请参见此线程),当然还要准备好手动编写、测试并在将来维护这些查询。

当你说“使用所有给定的数据、外键等将DTO转换为实体”时,你是否正在更新你获取的实体? - Toast
在第三步,是的,您可以更新(当然是在活动事务内)通过ID获取的实体。另一个选项是不手动获取,而是使用new运算符创建一个新实体,并从DTO设置所有字段(包括ID),然后像第4步描述的那样合并它。 - Andreas Gelever

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接