Hibernate高级应用

本文详细介绍了Hibernate对象的三种状态:瞬时、持久和托管,以及它们之间的转换。探讨了并发问题,包括悲观锁和乐观锁的概念及实现方式。此外,还深入讲解了Hibernate的Flush操作以及批处理策略。最后,阐述了一级缓存和二级缓存的工作原理,以及如何配置和使用二级缓存EHcache。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 生命周期

Hibernate有三种状态,可以互相发生转换。

瞬时对象(transientObjects)就是刚刚new出来的对象,数据库中没有记录,也没有被Hibernate的Session管理起来,这种对象是会被GC回收的。[数据库里没有,session里没有]

持久对象(persistentObjects)经历了持久化操作的对象,数据库有记录,被Hibernate的Session管理起来。[数据库里有,session里也有]

托管对象(detachObject),数据库有记录,但没被Hibernate的Session管理起来。[数据库里有,但是session里没有]

对象转换图
在这里插入图片描述

台词:hibernate的生命周期有三种状态,临时、持久和托管。所谓临时状态就是刚刚new出来的对象的状态,临时对象并未存在于数据库也没有被session管理,此时这个对象是有可能被GC回收的,如果临时对象经历了持久化操作,如save()或saveOrUpdate()等,那么它将进入持久状态,持久对象会出现在数据库中,且被session管理起来,当然如果一开始就是查询业务,如get()、load()和iterator()等操作,那么对象将直接进入持久状态。持久对象通过delete()方法可以退回到临时状态,也可以通过close()、clear()或evict()等操作进入托管状态,托管对象在数据库中仍是存在的,但不会再被session管理了,除非再次进行持久化操作,如update()或saveOrUpdate()方法等,那么它将退回到持久状态,或者调用delete()方法退回到临时状态,否则,托管对象最终也会被GC回收。

瞬时 - 持久
在这里插入图片描述

tips:如果调用session.delete(userPojo),则userPojo回退为瞬时对象。

持久 - 托管
在这里插入图片描述

如果session关闭,或者调用clear方法,或者调用evict方法,那么对象就从持久状态,变成了托管状态,该对象在数据库中仍然存在,但是不再由session管理。
如果继续去调用session.update(userPojo),则再次利用session接管这个对象,对象从托管状态重新变成持久状态,当然前提是session没有关闭。
如果调用session.delete(userPojo),则userPojo回退为瞬时对象。

2. 并发问题

案例:如果用户A查询到某商店的IPHONE剩余100台,然后A要买两个,同时,用户B也查询到库存100台IPHONE,B要买100个,这样就有可能产生并发问题。

2.1 悲观锁

悲观锁是数据库的update锁机制[select * from emp for update],不是hibernate锁。悲观锁是在查询上上锁,可以保证A在查询数据的时候,B被阻塞,无法进行查询。

测试:这里无法测试多线程效果,我们可以通过SQL语句看出来,悲观锁已经生效。

@Test
public void retrieveByUpGrade() {
	Session session = sessionFactory.openSession();
	Transaction transaction = session.beginTransaction();
	try {
		Emp emp = (Emp)session.load(Emp.class, 1,LockOptions.UPGRADE);
		System.out.println(emp);
		transaction.commit();
	} catch (Exception e) {
		transaction.rollback();
		e.printStackTrace();
	} finally {
		session.close();
	}
}

tips:在我们load方法或者get方法上,具有锁的参数,我们选择LockOptions.UPGRADE之后,在Load数据的时候,就不会有其它线程进来捣乱了,我们也可以通过Hibernate发送的SQL来观察到,SQL是for
update的。

悲观锁缺陷:悲观锁锁的范围实在是太大了,将整个查询过程都锁住了,很浪费性能,而乐观锁就完美的解决了这种性能问题。

2.2 乐观锁

乐观锁的原理就是在数据库表中添加一个版本字段version,初始为0。读取出数据时,将此版本号version一同读出,在提交修改操作之前,再查询一遍数据库中的version,如果和第一次查询到的一致,则予以更新,并将version加1,否则认为是过期数据,由程序控制,重新发送请求。

  1. A进入数据库,读取一条数据,得到version为0,拿在手里。
  2. B进入数据库,读取相同数据,得到version为0,拿在手里。
  3. A修改该条数据,修改完提交前又查询一遍本条数据的version仍为0,版本一致,允许修改,version+1,提交。
  4. B修改该条数据,修改完提交前又查询一遍本条数据的version已经改为1,此时B手里的数据为过期数据,不允许B进行操作。
  5. B获取最新version为1,然后由程序控制重新发送请求,但重新发送请求之前还要再查询version,发现也为1,版本一致,允许修改,version再+1,提交。

