Hibernate 初探

最近重新学习了一遍 Hibernate 框架,感觉收益颇多,再次做个学习总结和笔记。本篇文章主要记录和总结 Hibernate 的基本配置项和 一级缓存及 Hibernate 中的对象状态。

今天这篇总结将会用 Hibernate 最新的稳定版本,数据库也用新的稳定版本,由于设计到众多依赖项,所以就构建一个 Maven 工程,pom.xml 依赖项配置如下

<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-core</artifactId>
	<version>5.2.17.Final</version>
</dependency>

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>5.1.47</version>
</dependency>

一、基本配置及概念

源自百度百科:Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,它将 POJO 与数据库表建立映射关系,是一个全自动的 ORM(Object Relative DateBase-Mapping)框架。

1. XML 基本配置

hibernate.cfg.xml 配置 (总配置文件)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd" >
<hibernate-configuration>
<session-factory>
	<!-- 数据库连接部分 -->
	<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
	<property name="connection.url">jdbc:mysql://10.22.70.2:3306/database_chao?useUnicode=true&amp;characterEncoding=utf8</property>
	<property name="connection.username">xxx</property>
	<property name="connection.password">xxx</property>

	<!-- 数据库方言 -->
	<property name="dialect">org.hibernate.dialect.MySQL55Dialect </property>

	<!-- 其他配置部分 -->
	<property name="hbm2ddl.auto">update</property>
	<property name="show_sql">true</property>
	<property name="format_sql">true</property>

	<!-- 实体类映射 -->
	<mapping resource="com/ogemray/a_primary/entity/Contact.hbm.xml" />

</session-factory>
</hibernate-configuration>

hbm.xml 配置 (实体类映射文件)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >

<hibernate-mapping package="com.ogemray.a_primary.entity">

	<class name="Contact" table="t_contact">
		<id name="id" column="id" type="integer">
			<generator class="native"></generator>
		</id>
		<property name="name" type="string"></property>
		<property name="birthday" type="date"></property>
	</class>

</hibernate-mapping>

其他字段就不过累述,下面主要讲下总配置文件中的数据库方言、建表策略和实体映射文件中的主键生成策略。

2. Hibernate 数据库方言

方言就是总配置文件中的dialect字段,因为 Hibernate 是要把 JavaBean 对象映射成数据库能够识别的数据结构,而不同的数据库有自己不同的标准,因此,Hibernate 为了更好适配各种数据库,针对每种数据库都指定了一个方言。根据指定的方言,将不同数据类型、SQL 语法转换成 Hibernate 能理解的统一的格式。如果没有对应的方言,Hibernate 是无法进行数据关系转换映射的。

具体方言:https://blog.csdn.net/jialinqiang/article/details/8679171 因为现我基本都用 MySQL 数据库,关于 MySQL 方言有三种。

MySQLMyISAMDialectMySQLInnoDBDialect 都是继承 MySQLDialectMySQLInnoDBDialect 创建的数据库引擎是 InnoDB,而 MySQLMyISAMDialect 创建的数据库引擎是 MyISAM,InnoDB 支持事务和外键,MyISAM 不支持事务和外键,但是处理速度要比 InnoDB 快,MyISAM 类型的二进制数据文件可以用做在不同操作系统中迁移。也就是可以直接从 Windows 系统拷贝到 linux 系统中使用。

InnoDB 的 autocommit 默认是打开的,即每条 SQL 语句会默认被封装成一个事务,自动提交,这样会影响速度,所以最好是把多条 SQL 语句显示放在 begincommit 之间,组成一个事务去提交。

经过探索发现,在我目前尝试的这个版本,MySQL 5.1.47 默认创建的数据库引擎是 MyISAM,MySQL 5.5 之后默认创建引擎用的是 InnoDB ,MySQL 8 已经移除了 MyISAM 引擎。

总结:一般关系型数据库用 InnoDB 。

上面三个方言是针对 MySQL 5 之前的版本。主要变化还是在于建表 SQL 语句。MySQL 5 之后跟之前还是有不小的变化。比如 varchar 在 5.0.3 及之前版本最大长度限制为 255,之后版本最大长度限制为 65535。所以为了兼容 MySQL 5 之后,Hibernate 作者新出来了几种 dialect。

但是升级到 Hibernate 5 的时候,就会发现 MySQL5InnoDBDialect,被标注过时了。不仅仅是它过时了,所有带 InnoDB 的 Dialect 都被标注过时了,在标注有 InnoDBDialect 过时的同时 新加了 MySQL55Dialect 及 MySQL57Dialect。

如果查看源码就会发现 MySQL55Dialect 与 MySQL5InnoDBDialect 源码一模一样。

毕竟 MySQL 后面把 MyISAM 引擎给淘汰了,所以 Hibernate 作者认为没有必要再分为两类。

2. 建表策略

Hibernate 会根据 hbm2ddl.auto 设置生成对应的建表策略。

3. 主键生成策略

映射配置文件里面 <id> 元素内部 提供 <generator> 元素, 有 class 属性,指定数据表主键生成策略。Hibernate 里面对于主键生成策略有 11 种之多,下面来说说这其中最常用的几种策略。

<id name="id" column="id">
	<generator class="hilo">
		<!-- 指定保存 hi 位值的表名 -->
		<param name="table">hibernate_hilo</param>
		<!-- 指定保存 hi 位置列的名 -->
		<param name="column">next_hi</param>
		<!-- 指定地位最大值 -->
		<param name="max_lo">100</param>
	</generator>
</id>

现在 Hibernate 已经将这种主键生成方式给移除了 Hibernate主键生成策略源码.png

二、SessionFactory 创建

SessionFactory sessionFactory = null;

// 创建一个 SessionFactory
final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
		.configure() // 默认加载文件名为 hibernate.cfg.xml 配置文件
		.build();
try {
	sessionFactory = new MetadataSources( registry ).buildMetadata().buildSessionFactory();
}
catch (Exception e) {
	//创建 SessionFactory 错误时销毁注册表
	StandardServiceRegistryBuilder.destroy( registry );
}

三、Hibernate 一级缓存及对象状态

在说到 Hibernate 的一级缓存,就不得不说 Hibernate 中对象的三种状态。

Hibernate 中对象有三种状态:临时状态(Transient)持久状态(Persistent)游离状态(Detached)

理解 Hibernate 对象状态对于学习至关重要,话不多说,先从一张图开始。

Hibernate中对象状态.jpg

注意下几个操作 Hibernate 中对象状态的方法,对上面的图做个简单解释。

下面通过一段代码理解三种状态之间的关系

@Test
public void testSimpleObjectState() {

	Session session = sessionFactory.openSession();
	session.beginTransaction();

	Contact contact = new Contact();
	contact.setName("LiuChao");
	contact.setBirthday(new Date());
	/**
	 * ①.[临时状态]
	 * 此时 contact 对象只是刚 new 出来的临时状态
	 */

	session.save(contact);
	/**
	 * ②. [持久化状态]
	 * 对象被保存到 session 缓存中, 并发送 insert 语句 对象被持久化到数据库
	 */

	session.getTransaction().commit();
	session.close();
	/**
	 * ③. [游离状态]
	 * session 关闭, 缓存自然被清空, 但是 contact 对象已经被保存到数据库
	 */
}

下面我们再通过一段代码来理解一下 Hibernate 的一级缓存(session 缓存) 和 对象状态之间的关系:

@Test
public void testRelationshipBetweenSessionCacheAndObjectState() {

	Session session = sessionFactory.openSession();
	session.beginTransaction();

	Contact contact = session.get(Contact.class, 1);
	System.out.println(contact.toString());
	/**
	 * ①. [持久化状态]
	 * 直接从数据库里面查询出来的, 会发送一条 select 语句, 并被保存到 session 缓存中
	 */

	Contact contact2 = session.get(Contact.class, 1);
	System.out.println(contact2.toString());
	/**
	 * 这里不会再发送 select 语句, 而是直接 session 缓存里面拿
	 */

	contact2.setName("Merry");

	contact.setName("Tony");
	/**
	 * 这里的 contact 和 contact2 是同一个对象, 都是 session 所管理的对象
	 * 可以打印对象的 hashCode进行查看
	 */

	/**
	 * 在 commit 的时候, 会拿 session 所缓存的对象跟刚查询出来的时候作对比, 不一样则发一条 update 语句更新数据库
	 * 所以上面不管经过几次更改, 只有在 commit 的时候才会进行对比和更新
	 */
	session.getTransaction().commit();
	session.close();
}

这里是我的猜测总结:当查询或者插入对象的时候,将对象缓存到 session 中(这时会缓存两份,一份用于查询返回,一份用作后期样本对比),在 session 未被关闭和清空前,不管查询多少次,都是返回 session 缓存的那个对象,是同一个对象,用 C 语言来说返回的指针指向堆上的同一个地址。在 commit 的时候,Hibernate 会拿 session 缓存中用于查询返回的对象和缓存的另一份样本对象作对比,不一样则更新数据库。

这里可以通过一个小实验得到证明:在 commit 之前直接修改返回的对象,然后再通过 session.get 方法拿取到的是修改后的,但是数据库里面是以前的,commit 之后数据库才会改变。

以上只是我的个人猜测,实际 Hibernate 里面怎么实现还是得看源码,现在能力有限看不了,等以后看了之后会对这里进行更正。

最后要提到一个方法 flush(),session 通过调用 flush() 方法能提前更新缓存中改变的对象数据到数据库,而不用等到 commit。