【问题标题】:how to use em.merge() to insert OR update for jpa entities if primary key is generated by database?如果数据库生成主键,如何使用 em.merge() 为 jpa 实体插入或更新?
【发布时间】:2011-04-29 05:22:14
【问题描述】:

我有一个这样的 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 作为底层数据库。 “名称”被设计为唯一的键。使用 Hibernate 作为 JPA 提供者。

使用merge方法的问题是因为pk是db生成的,所以如果记录已经存在(名字已经存在)那么Hibernate会尝试将它插入db,我会得到一个唯一的键约束违规异常和不做更新。有没有人有一个很好的做法来处理这个问题?谢谢!

P.S:我的解决方法是这样的:

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;

    }
}

【问题讨论】:

标签: mysql jpa auto-increment


【解决方案1】:

如果主键由数据库生成,如何使用 em.merge() 为 jpa 实体插入 OR 更新?

您是否使用生成的标识符与 IMO 无关。这里的问题是您想在 PK 以外的某个唯一键上实现“upsert”,而 JPA 并没有真正提供对此的支持(merge 依赖于数据库身份)。

所以你有 AFAIK 2 选项。

要么先执行 INSERT,并在由于违反唯一约束而失败的情况下实施某种重试机制,然后查找并更新现有记录(使用新的实体管理器)。

或者,首先执行 SELECT,然后根据 SELECT 的结果插入或更新(这就是您所做的)。这可行,但不能 100% 保证,因为您可以在两个并发线程之间存在竞争条件(它们可能找不到给定 categoryName 的记录并尝试并行插入;最慢的线程将失败)。如果这不太可能,这可能是一个可以接受的解决方案。

更新:如果您不介意使用 MySQL 专有功能,可能会有第三个奖励选项,请参阅12.2.5.3. INSERT ... ON DUPLICATE KEY UPDATE Syntax。但从未使用 JPA 进行过测试。

【讨论】:

  • 顺便说一句,您能解释一下什么是“IMO 无关”和“AFAIK 2 选项”吗?谢谢。
  • @Bobo 好吧,1. 请解释 me 为什么使用生成的标识符与问题 2. 您是否看到更多解决方案(除了使用数据库专有功能)。
  • @Bobo:你的意思是,IMO 和 AFAIK 的含义?如果这就是你的意思,那么你就去吧:IMO = In My Opinion and AFAIK = As Far As I Know。
  • 对于第三个选项,我知道它会起作用,以前用过。但问题是它迫使我编写我试图避免的本地查询。我更喜欢使用标准的 EntityManager 方法(持久化、合并等)来完成 CRUD 操作。
  • 第二个选项“执行选择和更新”不可靠(如果您有许多可能同时插入/更新的用户,您会得到很多 SQL 异常)。如果您认为机会很小,可以接受。
【解决方案2】:

我之前没有看到过这个,所以我只想添加一个可能的解决方案,避免进行多次查询。 Versioning.

通常用作检查正在更新的记录在optimistic locking 场景中是否过时的简单方法,带有@Version 注释的列也可用于检查记录是否持久(存在于数据库中) .

这一切听起来可能很复杂,但事实并非如此。它归结为记录上的一个额外列,其值在每次更新时都会发生变化。我们在数据库中定义了一个额外的列版本,如下所示:

CREATE TABLE example
(
  id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  version INT,   -- <== It really is that simple!
  value VARCHAR(255)
);

并像这样在我们的Java类中用@Version标记对应的字段:

@Entity
public class Example {
    @Id 
    @GeneratedValue
    private Integer id;

    @Version  // <-- that's the trick!
    private Integer version;

    @Column(length=255)
    private String value;
}

@Version 注释将使 JPA 通过将其作为条件包含在任何更新语句中来使 JPA 将此列与乐观锁定一起使用,如下所示:

UPDATE example 
SET value = 'Hello, World!' 
WHERE id = 23
AND version = 2  -- <-- if version has changed, update won't happen

(JPA 自动完成,无需自己编写)

然后,它会检查一条记录是否已更新(如预期的那样)(在这种情况下,对象已过时)。

我们必须确保没有人可以设置版本字段,否则会破坏乐观锁定,但如果我们愿意,我们可以在 version 上创建一个 getter。我们还可以在 isPersistent 方法中使用版本字段,该方法将检查记录是否已经在数据库中,而无需进行查询:

@Entity
public class Example {
    // ...
    /** Indicates whether this entity is present in the database. */
    public boolean isPersistent() {
        return version != null;
    }
}

最后,我们可以在insertOrUpdate方法中使用这个方法:

public insertOrUpdate(Example example) {
    if (example.isPersistent()) {
        // record is already present in the db
        // update it here
    }
    else {
        // record is not present in the db
        // insert it here
    }
}

【讨论】:

  • 忘了提...您可以在id 列上执行相同的操作,但如果它是由数据库生成的(它在给定的示例中)。我在一个项目中使用这种技术,其中 ID 不是由数据库生成的,它派上用场了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-07-30
  • 1970-01-01
  • 2012-07-22
  • 2014-03-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多