乐观锁使用:在实体类中添加一个字段version,必须是private,以免外部调用,否则乐观锁失效。测试的时候,无需setVersion字段,hibernate会自动帮我们进行version的值的注入。

Emp.java中添加version字段,并设置set/get

private Integer version;

Emp.hbm.xml文件中添加version标签

<!--version要写在所有id和property之间-->
<version name="version"></version>

测试:这里无法测试多线程效果,我们可以通过SQL语句看出来,乐观锁已经生效。

@Test
public void retrieveByVersion() {
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    try {
        Emp emp = (Emp)session.load(Emp.class, 1);
        emp.setRealName("赵四");
        session.update(emp);
        transaction.commit();
    } catch (Exception e) {
        transaction.rollback();
        e.printStackTrace();
    } finally {
        session.close();
    }
}

tips:语句的条件中自动添加了version=?,说明在修改数据的时候,是需要根据version来判断的,即乐观锁生效。

3. Flush操作

flush是session中的一个方法,主要做了两件事,清理缓存(session临时区)以及执行SQL。flush操作在事物提交时或者执行查询之前,都会默认执行,也可以显示调用session.flush()来强制清理缓存。

session被创建后有临时区elementData和持久区tail,如下图红标。
在这里插入图片描述在这里插入图片描述

3.1 测试:Flush-UUID

当主键生成机制设定为UUID、并且事物隔离级别为ORACLE默认read committed时,我们debug三个连续操作:session.save()、session.flush()、transaction.commit()。

 try{
     session.save(emp);
     session.flush();
     session.commit();
 }

运行流程记录,如下

当save执行前
session临时区null
session持久区null
dirtyfalse
控制台SQL未出现
当save执行后
session临时区有值
session持久区有值
dirtytrue
控制台SQL未出现
当flush执行后
session临时区null
session持久区有值
dirtyfalse
控制台SQL出现
当commit执行后
session临时区null
session持久区有值
dirtyfalse
控制台SQL-

tips:save操作会将pojo交给session去管理,session的临时区和持久区各执一份,此时的数据是脏数据,因为是不确定的。
tips:在主键机制是uuid的情况下,save不执行SQL。
tips:flush或锁定持久区的数据,此时其他事物无法读取,flush执行SQL。

3.2 测试:Flush-Native

如果主键生成机制从UUID改为Native,则测试结果不同。

当save执行前
session临时区null
session持久区null
dirtyfalse
控制台SQL未出现
当save执行后
session临时区有值
session持久区有值
dirtytrue
控制台SQL出现
当flush执行后
session临时区null
session持久区有值
dirtyfalse
控制台SQL-
当commit执行后
session临时区null
session持久区有值
dirtyfalse
控制台SQL-

tips:native是hibernate帮我们进行自增,所以它直接就可以确定SQL内容。

3.3 批处理Flush

如果循环10万零8次save()操作,对session的临时区来说是一个不小的负载,很容易造成内存溢出,所以我们设计,每100次save()操作,flush一次session。

for(int i=0 ; i<10008 ; i++){
    UserPojo userPojo = new UserPojo();
    userPojo.setUsername("zhangsan");
    userPojo.setAge(50);
    session.save(userPojo);// 添加数据
    if(i%100==0) session.flush();// 每100条flush一次
}
session.flush();// flush最后那8条
transaction.commit();// 提交事物

4. 缓存

一条查询请求发出,会先去【一级缓存】中查询,如果有,直接返回数据,如果没有,再去【二级缓存】中查询(二级缓存开启的前提下),如果有,直接返回数据,如果还是没有,再去【数据库】中查询,查询到数据之后,再将这份数据备份到一级缓存和二级缓存中,最后返回数据。

所以说,缓存可以提高效率,但是缓存只能缓存实体对象,不能缓存其他属性。

4.1 一级缓存

一级缓存就是session级别的缓存,如果session关闭了,那么一级缓存就被清空,即一级缓存不能跨session存在。

我们在同一个session中做两遍相同的事情,会发现第二次的操作不发SQL,而是从一级缓存中获取。

分别测试get/load/list方法是否都使用一级缓存机制

@Test
public void firstLevelCacheRetrieveTest() {
	Session session = sessionFactory.openSession();
	Transaction transaction = session.beginTransaction();
	try {
     
		// 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
		Emp emp1 = (Emp) session.get(Emp.class, 1);
		// 不发SQL,从一级缓存中取值
		Emp emp2 = (Emp) session.get(Emp.class, 1);
		// 不发SQL,从一级缓存中取值
		Emp emp3 = (Emp) session.load(Emp.class, 1);
		System.out.println(emp1);
		System.out.println(emp2);
		System.out.println(emp3);

		Query query = session.createQuery("from Emp");
		// 发SQL,从数据库中取值
		List<Emp> emps1 = query.list();
		// 发SQL,从数据库中取值
		List<Emp> emps2 = query.list();
		System.out.println(emps1);
		System.out.println(emps2);

		session.flush();
		transaction.commit();
	} catch (Exception e) {
		transaction.rollback();
		e.printStackTrace();
	} finally {
		session.close();
	}
}

tips:get和load都使用一级缓存,但是list不使用。

4.1.1 一级缓存同步

在修改持久数据之后,缓存会自动同步。

查询一次数据,然后利用set修改这个数据,再查询一遍相同的数据,发SQL吗?数据会同步更改么?

@Test
public void firstLevelCacheDmlTest() {
	Session session = sessionFactory.openSession();
	Transaction transaction = session.beginTransaction();
	try {

        // 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
		Emp emp1 = (Emp) session.get(Emp.class, 1);

		// 对持久化对象的修改会直接影响数据库数据和一级缓存
		emp1.setRealName("奥特曼");

		// 不发SQL,使用一级缓存取出的值是"奥特曼",说明一级缓存同步更新了
		Emp emp2 = (Emp) session.get(Emp.class, 1);
		System.out.println(emp2);

		session.flush();
		transaction.commit();
	} catch (Exception e) {
		transaction.rollback();
		e.printStackTrace();
	} finally {
		session.close();
	}
}

4.1.2 一级缓存移除

我们可以使用session.evict()方法来逐出某个对象,或者直接使用session.clear()来清空一级缓存。

@Test
public void firstLevelCacheEvictAndClearTest() {
	Session session = sessionFactory.openSession();
	Transaction transaction = session.beginTransaction();
	try {
     
        // 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
		Emp emp1 = (Emp) session.get(Emp.class, 1);
		// 从一级缓存中逐出emp1对应的的结果
		session.evict(emp1);
        // 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
		Emp emp2 = (Emp) session.get(Emp.class, 1);
    
        // 清空一级缓存
        session.session(emp1);
        // 发SQL,从数据库中取值,并将本条数据结果放入一级缓存
		Emp emp3 = (Emp) session.get(Emp.class, 1);   
		System.out.println(emp1);
		System.out.println(emp2);
		System.out.println(emp3);
  
		session.flush();
		transaction.commit();
	} catch (Exception e) {
		transaction.rollback();
		e.printStackTrace();
	} finally {
		session.close();
	}
}

4.2 二级缓存EHcache

二级缓存是sessionFactory级别的,能被所有session共享(可以跨session),如果sessionFactory关闭,那么二级缓存随之消失。

哪些数据适合使用二级缓存?

  1. 很少被修改的数据
  2. 频繁被查询的数据
  3. 允许偶尔并发的数据
  4. 不是特别重要的数据

Hibernate自己有二级缓存的技术,但是没有第三方二级缓存技术EHcache好,EHcache支持在内存和硬盘上做缓存,支持查询缓存,而且EHcache缓存策略由自己决定,我们只需要配置即可。

在hibernate安装包/project/etc包下,找到ehcache.xml文件,拷贝在工程的classpath下,在这里可以修改二级缓存的一些配置。

Maven依赖

<!-- https://mvnrepository.com/artifact/net.sf.ehcache/ehcache-core -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.4.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-ehcache -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-ehcache</artifactId>
    <version>4.3.11.Final</version>
</dependency>

4.2.1 开启EHcache

在hibernate-cache.cfg.xml文件中手动开启二级缓存,并指定使用ehcache作为项目的二级缓存技术

<!-- 设置hibernate开启二级缓存,默认为false -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<!-- 指定使用EHcache作为hibernate的二级缓存机制 -->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>

在对应Emp.hbm.xml文件中开启EHcache,代表对这个类使用二级缓存

<cache usage="read-only"/>

tips:在的首位添加标签。

对get()进行跨session测试:注意,此时一定不要跨sessionFactory

@Test
public void secondLevelClearRetrieveTest() {
	Session session1 = sessionFactory.openSession();
	Emp emp1 = (Emp) session1.get(Emp.class, 1);
	System.out.println(emp1);
	session1.flush();
	session1.close();
	
	Session session2 = sessionFactory.openSession();
	Emp emp2 = (Emp) session2.get(Emp.class, 1);
	System.out.println(emp2);
	session2.flush();
	session2.close();
}

tips:自主测试load/iterator和list方法是否都支持二级缓存?

4.2.2 二级缓存策略

二级缓存的策略由中的usage属性来控制,常用值有两个:

1.read-only[只读策略:只能读取缓存中的数据,不能修改缓存中的数据]
2.read-write[读写策略:可以修改缓存中的数据,而且一旦修改数据库数据,那么二级缓存中的对应数据就会同步更新,如果再次查询该数据,不会发送SQL查询]

测试:read-only策略下,修改二级缓存中的数据会报错吗?

@Test
public void secondLevelReadOnlyTest() {
	Session session1 = sessionFactory.openSession();
	
	// 第一遍查询:从数据库中获取结果,并备份到一二级缓存中,发SQL
	Emp emp1 = (Emp) session1.get(Emp.class, 1);
	System.out.println(emp1);
	session1.flush();
	session1.close();
	
	// 第二遍查询:从二级缓存中获取结果,不发SQL
	Session session2 = sessionFactory.openSession();
	Transaction transaction = session2.beginTransaction();
	Emp emp2 = (Emp) session2.get(Emp.class, 1);
	System.out.println(emp2);
	
	// 将名字修改并提交,但因为二级缓存策略是read-only,数据库和二级缓存都无法被更改
	try {
		emp2.setRealName("谢兰");
	    session2.update(emp2);
	    // 
	    transaction.commit();
	} catch (HibernateException e) {
	    e.printStackTrace();
	    System.out.println("回滚了");
	    transaction.rollback();
	} finally {
		session2.flush();
	    session2.close();
	}
}

tips:如果将二级缓存策略改为"read-write",则不再报错,二级缓存和数据库中的数据会同时被更改。

4.2.3 二级缓存逐出

二级缓存也可以手动逐出,不过实际开发中不常用。

@Test
public void secondLevelEvictTest() {
	Session session1 = sessionFactory.openSession();
	// 第一遍查询:从数据库中获取结果,并备份到一二级缓存中,发SQL
	Emp emp1 = (Emp) session1.get(Emp.class, 1);
	System.out.println(emp1);
	session1.flush();
	session1.close();
	
	// 在二级缓存中逐出EHCache
	sessionFactory.getCache().evictEntity(Emp.class,1);
	
	// 第二遍查询:无法从二级缓存中获取结果,发SQL
	Session session2 = sessionFactory.openSession();
	Emp emp2 = (Emp) session2.get(Emp.class, 1);
	System.out.println(emp2);
	session1.flush();
	session1.close();
}

4.3 查询缓存

查询缓存可以缓存属性,也可以缓存实体对象的ID,一旦查询的数据库的表的数据发生改变,缓存就被清空。

查询缓存也需要在cfg文件中设置开启

<property name="hibernate.cache.use_query_cache">true</property>

测试:缓存Lang类型属性

@Test
public void secondLevelEvictTest() {
    Session session = sessionFactory.openSession();
    
    Query query1 = session.createQuery("select count(*) from Emp");
    query1.setCacheable(true);// 开启查询缓存
    Long num1 = (Long) query1.list().get(0);
    System.out.println(num1);
    
    Query query2 = session.createQuery("select count(*) from Emp");
    query2.setCacheable(true);// 开启查询缓存
    Long num2 = (Long) query2.list().get(0); 
    System.out.println(num2);
    
    session.close();
}

tips:list方法支持查询缓存,而iterator方法不支持查询缓存。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值