如何在 Spring Data 中漂亮地更新 JPA 实体?

Posted

技术标签:

【中文标题】如何在 Spring Data 中漂亮地更新 JPA 实体?【英文标题】:How to beautifully update a JPA entity in Spring Data? 【发布时间】:2017-02-06 01:20:28 【问题描述】:

因此,我查看了有关使用 Spring Data 进行 JPA 的各种教程,并且在很多情况下都做了不同的处理,我不太确定正确的方法是什么。

假设存在以下实体:

package ***Test.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 ***Test.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 保存到数据库中。

不幸的是,目前没有办法更新现有客户(除了删除数据库中的条目并使用新的自动生成的 id 创建一个新的客户)

但是,由于这是不可行的(特别是考虑到这样一个实体可能有数百个关系) - 所以我想到了 2 个直截了当的解决方案:

    为 Customer 类中的 id 创建一个设置器 - 从而允许设置 id,然后通过相应的存储库保存 Customer 对象。

    将 id 字段添加到构造函数中,每当您想要更新客户时,您始终使用旧 id 创建一个新对象,但其他字段的新值(在这种情况下只有名称)

所以我的问题是是否有一般规则如何做到这一点? 我解释的两种方法的缺点是什么?

【问题讨论】:

“所以目前没有办法更新现有客户(除了删除数据库中的条目并使用新的自动生成的 id 创建一个新的客户)”Say... what?。您是如何想到这个想法的? 如果您使用相同的 ID 调用 save() 并且您没有覆盖它,它会给您一个重复 ID 异常。如果您已正确配置 Spring Transactions,那么每当您从数据库中获取客户并简单地设置所需的值时,您会看到一些查询将被写入日志中。但是仍然让我们在没有 JPA 的情况下考虑它,在您的场景中,通常如何使用简单的 sql 更新客户行????如果你没有身份证???? 我稍后会补充一些澄清,感谢您的 cmets 我更新了问题以使其更清晰 您可以删除strategy = GenerationType.AUTO,因为它是默认的。 @Column(name = "id")id 属性完全相同。 【参考方案1】:

这更像是一个对象初始化问题而不是一个 jpa 问题,这两种方法都可以工作并且您可以同时拥有它们,通常如果数据成员值在实例化之前准备好您使用构造函数参数,如果这个value 可以在实例化后更新,您应该有一个 setter。

【讨论】:

你是对的,但是我想知道在使用 JPA 时是否有一些特殊的最佳实践 Hibernate 是一个 API 集合,它使您能够使用 java 模型而不是 sql,您需要准备好模型以便使用这些 API 是的,我知道这一点,但我想知道一种或另一种方法是否存在一些缺点,或者甚至可能存在一些隐藏的陷阱。 主要是我提到的使用 setter 或构造函数之间的区别,但是您可以搜索有关此的更多详细答案【参考方案2】:

如果您需要直接使用 DTO 而不是实体,那么您应该检索现有客户实例并将更新后的字段从 DTO 映射到该实例。

Customer entity = //load from DB
//map fields from DTO to entity

【讨论】:

所以您建议从数据库中加载实体,然后通过设置器更新字段? 再问一个问题:所以您会说对数据库进行另一次查询的开销通常可以忽略不计? 是的,这就是 JPA/Hibernate 的工作原理。无论 getOne() 还是 findById(),您都不可避免地会在更新之前加载整个实体。这是为了确保 L2C 正确更新而设计的。当然,如果您不使用缓存,则可以使用本机查询或 Spring Data @Modifying 进行更新,但要添加一些额外的工作。【参考方案3】:

简单的 JPA 更新..

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

【讨论】:

em.getReference 会执行得更好,因为它只获取 id 问题是关于 Spring Data。你的回答并没有真正的帮助。 @Journeycorner 这会删除客户对象中不可用的现有数据库记录吗?我没能做到这一点。【参考方案4】:

比@Tanjim Rahman 回答更好,您可以使用 Spring Data JPA 使用方法 T getOne(ID id)

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

更好,因为getOne(ID id) gets you only a reference(代理)对象并且不从数据库中获取它。在这个参考上,你可以设置你想要的,在save() 上它会像你期望的那样做一个 SQL UPDATE 语句。相比之下,当您调用 find() 时,就像在 @Tanjim Rahmans 中回答 spring data JPA 将执行 SQL SELECT 以从数据库中物理获取实体,当您只是更新时,您不需要。

【讨论】:

