解释使用 Hibernate 映射自动递增复合 id 序列的行为

Posted

技术标签:

【中文标题】解释使用 Hibernate 映射自动递增复合 id 序列的行为【英文标题】:Explain behaviors in mapping auto incremented composite id sequence with Hibernate 【发布时间】:2015-09-30 11:05:18 【问题描述】:

我有一张桌子

CREATE TABLE `SomeEntity` (
     `id` int(11) NOT NULL AUTO_INCREMENT,
     `subid` int(11) NOT NULL DEFAULT '0',
     PRIMARY KEY (`id`,`subid`),

我有一个实体类,其中有一个自动增量字段。我想在它被持久化时读取分配给它的自动增量 id

getter 的注解如下

  private long id;
   private int subid;
  @Id
  @GeneratedValue **//How do i correct this to have multiple rows with same id and different subid**
  @Column(name = "id")
  public long getId() 
    return id;
  

  @Id
  @Column(name = "subid")
  public int getSubid() 
    return subid;
  

我想拥有实体

id 1 subid 0 
id 1 subid 1
id 1 subid 2
id 2 subid 0

subid 在数据库中默认为 0,我在更新该行时以编程方式递增它。 我尝试了this SO post中的解决方案 JPA - Returning an auto generated id after persist()

 @Transactional
  @Override
  public void daoSaveEntity(SomeEntity entity) 
    entityManager.persist(entity);
  

现在在此事务之外,我正在尝试获取分配的自动增量 ID

      @Override
      public long serviceSaveEntity(SomeEntity entity) 
        dao.daoSaveEntity(entity);
        return entity.getId();
      

我从网络服务调用它

  @POST
  @Produces(MediaType.APPLICATION_JSON)
  @Consumes(MediaType.APPLICATION_JSON)
  public Response createEntity(SomeEntity entity) 

更新方法如下

 @Transactional
  public void updateReportJob(SomeEntity someEntity) 

    Query query =
        entityManager
.createQuery("UPDATE SomeEntity SET state=:newState WHERE id = :id");
    query.setParameter("newState","PASSIVE");
    query.setParameter("id", id);
    query.executeUpdate();
    double rand = Math.random();
    int i = (int) (rand * 300);
    try 
      Thread.sleep(i);  //only to simulate concurrency issues
     catch (InterruptedException e) 
      e.printStackTrace();
    
    List<Integer> resList =
        entityManager.createQuery("select max(subid) from SomeEntity WHERE id = :id")
            .setParameter("id", jobId).getResultList();
    // Increment old subid by 1
    int subid = resList.get(0);
    SomeEntity.setsubid(subid + 1);
    SomeEntity.setState("ACTIVE");
    // entityManager.merge(SomeEntity);
    entityManager.persist(SomeEntity);
  

我从 N 个线程发送 N 个并发更新,用于 ID 为 1 和以下几个其他属性的实体

SomeEnity 实体 = 新的 SomeEntity();

 entity.setId(1);
  long num = Thread.currentThread().getId();
  entity.setFieldOne("FieldOne" + num);
  entity.setFieldTwo("" + i);
  entity.setFieldThree("" + j);
  i++;
  j++;

案例 1,id 上带有 @Id,subid 上带有 @Id 注释,更新中带有 `entityManager.persist' 当我运行 300 个线程时,一些因连接异常“连接过多”而失败,数据库状态为

   id 1 subid 0 
   id 1 subid 1
   id 1 subid 2
   ..  ....
   id 1 subid 150

subid 始终是递增的,竞争条件只是因为竞争条件而未定义的 ACTIVE 条件

案例 2,id 上带有 @Id,subid 上带有 @Id 注释,并且 `entityManager.merge' 在更新中

id 1 subid 0 ID 1 子 ID 0 ID 2 子 ID 0 ………… id 151 subid 0 (也许只是巧合,比案例 1 多一个线程成功?)

案例 3 使用 @GeneratedValue@Id 在 id 和 **NO @Id 在 subid 上注释和 entityManager.persist 在更新** 异常 -- 传递给持久化的分离实体

案例 3 使用 @GeneratedValue@Id 在 id 和 **NO @Id 在 subid 和 entityManager.merge 上的注释更新** 如果按顺序运行更新,则数据库状态为

id 1 subid 0

下次更新后

id 1 subid 1

每次更新后更新同一行(导致一次只更新一行)

id 1 subid 2

案例 4 与案例 3 相同,同时更新 如果同时运行(有 300 个线程),我会得到以下异常

 org.hibernate.HibernateException: More than one row with the given identifier was found: 1

数据库状态是 id 1 subid 2(只有一个线程会成功,但由于竞争条件将 subid 从 0 更新为 2 )

案例 5 在 id 上使用 @GeneratedValue@Id,在 subid 上使用 @Id 注释 创建也因 subid org.hibernate.PropertyAccessException 失败:调用 SomeEntity.id 的 setter 时发生 IllegalArgumentException

请解释原因。从我知道的方法的 javadoc 中

persist - 使实例受管理和持久化。

merge - 将给定实体的状态合并到当前持久化上下文中。

我的问题更多是关于 hibernate 如何管理注释。 为什么在情况 3 会话尚未关闭的情况下会出现分离实体异常? 为什么在案例 5 中会出现 IllegalArgumentException?

我正在使用休眠 3.6 mysql 5 和 Spring 4 另外请提出一种实现这种增量 id 和 subid 的方法。(使用自定义 SelectGenerator ,带有演示实现或任何其他方式,无需进行列连接)

【问题讨论】:

更新了问题。id 是一个自动增量列,并且在没有 @GeneratedValue 的情况下与 hibernate 3.6.5 一起工作很好,只是在持久化 id 后它不会反映在对象中 它可能不会在持久化后立即发生,但它在 @Transactional 方法中(如链接帖子中所述),因此该值将反映在事务之外。经过几次尝试后,我发现它是由于复合主键而发生 因为 id 是自动递增的,所以不能有多个具有相同 id 的行,数据库将不允许您为该字段设置值。我认为问题不完整或没有正确提及。 见案例 1 。如果没有 GeneratedValue 注释,它允许有多个具有相同 ID 的行,即使它在数据库中是自动递增的,因为主键是 (id,subid) 【参考方案1】:

由于 id 字段已经是唯一的并且自动递增,因此在这种情况下您不需要复合 ID,因此您的实体可以如下所示:

@Id
@Column(name = "id")
public long getId() 
    return id;


@Column(name = "subid")
public int getSubid() 
    return subid;

可以使用实体管理器通过 id 获取实体:

entityManager.find(MyEntity.class, entityId); 

或者您可以使用同时采用 idsubid 的查询来获取实体:

MyEntity myEntity = entityManager.createTypeQuery("select me from MyEntity where id = :id and subid = :subid", MyEntity.class)
    .setParameter("id", entityId) 
    .setParameter("subid", entitySubId) 
    .getSingleResult();

Hibernate 还有一个SelectGenerator 可以从数据库列中获取 id,这在数据库使用触发器生成 id 时很有用。

不幸的是,它不适用于复合 id,因此您编写了自己的扩展 SelectGenerator 或使用单个字符串 id_sub_id 列将 id 和子 id 组合成单个 VARCHAR 列:

'1-0'
'1-1'
'2-0'
'2-1' 

您必须编写一个数据库触发器来使用特定于数据库的存储过程更新两列,并将这两列聚合到 VARCHAR 中。然后,您使用标准 SelectGenerator 将聚合列映射到字符串字段:

@Id
@Column(name = "id_sub_id")
@GeneratedValue( strategy = "trigger" )
@GenericGenerator( 
    name="trigger", strategy="org.hibernate.id.SelectGenerator",
    parameters = 
        @Parameter( name="keys", value="id_sub_id" )
    
)
public String getId() 
    return id;

【讨论】:

是的,我的问题中的注释不正确,但是我如何更正它以使一个 id 具有多个子 ID。每次更新时我都想增加子 ID 您能否提供一些有关实现扩展 SelectGenerator 或指向任何链接的详细信息。我发现拥有这里提到的自定义身份生成器相当简单 supportmycode.com/2014/08/22/custom-id-generator-in-hibernate 。我也可以有两个自定义 id 生成器对于 id 和 subid。不是这样吗? 哦..我没有找到 SelectGenerator 的任何示例实现。通过查看它的源代码,我假设我必须覆盖 getResult 。我仍在搜索所有样板代码细节以实现一个简单的逻辑,如果id 为 0 然后 id++ version=0 否则 id 保持不变 version++。假设 id 在创建调用中为 0 是安全的 在这里***.com/questions/5003596/… 我发现复合键的一部分可以具有 GeneratedValue 所以如果我只有一个用于 id 字段的 IdentifierGenerator 以及 subid 上的 @Id 那么它应该可以工作。 需要同步数据库中的id序列分配。在 Java 中执行此操作将无法跨多个 JVM 节点工作。【参考方案2】:

假设我有一些具有 ID 和版本的书籍。ID 属于一本书 它可以有许多更新版本,最新版本是当前版本 最常被查询的版本。什么会更好 方法?我应该使用一对多映射

请参阅下面的设计大纲以了解应如何正确映射:

保存书:

@Transactional
public void saveBook(Book book) 
    em.persist(book);

保存书籍版本:

@Transactional
public void saveBookVersion(BookVersion bookVersion) 
    Book book = em.find(Book.class, bookVersion.getBook().getId());
    bookVersion.setBook(book);
    book.setLatestBookVersion(bookVersion);
    em.persist(bookVersion);

检索最新图书版本:

@Transactional(readOnly=true)
public Book getLatestBookVersion(Long bookId) 
   // it is enough if lastestBookVersion is loaded eagerly
   return em.find(Book.class, bookId);

上述逻辑模型的表架构映射:

【讨论】:

BookVersion 表中的 ID 是什么?是版本 ID 还是书籍 ID(书籍的主键)。您能否将书籍和书籍版本的表架构添加到您的答案中? 如果是书本 id,那么生成版本 id 将再次出现同样的问题(如您的回答中的 DateIssue)。版本 id 也是连续的 BookVersion 实体/表的id。为每个实体/表设置 id 通常是一个好习惯。 我建议您仅将 id 用作内部实体/表标识符,而不将其用作“业务”属性,例如,如果书籍版本有某个数字,我会将其定义为单独的属性例如 BookVersion.versionNumber。遵循此做法可帮助您保持映射简洁明了。 BookVersion 表中的id不是自增的吧?【参考方案3】:

只是因为没有人提到这个。

我有一张桌子

CREATE TABLE `SomeEntity` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`subid` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`,`subid`),

我有一个实体类,里面有一个自动增量字段。

[..]

subid 在数据库中默认为 0,我在更新该行时以编程方式递增它。

对我来说,这听起来很像版本控制。

其实有一个注解可以做你想做的事,而不需要你写任何代码:

@Version

一个版本化的实体用@Version注解标记, 如下代码sn-p所示:

public class User 
    @ID Integer id;
    @Version Integer version;

及其对应的数据库模式有一个版本列, 例如由以下 SQL 语句创建的:

CREATE TABLE `User` (
    `id` NUMBER NOT NULL, 
    `version` NUMBER,
    PRIMARY KEY (`id`)
);

版本属性可以是 int、short、long 或 timestamp。 事务成功提交时递增。 这会产生如下 SQL 操作:

UPDATE User SET ..., version = version + 1
WHERE id = ? AND version = readVersion

来源:blogs.oracle.com强调我的

我稍微修改了这个例子。我建议您将框类型 IntegerLong 等用于数据库字段。即使是id,即NOT NULL,在您第一次保存实体之前,也会为空。我发现使用框类型可以明确表明这些字段可以并且将是null

【讨论】:

以上是关于解释使用 Hibernate 映射自动递增复合 id 序列的行为的主要内容,如果未能解决你的问题,请参考以下文章

使用 Hibernate 注释映射 PostgreSQL 串行类型

Hibernate复合主键映射

Hibernate复合主键映射

hibernate 复合主键映射

如何用 Hibernate 映射用户数据类型(复合类型)

Hibernate 不填充 AUTO_INCREMENT 列作为复合 PK、错误或反功能的一部分?