如何在 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 放在你的类或指定的函数上,两者都可以。 我在某处读到此注释在您的函数流结束时执行“提交”操作。因此,您在实体中修改的所有内容都会更新到数据库中。
【讨论】:
如果人员是 ListJpaRepository 中有一个方法
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