如果主键是由数据库生成的,如何使用 em.merge() 插入或更新 jpa 实体?
我有一个像这样的 JPA 实体:
@Entity
@Table(name = "category")
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Integer id;
@Basic(optional = false)
@Column(name = "name")
private String name;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "category")
private Collection<ItemCategory> itemCategoryCollection;
//...
}
使用 Mysql 作为底层数据库。 “name”被设计为唯一的键。使用 Hibernate 作为 JPA 提供程序。
使用合并方法的问题是,因为 pk 是由 db 生成的,所以如果记录已经存在(名称已经存在),那么 Hibernate 将尝试将其插入到 db 中,我将得到一个唯一的键约束冲突异常,并且不执行更新 。有没有人有一个好的做法来处理这个问题?谢谢你!
PS:我的解决方法是这样的:
public void save(Category entity) {
Category existingEntity = this.find(entity.getName());
if (existingEntity == null) {
em.persist(entity);
//code to commit ...
} else {
entity.setId(existingEntity.getId());
em.merge(entity);
//code to commit ...
}
}
public Category find(String categoryName) {
try {
return (Category) getEm().createNamedQuery("Category.findByName").
setParameter("name", categoryName).getSingleResult();
} catch (NoResultException e) {
return null;
}
}
I have an JPA entity like this:
@Entity
@Table(name = "category")
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Basic(optional = false)
@Column(name = "id")
private Integer id;
@Basic(optional = false)
@Column(name = "name")
private String name;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "category")
private Collection<ItemCategory> itemCategoryCollection;
//...
}
Use Mysql as the underlying database. "name" is designed as a unique key. Use Hibernate as JPA provider.
The problem with using merge method is that because pk is generated by db, so if the record already exist (the name is already there) then Hibernate will trying inserting it to db and I will get an unique key constrain violation exception and not doing the update . Does any one have a good practice to handle that? Thank you!
P.S: my workaround is like this:
public void save(Category entity) {
Category existingEntity = this.find(entity.getName());
if (existingEntity == null) {
em.persist(entity);
//code to commit ...
} else {
entity.setId(existingEntity.getId());
em.merge(entity);
//code to commit ...
}
}
public Category find(String categoryName) {
try {
return (Category) getEm().createNamedQuery("Category.findByName").
setParameter("name", categoryName).getSingleResult();
} catch (NoResultException e) {
return null;
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
无论您是否使用生成的标识符,IMO 都无关紧要。这里的问题是,您想要在 PK 之外的某个唯一键上实现“upsert”,而 JPA 并没有真正提供对此的支持(
merge
依赖于数据库身份)。所以你有 AFAIK 2 个选择。
首先执行 INSERT 并实现一些重试机制,以防由于违反唯一约束而失败,然后查找并更新现有记录(使用新的实体管理器)。
或者,首先执行 SELECT,然后根据 SELECT 的结果插入或更新(这就是您所做的)。这可以工作,但不能 100% 保证,因为两个并发线程之间可能存在竞争条件(它们可能找不到给定类别名称的记录并尝试并行插入;最慢的线程将失败)。如果这不太可能,这可能是一个可接受的解决方案。
更新:如果您不介意使用 MySQL 专有功能,可能还有第三个奖励选项,请参阅 12.2.5.3。 INSERT ... ON DUPLICATE KEY UPDATE 语法。但从未使用 JPA 进行过测试。
Whether you're using generated identifiers or not is IMO irrelevant. The problem here is that you want to implement an "upsert" on some unique key other than the PK and JPA doesn't really provide support for that (
merge
relies on database identity).So you have AFAIK 2 options.
Either perform an INSERT first and implement some retry mechanism in case of failure because of a unique constraint violation and then find and update the existing record (using a new entity manager).
Or, perform a SELECT first and then insert or update depending on the outcome of the SELECT (this is what you did). This works but is not 100% guaranteed as you can have a race condition between two concurrent threads (they might not find a record for a given categoryName and try to insert in parallel; the slowest thread will fail). If this is unlikely, it might be an acceptable solution.
Update: There might be a 3rd bonus option if you don't mind using a MySQL proprietary feature, see 12.2.5.3. INSERT ... ON DUPLICATE KEY UPDATE Syntax. Never tested with JPA though.
我之前没有看到过这一点,所以我只是想添加一个可能的解决方案来避免进行多个查询。 版本控制。
通常用作在乐观锁定场景中检查正在更新的记录是否已过时的简单方法,还可以使用 @Version 注释的列来检查记录是否是持久的(存在于数据库中)。
这一切听起来可能很复杂,但事实并非如此。它归结为记录上的一个额外列,其值在每次更新时都会发生变化。我们在数据库中定义一个额外的列版本,如下所示:
并用
@Version
标记 Java 类中的相应字段,如下所示:@Version 注释将使 JPA 通过包含该列来使用乐观锁定的列作为任何更新语句中的条件,如下所示:
(JPA 自动执行此操作,无需您自己编写)
然后,它检查一条记录是否已更新(按预期)(在这种情况下)该对象已过时)。
我们必须确保没有人可以设置版本字段,否则会弄乱乐观锁定,但如果需要,我们可以在
version
上创建一个 getter。我们还可以在方法isPercient
中使用版本字段,该方法将检查记录是否已经在数据库中,而无需进行查询:最后,我们可以在
insertOrUpdate 中使用此方法方法:
I haven't seen this mentioned before so I just would like to add a possible solution that avoids making multiple queries. Versioning.
Normally used as a simple way to check whether a record being updated has gone stale in optimistic locking scenario's, columns annotated with @Version can also be used to check whether a record is persistent (present in the db) or not.
This all may sound complicated, but it really isn't. What it boils down to is an extra column on the record whose value changes on every update. We define an extra column version in our database like this:
And mark the corresponding field in our Java class with
@Version
like this:The @Version annotation will make JPA use this column with optimistic locking by including it as a condition in any update statements, like this:
(JPA does this automatically, no need to write it yourself)
Then afterwards it checks whether one record was updated (as expected) or not (in which case the object was stale).
We must make sure nobody can set the version field or it would mess up optimistic locking, but we can make a getter on
version
if we want. We can also use the version field in a methodisPersistent
that will check whether the record is in the DB already or not without ever making a query:Finally, we can use this method in our
insertOrUpdate
method: