“Hibernate笔记 4:核心知识”的版本间差异

来自Wikioe
跳到导航 跳到搜索
第238行: 第238行:
对于保证“同一 Session 对象”,有两种方式:
对于保证“同一 Session 对象”,有两种方式:
# 在业务层获取到 Session, 并将 Session 作为参数传递给 DAO;
# 在业务层获取到 Session, 并将 Session 作为参数传递给 DAO;
# 使用 '''ThreadLocal''' 将业务层获取的 Session 绑定到当前线程中, 然后在 DAO 中获取 Session 的时候, 都从当前线程中获取。【⭐绑定 Session 到本地线程⭐】
# 使用 '''ThreadLocal''' 将业务层获取的 Session 绑定到当前线程中, 然后在 DAO 中获取 Session 的时候, 都从当前线程中获取。【⭐绑定 Session 到当前线程⭐】


  显然,“绑定 Session 到本地线程”是更优解。   —— 对此,Hibernate 中,对此已经提供了支持,只需配置即可。
 
  显然,“绑定 Session 到当前线程”是更优解。   —— 对此,Hibernate 中,对此已经提供了支持,只需配置即可。


而后,常用的 HibernateUtil 工具类中,获取 Session 的方式也应由 '''<syntaxhighlight lang="Java" inline>sessionFactory.openSession()</syntaxhighlight>''' 替换为使用 '''<syntaxhighlight lang="Java" inline>sessionFactory.getCurrentSession()</syntaxhighlight>'''。
而后,常用的 HibernateUtil 工具类中,获取 Session 的方式也应由 '''<syntaxhighlight lang="Java" inline>sessionFactory.openSession()</syntaxhighlight>''' 替换为使用 '''<syntaxhighlight lang="Java" inline>sessionFactory.getCurrentSession()</syntaxhighlight>'''。

2022年6月11日 (六) 04:30的版本


持久化类的编写规则

如果一个 Java 类与数据库表建立了映射关系,那么这个类称为是持久化类。 (该类有对应的映射文件与数据库表相关联)

编写规则:

  1. 持久化类衙要提供无参数构造方法
    因为在 Hibernate 的底层需要使用反射生成类的实例;
  2. 持久化类必须:属性私有,且提供公有的 get 和 set 方法
    因为在 Hibernate 底 层会将查询到的数据进行封装;
  3. 持久化类的属性要尽量使用包装类的类型。
    因为包装类和基本数据类型的默认值不同,包装类的类型语义描述更消晰而基本数据类型不容易描述;
  4. 持久化类要有一个唯一标识 OID 与数据库表的主键相对应。
    Hibernate 通过 OID 区分在内存中的持久化类;
  5. 持久化类尽量不要使用 final 进行修饰。
    因为 Hibernate 中有延迟加载的机制,这个机制中会产生【代理对象】,Hibernate 产生代理对象使用的是【字节码的增强技术】完成的,其实就是产生了当前类的一个子类对象来实现的。
    如果使用了 final 修饰待久化类,那么就不能产生子类,从而不能产生【代理对象】,那么 Hibernate 的【延迟加载】策略就会失效

什么是 OID?

OID(object identifier,“对象标识符”),是 hibernate 用于区分两个对象(持久化类)是否是同一个对象的标识。

持久化类的 OID 用于与数据库表的主键相映射,所以一般不手动指定。

OID 在对象持久化之前是 null,持久化的时候 hibernate 或者我们手动指定一个 id(被插入到数据库当做主键,在 session中 当做索引),所以,需要保证 OID 与主键的一致性,比如:类型、长度等。

主键生成策略

Hibernate 的主键依据:不同的主键类型、不同的数据库,可以有不同生成策略。

主键类型:

  1. 自然主键:带有业务含义的主键(比如:学号,工作编号)。
    • 如果不手动指定主键就提交缓存进行更新,则会报错!
    • 主键生成策略:
      1. assinged:Hibernate不维护主键,开发人员需要手动设置主键;
        • 如果不指定过元素的 <generator> 属性,则默认使用该主键生成策略。
  2. 代理主键:通过编码自动生成的,无业务含义的主键。
    • 主键生成策略:
      1. increment:(由 Hibernate 提供)自动增长
        • 适用主键:short、int、long 类型的主键;
        • 不适用与并发访问数据库;
      2. identity:(由数据库提供)自动增长
        • 适用主键:short、int、long 类型的主键;
        • 适用数据库:支持自动增长的数据库(如:DB2、 MySQL、 MS SQL Server、 Sybase 和 HypersomcSQL);(不适用于 Oracle)
      3. sequence:(由数据库提供)序列
        • 适用主键:short、int、long 类型的主键;
        • 适用数据库:支持序列的方式的数据库(如:Oracle、db2、sap、db、postgresql);
      4. native:本地策略,根据底层的数据库不同,自动选择使用 identity 还是 sequence。
        • 适用主键:short、int、long 类型的主键;
      5. uuid:Hibernate 采用 128 位的 UUID 算法来生成标识符。
        该算法能够在网络环境中生成唯一的字符串标识符,其 UUID 被编码为一个长度为 32 位的十六进制字符串
        • 适用于:字符串类型的主键。
        • 这种策略并不流行,因为字符串类型的主键比整数类型的主键占用更多的数据库空间。

生命周期(实体状态)

Session 的生命周期是以一个逻辑事物的开始和结束为边界,Session 的主要功能是提供创建、读取和删除映射的实体类的操作。

实体可能存在于三种状态:

  1. 瞬时态(transient):实体对象在内存是自由存在的(即为普通的 Java 对象),即:“该实体从未与任何持久化上下文关联过,它没有持久化标识”。
    • 特点:
      1. 不存在待久化标识 OID (相当于主键值);
      2. 尚未与 Session 关联;
      3. 数据库无对应记录;
    • 对象失去引用后将被 JVM 回收。
  2. 持久态(persistent):实体对象处于 Hibernate 框架所管理的状态,即:“该实体在数据库中有对应的记录,并拥有一个持久化标识”。
    • 特点:
      1. 存在待久化标识 OID
      2. 已与 Session 关联(加入到了 Session 缓存,且 Session 未关闭);
      3. 数据库有对应记录;
    • 持久态对象发生改变时,Hibernate 将会依据其改变自动更新。【无需 update 等操作】
      • 在一个 Session 中,对持久对象的改变不会马上对数据库进行变更,而是发生在 Transaction 终止,执行 commit 之后。
  3. 游离态(detached,脱管态):由持久态实体对象转变(关联的 Session 被关闭)而来。
    • 特点:
      1. 存在持久化标识 OID
      2. 失去了与 Session 的关联;
      3. 数据库有对应记录;
    • 游离态对象发生改变时,Hibernate 不能检测到。
      对象的引用依然有效,也可以继续被修改,但修改将不会影响到到数据库中的数据。
“瞬时态”与“游离态”的区别是“是否存在 OID”。(数据库对应记录不是重点)???

如:
   Customer customer = new Customer();   // 瞬时态
   customer.setCust_id(1);   // 脱管态
   customer.setCust_id(null);   // 瞬时态

状态转换

三种状态可以互相转换:

Hibernate 生命周期图.png

持久态的自动更新

Hibernate 会依据 persistent 状态的实体对象的属性变化,而自动更新(无需 update 等操作)数据库中相对应的记录。


注意:在一个 Session 中,对持久对象的改变不会马上对数据库进行变更,而是发生在 Transaction 终止,执行 commit 之后。

示例:

	@Test 
	// 测试:持久态对象的自动更新
	public void demo() { 
		Session session = HibernateUtils.openSession();
		Transaction tx = session.beginTransac七ion(); 
		
		// 获得持久态对象.
		Customer customer = session.get(Customer.class, 11);
		// 修改持久态对象
		customer.setCust name("主五"); 
		
		// session.update(custorner);   // 不用手动调用 update 方法就可以更新
		
		tx.commit(); 
		session.close(); 
	}
如上,更改持久态对象属性之后,不调用 update 等方法,依旧可以更新数据库记录。
持久态的自动更新,依赖于 Hibernate 的一级缓存。

Hibernate 缓存

Hibernate 的缓存分为一级缓存二级缓存,Hibernate 的这两级缓存都位于持久化层,存储的都是数据库数据的备份。

作用:减少对数据库的访问次数。

其中:

  1. “一级缓存”:Session 级别的缓存,事务范围。
    • Hibernate 的内置缓存(默认的缓存机制),不能被卸载;
  2. “二级缓存”:SessionFactory 级别的缓存,应用范围(应用中的所有 Session 共享)。
    • 默认关闭,需要手动配置;

一级缓存(Session 缓存)

Session 缓存:是一块内存空间,用来存放相互管理的对象。

作用:
1、减少访问数据库的频率。
2、保证缓存中的对象与数据库中的相关记录保持同步。【快照区】

在使用 Hibernate 查询对象的时候,首先会使用对象属性的 OID 值在 Hibernate 的一级缓存中查找:

  1. 如果找到匹配 OID 值的对象:就直接将该对象从“一级缓存”中取出使用,不会再查询数据库;
  2. 如果没有匹配 OID 值的对象:去数据库中查找相应数据,并将查询到的数据信息放置到“一级缓存”。


示例:

	@Test 
	// 测试 Hibernate 一级缓存的存在 
	public void demo() { 
		Session session = HibernateUtils.openSession(); 
		Transaction tx = session.beginTransaction(); 
	
		// 查询客户1:发出 sql 查询,并将结果放入一级缓存
		Customer customerl = session.get(Customer.class, 11);
		System.out.println(customerl); 

		// 查询客户1:从一级缓存中获取数据,而不再发出 sql 查询
		Customer customer2 = session.get(Customer.class, 11) ;
		System.out.println(customer2); 

		// 验证两次查询结果:true。(一级缓存的内容是对象的地址)
		System.out.println(customerl == customer2);
		
		tx.commit(); 
		session.close(); 
	}
从控制台输出(已配置 Hibernate 输出底层 sql 语句)可以发现,以上代码只发出了一次查询语句。

内部结构(快照区)

Hibernate 向“一级缓存”放入数据时,同时复制一份数据放入到 Hibernate 快照区中。

   当使用 commit() 方法提交事务时,同时会清理 Session 缓存,这时会使用 OID 判断 Session 缓存中的对象和快照中的对象是否一致,
   1、如果两个对象中的属性发生变化,则执行 update 语句, 将缓存的内容同步到数据库,并更新快照。
   2、如果数据一致,则不执行 update 语句。


快照区的作用就是:确保缓存的数据与数据库一致。【持久态对象的“自动更新”,便依据于此】

相关方法

Session 接口(及其他查询接口,如:Query)的相关方法会导致 Session 缓存的:增加、删除、清空。
  1. 增加
    1. 调用 Session 接口的 save()update()saveOrUpdate() 时:如果 Session 缓存中没有相应的对象,Hibernate 就会把从数据库中查询到的相应对象信息加入缓存。
    2. 调用 Session 接口的 load()get(),及 Query接口的 list()iterator() 时:如果 Session 缓存中有相应的对象,则直接取出并返回;否则,从数据库查询数据并放到缓存。
  2. 删除:调用 Session 的 evict() 方法时,会从 Session 缓存移除对应数据。
  3. 清空:调用 Session 的 clear()close() 方法时,会清空 Session 缓存。

清理时机

Session 缓存是由一系列的 Java 集合构成的:

    当一个对象被加入到 Session 缓存中,这个对象的引用就加入到了 Java 的集合中,即使以后应用程序中的引用变量不再引用该对象,只要 Session 缓存不被清空,这个对象一直处于生命周期中。

Session 缓存的清理时机:

  1. 提交前:当调用 Transaction 的 commit() 方法时,commit() 方法先清理缓存(前提是:FlushMode.COMMIT/AUTO),然后再向数据库提交事务。
  2. 查询前:当应用程序调用 Session 的 find() 或者 iterate() 时,如果缓存中的持久化对象的属性发生了变化,就会先清理缓存,以保证查询结果能反映持久化对象的最新状态。
  3. flush:当应用程序显示调用 Session 的 flush() 方法的时候。

如下图:

FlushMode Session.find()/iterate() Session.commit() Session.flush()
FlushMode.AUTO
FlushMode.COMMIT
FlushMode.NEVER
  • 清理(✔),不清理(✘)

二级缓存(SessionFactory 缓存)

SessionFactory 在 Hibernate 中起到一个缓冲区作用,Hibernate 可以将自动生成的 SQL 语句、映射数据以及某些可重复利用的的数据放在这个缓冲区中。同时它还保存了对数据库配置的所有映射关系,维护了当前的二级缓存

事务管理

在 Hibernate 中,可以通过代码来操作管理事务(如:开启、提交、回滚),此外,还可以在 Hibernate 的配置文件中对事务进行配置。 


事务管理方式:
1、代码。   ——如【Hibernate笔记 3:核心API#Transaction】中所示;
2、配置文件。

通过配置文件设置事务管理,步骤:

  1. 配置:事务隔离级别;
  2. 配置:Session 管理方式;(绑定到本地线程)
  3. HibernateUtil 工具类修改 Session 获取方式:
    	/**
    	* 获取当前线程绑定的会话:
    	*/
    	public static Session getCurrentSession(){ 
    		return sessionFactory.getCurrentSession(); 
    	}
    

关于“Session 管理方式:绑定到本地线程”

在真正进行事务管理的时候,事务控制应该在 Service 层(而非 DAO 层)实现, 并且在 Service 中调用多个 DAO 实现一个业务逻辑的操作。

所以,最主要的是:如何保证“在 Service 中开启的事务时所使用的”和“DAO 中多个操作所使用的”是同一个 Session 对象

对于保证“同一 Session 对象”,有两种方式:

  1. 在业务层获取到 Session, 并将 Session 作为参数传递给 DAO;
  2. 使用 ThreadLocal 将业务层获取的 Session 绑定到当前线程中, 然后在 DAO 中获取 Session 的时候, 都从当前线程中获取。【⭐绑定 Session 到当前线程⭐】


显然,“绑定 Session 到当前线程”是更优解。   —— 对此,Hibernate 中,对此已经提供了支持,只需配置即可。

而后,常用的 HibernateUtil 工具类中,获取 Session 的方式也应由 sessionFactory.openSession() 替换为使用 sessionFactory.getCurrentSession()

关于“事务”

事务的“ACID”:Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)

    ——见:InnoDB:InnoDB 和 ACID 模型

事务的“并发问题”:脏读、不可重复读、幻读

    ——见:InnoDB 锁机制解析#锁问题

 事务的“隔离级别”:“READ UNCOMMITTED”、“READ COMMITTED”、“REPEATABLE READ”、“SERIALIZABLE”
 
    ——见:InnoDB:InnoDB 事务模型#事务隔离级别


相关内容,见:InnoDB:InnoDB 锁和事务模型

Flush机制