Robert Niestroj 和 Lukas Makor,我已经检查了你的答案。它肯定会比 find 性能好,因为没有从 DB 读取。但它也有安全问题。如果您不想更新 Customer 表的列怎么办。想象一下,您想更新客户名称,但不想更新客户创建日期(因为它只能通过插入来完成,从不更新)。在这些情况下,最好在更新之前获取数据库当前行。此外,如果您想使用 JPA 版本进行乐观锁定,您可能会遇到问题。 这将针对所有字段发出更新语句update tbl set f1=v1, f2=v2, etc... where id=?dba.stackexchange.com/q/176582/148282 在我的测试用例中抛出 LazyInitializationException。 错误答案如何设置“正确答案”?你有id字段和目标columen+value吗?为什么不发布更新? 这是不正确的。引用用于延迟加载(或者如果引用链接到实体,则根本不需要加载)。它与延迟更新值无关。除非有人可以展示一个像这样工作的 JPA 实现,否则没有证据表明这种情况发生了,而且它不符合 JPA 规范(尽管我猜它也没有明确禁止它)。【参考方案5】:

在 Spring Data 中,如果您有 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);

  

一些解决方案声称使用 Spring 数据并使用 JPA oldschool(即使以丢失更新的方式)。

【讨论】:

我对这个解决方案有疑问,如果我在更新后再次查询对象,我会从休眠会话缓存中获取它。即使有@Modifying 注释 你的调用服务是否带有@Transactional注解? yes :/ 最后,我看到提交已正确完成,但 Hibernate 有一个会话缓存,在保存/删除时会刷新。但是在这里它没有被刷新,我可以通过调试日志清楚地看到从会话缓存中获取实体 一级缓存在提交时刷新,所有修改都应用于数据库 - 在容器事务中 - 如果查询一个实体,其修改尚未与数据库同步。你知道吗?可能是副作用的可靠来源。 也许你遇到了边缘情况。通常,hibernate 会“在后台”检测到这一点,因为 Hibernate 将跟踪第一级缓存中的所有实体。如果您要更新一个实体,它不会被刷新,直到 a) 事务结束或 b) 查询实体。你能在测试中隔离这种行为吗?然后你可以把它交给 Spring Data 社区并寻求帮助。哦,您的 Application 是否带有 @EnableJpaRepositories 注释?【参考方案6】:

所以现在假设客户想要在 webui 中更改他的名字 - 然后会有一些控制器动作,那里会有 使用旧 ID 和新名称更新了 DTO。

通常,您有以下工作流程:

    用户从服务器请求他的数据并在 UI 中获取它们; 用户更正他的数据并将其发送回具有现有 ID 的服务器; 在服务器上,您通过用户获取更新数据的 DTO,按 ID 在 DB 中找到它(否则抛出异常)并转换 DTO -> 具有所有给定数据、外键等的实体... 然后您只需合并它,或者如果使用 Spring Data 调用 save(),这反过来将 合并它(参见 thread);

P.S.这个操作必然会产生2个查询:select和update。同样,2 个查询,即使您想更新单个字段。但是,如果您在实体类之上使用 Hibernate 专有的 @DynamicUpdate 注释,它将帮助您不将所有字段包含到更新语句中,而只包含实际更改的字段。

P.S. 如果您不想为第一个 select 语句付费而更喜欢使用 Spring Data 的 @Modifying 查询,请准备好丢失与可修改实体相关的 L2C 缓存区域;本地更新查询的情况更糟(参见thread),当然还要准备手动编写这些查询,测试它们并在将来支持它们。

【讨论】:

当您说“使用所有给定数据、外键等转换 DTO -> 实体时...”。您是否正在更新您获取的实体? 在第 3 步,是的,您可以更新(当然在活动事务中)您通过 ID 获取的实体,另一个选项不是手动获取,而是使用 new 运算符创建一个新实体并设置所有来自 DTO 的字段(包括 ID),然后按照步骤 4 的描述将其合并;【参考方案7】:

我遇到过这个问题! 幸运的是,我确定了 2 种方法 并且理解了一些事情,但其余的并不清楚。 希望知道的人讨论或支持。

    使用 RepositoryExtendJPA.save(entity)。 示例: List<Person> person = this.PersonRepository.findById(0) person.setName("Neo"); This.PersonReository.save(person); 此块代码更新了 id = 0 的记录的新名称; 使用 javax 或 spring 框架中的 @Transactional。 让@Transactional 放在你的类或指定的函数上,两者都可以。 我在某处读到此注释在您的函数流结束时执行“提交”操作。因此,您在实体中修改的所有内容都会更新到数据库中。

【讨论】:

如果人员是 List ...,则不能在人员对象上调用 setName 是的,这是错误的。让我们得到 per = person.get(0) > 之后使用 setName for per。最好的尊重【参考方案8】:

JpaRepository 中有一个方法

getOne

目前已弃用,支持

getById

所以正确的方法是

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

【讨论】:

以上是关于如何在 Spring Data 中漂亮地更新 JPA 实体?的主要内容,如果未能解决你的问题,请参考以下文章

当使用 Spring MVC for REST 时,如何让 Jackson 漂亮地打印呈现的 JSON?

如何在Spring MVC中基于http请求头启用json的动态漂亮打印?

如何在 Spring Boot 中选择性地升级依赖项? (示例案例:Spring Data)

Spring Mysql com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column

使用 spring data jpa 更新单个字段

Hibernate 5.x和Spring Data 2.x:如何在服务保存方法中更新原始对象