an assertion failure occured (this may indicate a bug in Hibernate, but is more likely due to unsafe

本文解析了Hibernate中因非线程安全的会话访问导致的AssertionFailure异常。通过实例演示了不当的会话管理如何引发问题,并提供了修复建议。

  这是在一次事务提交时遇到的异常。
        an assertion failure occured (this may indicate a bug in hibernate, but is more likely due to unsafe use of the session)
net.sf.hibernate.AssertionFailure: possible nonthreadsafe access to session
注:非possible non-threadsafe access to the session (那是另外的错误,类似但不一样)

  

        这个异常应该很多的朋友都遇到过,原因可能各不相同。但所有的异常都应该是在flush或者事务提交的过程中发生的。这一般由我们在事务开始至事务提交的过程中进行了不正确的操作导致,也会在多线程同时操作一个Session时发生,这里我们仅仅讨论单线程的情况,多线程除了线程同步外基本与此相同。


        至于具体是什么样的错误操作那?我给大家看一个例子(假设Hibernate配置正确,为保持代码简洁,不引入包及处理任何异常)


[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. SessionFactory sf = new Configuration().configure().buildSessionFactory() ;  
  2. Session s = sf.openSession();  
  3. Cat cat = new Cat();  
  4. Transaction tran = s.beginTransaction(); (1)  
  5. s.save(cat); (2)(此处同样可以为update delete)  
  6. s.evict(cat); (3)  
  7. tran.commit(); (4)  
  8. s.close();(5)  

        这就是引起此异常的典型错误。我当时就遇到了这个异常,检查代码时根本没感觉到这段代码出了问题,想当然的认为在Session上开始一个事务,通过Session将对象存入数据库,再将这个对象从Session上拆离,提交事务,这是一个很正常的流程。如果这里正常的话,那问题一定在别处。

    

        问题恰恰就在这里,我的想法也许没有错,但是一个错误的论据所引出的观点永远都不可能是正确的。因为我一直以为直接在对数据库进行操作,忘记了在我与数据库之间隔了一个Hibernate,Hibernate在为我们提供持久化服务的同时,也改变了我们对数据库的操作方式,这种方式与我们直接的数据库操作有着很多的不同,正因为我们对这种方式没有一个大致的了解造成了我们的应用并未得到预先设想的结果。


        那Hibernate的持久化机制到底有什么不同那?简单的说,Hibernate在数据库层之上实现了一个缓存区,当应用save或者update一个对象时,Hibernate并未将这个对象实际的写入数据库中,而仅仅是在缓存中根据应用的行为做了登记,在真正需要将缓存中的数据flush入数据库时才执行先前登记的所有行为。


        在实际执行的过程中,每个Session是通过几个映射和集合来维护所有与该Session建立了关联的对象以及应用对这些对象所进行的操作的,与我们这次讨论有关的有entityEntries(与Session相关联的对象的映射)、insertions(所有的插入操作集合)、deletions(删除操作集合)、updates(更新操作集合)。下面我就开始解释在最开始的例子中,Hibernate到底是怎样运作的。
        (1)生成一个事务的对象,并标记当前的Session处于事务状态(注:此时并未启动数据库级事务)。
        (2)应用使用s.save保存cat对象,这个时候Session将cat这个对象放入entityEntries,用来标记cat已经和当前的会话建立了关联,由于应用对cat做了保存的操作,Session还要在insertions中登记应用的这个插入行为(行为包括:对象引用、对象id、Session、持久化处理类)。
        (3)s.evict(cat)将cat对象从s会话中拆离,这时s会从entityEntries中将cat这个对象移出。
        (4)事务提交,需要将所有缓存flush入数据库,Session启动一个事务,并按照insert,update,……,delete的顺序提交所有之前登记的操作(注意:所有insert执行完毕后才会执行update,这里的特殊处理也可能会将你的程序搞得一团糟,如需要控制操作的执行顺序,要善于使用flush),现在cat不在entityEntries中,但在执行insert的行为时只需要访问insertions就足够了,所以此时不会有任何的异常。异常出现在插入后通知Session该对象已经插入完毕这个步骤上,这个步骤中需要将entityEntries中cat的existsInDatabase标志置为true,由于cat并不存在于entityEntries中,此时Hibernate就认为insertions和entityEntries可能因为线程安全的问题产生了不同步(也不知道Hibernate的开发者是否考虑到例子中的处理方式,如果没有的话,这也许算是一个bug吧),于是一个net.sf.hibernate.AssertionFailure就被抛出,程序终止。


        我想现在大家应该明白例子中的程序到底哪里有问题了吧,我们的错误的认为s.save会立即的执行,而将cat对象过早的与Session拆离,造成了Session的insertions和entityEntries中内容的不同步。所以我们在做此类操作时一定要清楚Hibernate什么时候会将数据flush入数据库,在未flush之前不要将已进行操作的对象从Session上拆离。


        对于这个错误的解决方法是,我们可以在(2)和(3)之间插入一个s.flush(),强制Session将缓存中的数据flush入数据库(此时Hibernate会提前启动事务,将(2)中的save登记的insert语句登记在数据库事务中,并将所有操作集合清空),这样在(4)事务提交时insertions集合就已经是空的了,即使我们拆离了cat也不会有任何的异常了。

        前面简单的介绍了一下Hibernate的flush机制和对我们程序可能带来的影响以及相应的解决方法,Hibernate的缓存机制还会在其他的方面给我们的程序带来一些意想不到的影响。看下面的例子:


[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. (name为cat表的主键)  
  2. Cat cat = new Cat();  
  3. cat.setName(“tom”);  
  4. s.save(cat);  
  5. cat.setName(“mary”);  
  6. s.update(cat);(6)  
  7. Cat littleCat = new Cat();  
  8. littleCat.setName(“tom”);  
  9. s.save(littleCat);  
  10. s.flush();  

        这个例子看起来有什么问题?估计不了解Hibernate缓存机制的人多半会说没有问题,但它也一样不能按照我们的思路正常运行,在flush过程中会产生主键冲突,可能你想问:“在save(littleCat)之前不是已经更改cat.name并已经更新了么?为什么还会发生主键冲突那?”这里的原因就是我在解释第一个例子时所提到的缓存flush顺序的问题,Hibernate按照insert,update,……,delete的顺序提交所有登记的操作,所以你的s.update(cat)虽然在程序中出现在s.save(littleCat)之前,但是在flush的过程中,所有的save都将在update之前执行,这就造成了主键冲突的发生。


        这个例子中的更改方法一样是在(6)之后加入s.flush()强制Session在保存littleCat之前更新cat的name。这样在第二次flush时就只会执行s.save(littleCat)这次登记的动作,这样就不会出现主键冲突的状况。


        再看一个例子(很奇怪的例子,但是能够说明问题)

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. Cat cat = new Cat();  
  2. cat.setName(“tom”);  
  3. s.save(cat); (7)  
  4. s.delete(cat);(8)  
  5. cat.id=null;(9)  
  6. s.save(cat);(10)  
  7. s.flush();  

        这个例子在运行时会产生异常net.sf.hibernate.HibernateException: identifier of an instance of Cat altered from 8b818e920a86f038010a86f03a9d0001 to null

        这里例子也是有关于缓存的问题,但是原因稍有不同:
      (7)和(2)的处理相同。
      (8)Session会在deletions中登记这个删除动作,同时更新entityEntries中该对象的登记状态为DELETED。
      (9)Cat类的标识符字段为id,将其置为null便于重新分配id并保存进数据库。
     (10)此时Session会首先在entityEntries查找cat对象是否曾经与Session做过关联,因为cat只改变了属性值,引用并未改变,所以会取得状态为     DELETED的那个登记对象。由于第二次保存的对象已经在当前Session中删除,save会强制Session将缓存flush才会继续,flush的过程中首先要执行最开始的save动作,在这个save中检查了cat这个对象的id是否与原来执行动作时的id相同。不幸的是,此时cat的id被赋为null,异常被抛出,程序终止(此处要注意,我们在以后的开发过程尽量不要在flush之前改变已经进行了操作的对象的id)。

这个例子中的错误也是由于缓存的延时更新造成的(当然,与不正规的使用Hibernate也有关系),处理方法有两种:
1、在(8)之后flush,这样就可以保证(10)处save将cat作为一个全新的对象进行保存。
2、删除(9),这样第二次save所引起的强制flush可以正常的执行,在数据库中插入cat对象后将其删除,然后继续第二次save重新插入cat对象,此时cat的id仍与从前一致。


        这两种方法可以根据不同的需要来使用,呵呵,总觉得好像是很不正规的方式来解决问题,但是问题本身也不够正规,只希望能够在应用开发中给大家一些帮助,不对的地方也希望各位给与指正。


   总的来说,由于Hibernate的flush处理机制,我们在一些复杂的对象更新和保存的过程中就要考虑数据库操作顺序的改变以及延时flush是否对程序的结果有影响。如果确实存在着影响,那就可以在需要保持这种操作顺序的位置加入flush强制Hibernate将缓存中记录的操作flush入数据库,这样看起来也许不太美观,但很有效。

http://blog.csdn.NET/tianlincao/article/details/6040228

<think>好的,我现在需要帮用户解决Hibernate出现的错误“HHH000099: an assertion failure occurred... null identifier”。首先,我得回忆一下这个错误的常见原因。用户之前问过关于@GenericGenerator的使用,可能他们在自定义主键生成时出了问题。 首先,这个错误通常是因为Hibernate在保存实体时,发现主键(identifier)为null。可能的原因包括主键生成策略配置错误、实体类的主键字段没有被正确初始化,或者在事务之外操作Session等。 接下来,我需要分步骤分析可能的原因。比如,检查是否忘记在主键字段上添加@GeneratedValue,或者@GenericGenerator的配置是否正确。比如用户可能配置了自定义生成器,但生成器没有正确生成ID,导致保存时主键为空。 然后,考虑数据库的ID生成策略是否支持。例如,使用AUTO_INCREMENT的数据库表,但Hibernate配置可能错误地使用了SEQUENCE,或者相反。此外,事务管理的问题也可能导致Session使用不当,比如在非事务环境下调用了save方法,导致Hibernate无法正确生成ID。 还要想到实体类的主键类型是否匹配。例如,数据库中的主键是数字类型,但实体类中定义的是String类型,导致转换失败,生成null。或者自定义生成器返回了null,这时候需要检查生成器的逻辑。 然后,用户可能需要查看堆栈跟踪,确定错误发生的具体位置。比如,是否在调用save()或flush()时触发了断言失败。如果是自定义生成器的问题,可能需要调试生成器的代码,确保其正确生成非空的ID。 另外,Hibernate的版本兼容性也是一个因素。用户可能在使用旧版的Hibernate时遇到了已知的问题,或者配置方式在新版中有所变化,导致策略失效。 最后,我需要将这些可能的原因整理成清晰的解决步骤,帮助用户逐一排查,并提供示例代码,比如如何正确配置@GenericGenerator和@GeneratedValue,如何确保事务管理正确,以及如何检查自定义生成器的实现。</think>--- **错误 `HHH000099: null identifier` 通常表示 Hibernate 在保存实体时检测到主键(identifier)为 `null`,违反了实体主键的非空约束。以下是逐步排查和解决方案:** --- ### **1. 常见原因** - **主键生成策略未生效**:未正确配置 `@GeneratedValue` 或生成器逻辑错误。 - **手动赋值主键**:代码中手动将主键设为 `null`,但未使用生成策略。 - **事务或 Session 使用不当**:在非事务环境下操作实体,或 Session 生命周期管理错误。 - **自定义生成器返回 `null`**:自定义的 `IdentifierGenerator` 未正确生成主键。 --- ### **2. 逐步排查与解决** #### **(1) 检查主键生成策略配置** 确保主键字段正确使用 `@GeneratedValue` 和生成器(如 `@GenericGenerator`): ```java @Entity public class User { @Id @GeneratedValue(generator = "uuid") // 确保与 @GenericGenerator 的 name 一致 @GenericGenerator(name = "uuid", strategy = "uuid2") // 使用正确的策略 private String id; // 主键类型需与生成策略匹配(如 UUID 对应 String) } ``` - **关键点**:`strategy` 必须有效(如 `uuid2`、`sequence`),且主键类型匹配(如 UUID 用 `String`,自增用 `Long`)。 #### **(2) 避免手动赋值主键** 如果使用生成策略,**不要手动设置主键**: ```java User user = new User(); user.setId(null); // 错误!应删除此行,由 Hibernate 自动生成 session.save(user); ``` #### **(3) 验证事务管理** 确保数据库操作在事务内执行,避免因事务未提交导致意外行为: ```java Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); // 必须开启事务 try { User user = new User(); session.save(user); tx.commit(); // 提交事务 } catch (Exception e) { tx.rollback(); // 回滚 } ``` #### **(4) 检查自定义生成器逻辑** 若使用自定义 `IdentifierGenerator`,确保其 `generate()` 方法返回非空值: ```java public class CustomIdGenerator implements IdentifierGenerator { @Override public Serializable generate(SharedSessionContractImplementor session, Object obj) { // 返回非空值,如时间戳 + 随机数 return "ID-" + System.currentTimeMillis(); } } // 实体类配置 @GenericGenerator(name = "custom-gen", strategy = "com.example.CustomIdGenerator") @GeneratedValue(generator = "custom-gen") private String id; ``` #### **(5) 检查数据库表结构** 确保数据库表的主键列允许自动生成值(如自增、序列等): - **MySQL**:主键应为 `AUTO_INCREMENT`。 - **PostgreSQL**:主键应绑定到 `SEQUENCE`。 - **示例 DDL**: ```sql CREATE TABLE user ( id VARCHAR(36) PRIMARY KEY DEFAULT (UUID()), -- 若使用 UUID -- 或 id BIGINT PRIMARY KEY AUTO_INCREMENT ); ``` --- ### **3. 其他可能性** #### **(1) 延迟写入问题** Hibernate 可能在 `flush()` 时才检测主键,检查是否过早访问了未生成的主键: ```java User user = new User(); session.save(user); System.out.println(user.getId()); // 此时可能为 null session.flush(); // 强制写入数据库,生成主键 System.out.println(user.getId()); // 此处应有值 ``` #### **(2) 版本兼容性** - **Hibernate 5+**:部分旧版策略(如 `hilo`)已废弃,改用 `@SequenceStyleGenerator`。 - **依赖冲突**:检查 `hibernate-core` 版本是否与其他 JPA 依赖冲突。 --- ### **4. 调试步骤** 1. **启用 Hibernate 日志**:在 `application.properties` 中添加: ```properties spring.jpa.show-sql=true logging.level.org.hibernate=DEBUG ``` 2. **检查生成的 SQL**:确认插入语句是否包含主键值。 3. **断点调试**:在 `IdentifierGenerator.generate()` 方法或实体构造函数中打断点,观察主键赋值过程。 --- ### **总结** 通过检查生成策略、事务管理、自定义生成器逻辑和数据库表结构,通常可以定位并解决 `null identifier` 问题。若问题仍存在,建议提供完整的主键配置代码和实体类定义进一步分析。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值