Hibernate是一个关于java方面的ORM(Object/Relation Mapping,对象关系映射)框架。
ORM的思想:主要实现的功能就是将关系数据库中表的记录应映射成对象,以对象的形式展现,开发者可以把数据库的操作转化为对对象的操作,其实底层还是要向数据库发送SQL,所以说到底他还是对JDBC的封装。
元数据文件:用数据描述数据,这里是指用来描述对象和数据库表的对象关系的文件,通常使用xml文件进行操作。
框架的开发者文档永远都是自学的材料,所以在开头先将之前上传到github上文档贴上来,这个是Hibernate5.2.4开发者文档,渲染效果不太好,但好歹不影响阅读,语法和用法大同小异,本地的文档位置为:hibernate-release-5.2.4.Final\documentation\userguide\html_single\Hibernate_User_Guide.html
1. IDEA中创建Hibernate_HelloWord
在IDEA中创建Module时,选中Hibernate,然后再勾选下面的Create default hibernate configuration and main class
,这样会在创建Mdule的时候自动在src文件下创建hibernate的配置文件:hibernate.cfg.xml(当然这一步可能在创建项目时忘记勾选,那么可以通过Project Structure ->Modules -> 选中Module的Hibernate插入,路径选择src即可手动配置)。
除了上面的配置文件还需要有映射文件:*.hbm.xml,也就是上面所讲的元数据文件,在IDEA中,可以通过数据库的表自动创建ORM的配置文件,这里首先配置数据库(窗口右下角的的窗口标志–>Database–>添加对应的数据源Data Source–>填写地址、用户名、密码等信息进行连接,如果没有驱动下面有提示你下载),然后同样调出左侧persistence的窗口,选择相对的项目,右击Generate Persistence Mapping -> By Database Schema,填写相应的信息,注意红色部分:
然后IDEA不仅将会帮助我们创建*.hbm.xml的映射文件,而且会自动创建对应的实体类,但是在创建好的映射文件中并没有连接到数据源,点击进入后,在报错处Alt+Enter组合键调出提示选择Assign Data Sources即可,然后在右侧的Data Source空格栏处点击分配数据源即可(只有在右侧的空格栏点击有效)
【注意】后来发现学习过程中遇到这样的场景,先创建实体类,然后根据实体类生成映射文件,这个问题在Eclipse插件可以自动生成,但是在IDEA中还不知道怎么解决,只有手动创建,这种需求一般在实际开发中不常有。
经过上面的前戏,呸,预热工作以后,我们进入正题:
1.创建一个实体类Bean
package com.hhu.hibernate;
/**
* Created by WeiguoLiu on 2018/3/1.
*/
public class Hibernate {
private Integer id;
private String name;
private String version;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Hibernate(String name, String version) {
this.name = name;
this.version = version;
}
public Hibernate() {
}
}
2.写上面实体类的对象关系映射文件(这里对其中的对象和关系做一个简单的说明,对象在这个里面就是指的java实现的具体的Bean的实体类以及其属性,而关系则是指数据库或者数据表[常用的关系型数据库用的比较多]以及其字段):Hibernate.hbm.xml,这个在IDEA中真的是要自己手写:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<!--指明实体类和数据库中表的对应关系-->
<class name="com.hhu.hibernate.Hibernate" table="HIBERNATE">
<!--设置主键,name表示实体类Bean的属性名-->
<id name="id" type="java.lang.Integer">
<!--指定数据表中的列名-->
<column name="ID"/>
<!--指定主键生成策略,native表示用数据库本地生成策略方式,比如Mysql会自动使用自增的方式生成主键-->
<generator class="native"/>
</id>
<!--设置普通字段的属性对应关系-->
<property name="name" type="java.lang.String">
<column name="NAME"/>
</property>
<property name="version" type="java.lang.String">
<column name="VERSION"/>
</property>
</class>
</hibernate-mapping>
3.写Hibernate的配置文件,将映射文件写入配置文件:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<!--注意这里的每个属性前面都是有hibernate的前缀的,在这不写可以,但是在和Spring的时候一定要写这个前缀-->
<property name="connection.username">root</property>
<property name="connection.password">***</property>
<property name="connection.url">jdbc:mysql://localhost:3306/db_studentinfo</property>
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<!--配置hibernate的基本信息-->
<property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
<!--执行是否在控制台打印SQL-->
<property name="show_sql">true</property>
<!--是否对SQL进行格式化-->
<property name="format_sql">true</property>
<!-- 指定生成数据表的策略 -->
<property name="hbm2ddl.auto">create</property>
<!--加入关联的映射文件*.hbm.xml,注意这里使用的是目录结构,而不是包名,包名是以点作为分隔符-->
<mapping resource="com/hhu/hibernate/Hibernate.hbm.xml"/>
</session-factory>
</hibernate-configuration>
【注意】注意,在IDEA中,如果有mapping
标签在,那么必须放在最后,否则报错。上述数据库的生成策略为create
是因为这里数据库中开始没有相应的数据表,所以选择这个策略进行自动创建表的操作,再表创建好了之后,正常都会将这个策略改为update
,当然除了上述两种策略外还有create-drop
和validate
:
create
:根据Hibernate的映射文件生成数据表,每次运行都会创建新的数据表,如果已存在就会先删除对应的表在创建新的表。create-drop
:根据映射文件成数据表,但是SessionFactory在关闭后会自动删除刚才生成的表。update
:最常用,如果数据库中没有对应的表则创建新表,如果存在相应的表则只对已有的表进行更新操作,不对表中数据做删除操作,甚至在hbm文件中增加一个字段,也不会删除数据库表已有的字段,只是增加。validate
:如果hbm对应的表和数据库中已有的表不同,那么会抛一个异常,比如字段名不同,但是它不会对表做修改。
除此之外,这里关于方言的书写不同版本的Hibernate,写法不一样,在Hibernate-5.2.12版本中使用InnoDB方言(支持事务)中已经丢弃了如下的写法:
<property name="dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>
而取代之的是:
<property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
否则Hibernate执行报错。
4.利用Hibernate执行数据库的操作
package com.hhu.hibernate;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.junit.Test;
/**
* 测试Hibernate的工作流程
* Created by WeiguoLiu on 2018/3/1.
*/
public class HibernateTest {
@Test
public void test() {
//1.创建SessionFactory对象
SessionFactory sessionFactory = null;
//1.2创建Configuration对象:对应hibernate的基本配置信息和对象关系应映射对象,这里用configure()方法是默认关联根目录的配置文件,否则需要手动指定,他就是负责加载cfg.xml文件
Configuration configuration = new Configuration().configure();
//创建SessionFactory
sessionFactory = configuration.buildSessionFactory();
//2.创建一个Session
Session session = sessionFactory.openSession();
//3.开启事务
Transaction transaction = session.beginTransaction();
//4.执行保存操作
Hibernate hibernate = new Hibernate("Hibernate old", "03.12");
session.save(hibernate);
//获取操作
Hibernate hibernate2 = session.get(Hibernate.class, 1);
System.out.println(hibernate2);
//5.提交事务
transaction.commit();
//6.关闭Session
session.close();
//7.关闭SessionFactory对象
sessionFactory.close();
}
}
【注意】这里的问题就是SessionFactory的创建问题,老版本的创建代码为:
// 1.3创建ServiceRegistry对象,注意4.3以后的版本不再使用ServiceRegistryBuilder对象
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().
applySettings(configuration.getProperties()).build();
//创建SessionFactory
sessionFactory = configuration.buildSessionFactory(serviceRegistry);
上述的StandardServiceRegistryBuilder().applySettings(configuration.getProperties())
的返回对象实际上还是StandardServiceRegistryBuilder
,然后直接build即可得到服务注册器。而在Hibernate-5.2.12中直接使用
sessionFactory = configuration.buildSessionFactory();
即可,老版本的使用方式在这里将会报错,SessionFactory是针对单个数据库映射关系编译后的内存镜像,它是线程安全的(一经创建就不会被修改),但是它的创建是比较耗费资源的,一般情况下,一个应用只会初始化一个SessionFactory对象,他是创建Session
的工厂,通过session可以对数据库进行操作,其实就是对jdbc的封装,Session
中提供了一系列的方法:
- 取得持久化对象的方法:
get()
和load()
- 持久话对象的增、删、改对应于
Session
的save()
、delete()
、update()
- 开启事务:
beginTransaction()
,事务的常用方法:commit()
提交事务,rollback()
回滚事务,wasCommitted()
检查事务是否提交。最后贴一下Hibernate这个框架所需要的jar有:
antlr-2.7.7.jar
classmate-1.3.0.jar
dom4j-1.6.1.jar
hibernate-commons-annotations-5.0.1.Final.jar
hibernate-core-5.2.12.Final.jar
hibernate-jpa-2.1-api-1.0.0.Final.jar
jandex-2.0.3.Final.jar
javassist-3.20.0-GA.jar
jboss-logging-3.3.0.Final.jar
jboss-transaction-api_1.2_spec-1.0.1.Final.jar
当然除了上面该框架所需要的jar,mysql的连接驱动和c3p0的数据库连接池也必然是需要的。
2.关于Session
Session接口操作数据库的最主要的接口,提供了对java对象的操作方法,Session具有一个缓存(缓存中的对象称之为持久话对象),Session缓存就是通常所说的Hibernate的一级缓存,后面还会有二级缓存,持久话对象和数据库中的相关记录库相对应,Session能够在某一时刻,按照缓存中的对象的变化执行相关的SQL语句,实现对象和数据库中记录的同步更新,这个过程称为刷新缓存。在持久化方面,Hibernate将对象分为4种状态:持久态、临时态、游离态、删除态,Session的一些方法可以使对象从一种状态转换为另一个状态。
- 临时对象:OID为空,不处于Session缓存中,在数据库中没有对应的记录;
- 持久化对象:OID不为空,位于Session缓存中,它和数据库中的记录一一对应;
- 游离对象:OID不为null,不处于Session缓存中,一般是由持久化对象转变过来的;
- 删除对象:在数据库中没有对应的记录,但是曾经有过,也曾经被Session管理过;
它们之间的转换关系如下:
下面是关于Session的缓存做一个简单的说明,在Hibernate的实现过程中,Session具备一级缓存的特点,在做查询的时候,利用Session获取到一个对象的同时,Hibernate会将这个对象还存到Session中,如果下次Hibernate再次进行查询时,不会直接到数据库中进行查询,而是先到Session中尽心查询,如果缓存中存在待查询的对象那么直接返回该对象,如果缓存中没有这个对象才会到数据库中进行查询,到数据库查询最直观的就是会向数据库发送SQL语句。下面是一个小栗子:
package com.hhu.entity;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* Created by WeiguoLiu on 2018/3/5.
*/
public class HibernateTest {
Configuration configuration = null;
SessionFactory sessionFactory = null;
//注意在实际生产过程中是不会将Session和Transaction搞成成员变量的,因为可能存在并发的问题
Session session = null;
Transaction tx = null;
@Before
public void setConfiguration() {
//引入cfg的配置
configuration = new Configuration().configure();
//创建SessionFactory
sessionFactory = configuration.buildSessionFactory();
//打开会话
session = sessionFactory.openSession();
//开启事务
tx = session.beginTransaction();
}
@Test
public void test() {
//获取ID为1的记录了
HibernateEntity hibernateEntity = session.get(HibernateEntity.class, 1);
System.out.println(hibernateEntity);
HibernateEntity hibernateEntity2 = session.get(HibernateEntity.class, 1);
System.out.println(hibernateEntity2);
}
@After
public void after() {
//提交事务
tx.commit();
//关闭会话
session.close();
//关闭会话工厂
sessionFactory.close();
}
}
【注】上面的测试中是对ID为1的HibernateEntity对象进行了两次查询,在控制台中可以发现Hibernate只在第一次查询的时候发送了SQL,第二次则没有,这样的机制在一定程度上加快了查询效率、也分担了数据库的压力。只要Session实例没有结束生命周期且没有清理缓存,那么里面的缓存对象也不会消失,Session中对缓存的操作提供了三种:
session.flush()
: 这个方法的方向是从Session缓存到数据库,如果两者的状态不一致,Hibernate则会发送对应的SQL语句到数据库,使数据中对应记录和Session缓存中该对象的状态一致。注意,在执行事务Transaction
的提交的时候,会事先自动调用一下这个方法;session.refresh(h)
:这个方法的方向是数据库到Session缓存,它会强制地向数据库发送Select的SQL语句,使缓存中对象的状态是数据库中最新的状态,Mysql的默认隔离级别为REPEATABLE-READ(可重复读),这个隔离级别可以在Hibernate的配置文件中设置为2(读已提交),否则不设为2用该方法应该不会生效,但是在我这个版本中却生效了。session.clear():
清理Session缓存,使Session缓存中的所有对象全部消失。
这里有一个疑问,以前学习Hibernate的时候,只有开启事务和提交事务才能使变化落实到数据库层面,但这一次学习过程中使用的是Hibernate-5.2.12版本,发现就算不打开事务(beginTransaction
)、不提交事务(commit
),对象的变化也能落实到数据库中,这一点让我很困惑,后来在不断的测试中发现,对于其中涉及到修改(增、删、改)的行为是必须打开事务、然后关闭才能在数据库中生效,而对于读操作好像没有啥关系(都能读取到)。而且在后面和Spring整合的时候,不能使用Spring的注解式的声明式事务,就算使用了,在事务发生异常的时候,事务也没有出现预期的回滚行为。
Session的关于数据库的常用的操作有下面的一些:
1.save(entity)
:这个是存储一个新的对象到数据库的方法,可以是一个临时对象持久化为一个持久化对象,这个过程会为这个对象分配一个ID,在flush缓存时会发送一个Insert语句,最要注意的是持久化对象的ID在Hibernate中是不允许修改的,否则会抛出异常,比如下面的代码段:
HibernateEntity h = new HibernateEntity();
h.setName("testSave");
h.setVersion("v2.1");
h.setId(100);
System.out.println(h);
session.save(h);
h.setId(101);
System.out.println(h);
这个代码在执行过session.save()
方法过后,h对象从临时状态转变成持久状态,执行的setId()
这个方法时会抛出一个异常,我这里抛出的异常如下:
javax.persistence.PersistenceException: org.hibernate.HibernateException: identifier of an instance of com.hhu.entity.HibernateEntity was altered from 100 to 101
2.persist(entity)
:这个方法也可以实现和save()
方法一样的功能将临时对象持久话,但是不同的是,如果在调用该方法之前,对象已经有了id,则不会执行insert,而是抛出异常。
3.get(entity)
:这个方法可以从数据库中获取相应的对象,并且当session一调用这个方法就立即发送SQL语句加载对象,不管程序中是否用到这个对象,如果对象不存在数据库就返回空值即可。
4.load(entity)
:和上面get()
方法一样,也是从数据库中获取相应的对象,但是和上一个方法不一样的是,这个方法不是立即发送SQL语句,而是等后面有用到这个对象的地方时才会发送相应的查询语句,延迟加载。
【注意】在执行这个方法时获取的实际上是一个代理对象,而不是实际的数据库对应的记录。所以如果数据库中没有与待查询对象对应的数据记录时,也就是`get()`返回`null`的情况,`load`方法会报错,因为返回的是代理对象,在数据库中都没有对应的记录,所以就没法代理,所以报错,另外如果在执行过相应的get或者load操作后立即关闭session,前者不会报异常,因为已经加载到对象了,但是后者则会抛异常`LazyInitializationException`,因为在用到对象的时候,通过代理对象查询实际对象的时候却发现和数据库的连接断开了,所以会抛出异常(但是实际过程中5.2版本中测试都是没法通过的,他认为必须先进行事务的提交,否则关闭Session是非法的)
5.update(entity)
:这个方法是更新对象的状态,值得注意的是,一般获取持久化对象后,修改属性后,无需显式地调用这个方法进行更新操作,因为事务在最后进行commit()
时候会自动执行flush()
,由于是持久化对象,更改它在Session缓存中的属性后,提交事务的时候,Hibernate会自动将缓存中的状态改变持久化到数据库中,但是对于游离对象(脱离了Session的管理)必须要显式的调用才能对数据库的进行更新,同时也游离对象变成了一个持久化对象。比如下面关于游离对象的显式更新的小栗子:
HibernateEntity h = session.get(HibernateEntity.class, 200);
h.setName("update22");
tx.commit();
session.close();
//此时h对象已经脱离了上一个Session的管理变为游离对象
session = sessionFactory.openSession();
tx = session.beginTransaction();
h.setName("update99");
//对于游离对象必须显式的调用update方法
session.update(h);
6.saveOrUpdate(entity)
:这个方法可以执行保存或者更新两种操作,具体看对象的状态,如果是临时对象则执行保存操作,如果是游离对象,则执行更新操作;
7.delete(entity)
:删除持久化对象或者游离对象;
8.evict(entity)
:将对象从Session缓存中移除,下面是个小栗子:
“`java
HibernateEntity h = session.get(HibernateEntity.class, 2);
h.setName(“Evict3333”);
//将一个对象从缓存中移除,所以在提交事务前flush操作不会对该对象执行更新操作
session.evict(h);
```
9.doWork()
:session的这个方法可以获取Connection
对象,通过Connection可以让Hibernate调用数据库的存储过程,比如下面的:
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
System.out.println(connection);
//调用数据库的存储过程
}
});
3.Hibernate的核心配置文件hibernate.cfg.xml
在第一个章节已经对这个配置文件做了一些简单的说明,关于数据库的连接和其基本本身的配置这里就不提了,下面对数据库连接池的配置(仅仅需要在配置文件中配置下相关属性即可使用C3P0创建数据库连接)做一个简单的说明:
1.导入除了Hibernate的核心依赖外,还需要额外导入的jar有:org.hibernate:hibernate-c3p0:5.2.12.Final和基本c3p0的jar包和数据库的驱动;
2.在Hibernate的配置文件中配置数据库连接池的相关属性:
<!--配置数据库连接池,可选属性,在和Spring整合时,不要忘了一定要加上hibernate的前缀-->
<!--数据库连接池的最大连接数-->
<property name="c3p0.max_size">10</property>
<!--数据库连接池的最小连接数-->
<property name="c3p0.min_size">3</property>
<!--当数据库连接池中连接耗尽时,同一时刻获取多少个连接-->
<property name="c3p0.acquire_increment">2</property>
<!--数据库连接池中连接对象在空闲多长时间后被销毁-->
<property name="c3p0.timeout">5000</property>
<!--
连接池检测线程多长时间检测一次池中所有连接对象是否超时,
连接池本身不会把自己从连接池中移除,而是专门有一个线程周期性来干这事儿,
通过比较连接对象最后一次使用的时间和当前时间的差值和timeout做对比,得出是否
要销毁这个连接对象。
-->
<property name="c3p0.idle_test_period">2000</property>
<!--缓存Statement对象的数量-->
<property name="c3p0.max_statements">10</property>
<!--设定JDBC的Statement读取数据的时候每次从数据库中读取的记录数,比如要读取100条记录,那么每次读取10条,分10次读,减少内存的消耗,实际开发中这个值通常取100
-->
<property name="jdbc.fetch_size">100</property>
<!--设定对数据库进行批量操作时批次的数量,类似于缓冲区的大小,
这个值越大,批量操作时的发送SQL的次数就越小,速度也就越快,
实际开发中,这个值取30比较合适
-->
<property name="jdbc.batch_size">30</property>
3.其他的配置和正常的Hibernate配置一样,下面做一个简单的测试:
//前后的基本流程已省,请自行脑补
@Test
public void testDoWork() {
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
System.out.println(connection);
}
});
}
可以查看获取Connection的信息判断是否获取的连接是不是通过c3p0获取的;
4. 对象关系映射文件:*.hbm.xml
对象关系映射文件主要是用来作POJO类和数据库之间的映射关系,也可以说是持久化类和数据表之间的对应关系,在运行时Hibernate会根据这个文件生成各种SQL语句。上面对这个文件也有相应的说明,通常基本配置如下:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<!--指明实体类和数据库中表的对应关系-->
<class name="com.hhu.hibernate.Hibernate" table="HIBERNATE">
<!--设置主键,name表示实体类Bean的属性名-->
<id name="id" type="java.lang.Integer">
<!--指定数据表中的列名-->
<column name="ID"/>
<!--指定生成策略,native表示用数据库本地生成策略方式-->
<generator class="native"/>
</id>
<!--设置普通字段的属性对应关系-->
<property name="name" type="java.lang.String">
<column name="NAME"/>
</property>
<property name="version" type="java.lang.String">
<column name="VERSION"/>
</property>
</class>
</hibernate-mapping>
核心主标签是`’,主要层级关系如下:
类层次<class>
–主键<id>
–基本类型<property>
–实体引用类<many-to-one>
和<one-to-many>
–集合<set>
、<map>
、<array>
【注意】一个<hibernate-mapping>
中可以有多个<class>
,但是建议只写一个,方便管理。
下面对上面各个层级标标签做如下说明:
<class>
标签:里面可以通过dynamic-update="true"
的配置动态更新,所谓的动态更新就是根据session的行为进行SQL语句的发送,如果没有进行这个配置,会按照指定的模板SQL进行发送,比如:
HibernateEntity h = session.get(HibernateEntity.class, 200);
h.setName("update22");
提交事务后,控制台打印的SQL语句如下:
update
hibernate
set
NAME=?,
VERSION=?
where
ID=?
可以看到虽然对于Session缓存中的h对象只修改了Name属性,但是发送SQL时是将所有的属性NAME、VERSION都更新了,配置了动态更新后,只会对更改的属性发送相应的SQL语句,这里配置了动态更新后,发送的SQL语句就只对NAME属性进行了修改:
update
hibernate
set
NAME=?
where
ID=?
<id>
和<property>
标签:它们是用来指定实体类属性和数据库表字段的对应关系,前者用来指定表的主键属性,后者用来指定普通属性字段。两者里面都有name
、type
、unique
、update
等属性,其中name
是指的java实体类的中的属性名(大小写一致),type
指定该属性映射到数据库中的类型(比如java.lang.String
,除了可以使用java中的全类名,还可以使用Hibernate中的类型,对应的写成string
,其他类型见下面的图),unique
可以指定是否为当前列字段添加唯一约束,update
可以指定该字段是否可以被修改;两个标签下面还有子标签<column>
指定数据库表中对应的字段名(一般全部大写),其中<id>
中还有一个<generator>
子标签(用其中的class
属性指定主键的生成策略,主要有increment
、identity
、hilo
和native
以及assigned
。其中increment
是由Hibernate的递增方式为主键赋值,Hibernate首先会读取对应表中的主键最大值,然后在这个最大主键的基础上进行递增,但是问题是如果可能存在并发的情况,主键可能会出现重复最后报错,所以这种策略只适合测试,实际开发中基本不会用;identity
则是由底层数据库负责生成标识符,但是前提是底层数据库必须支持把主键定义为自增长字段类型,像DB2、MySQL、MMSQLServer、Sybase就可以,但是Oracle不支持;hilo
是由Hibernate根据high/low算法生成的标识符,它从数据库特定表的字段中获取high值,最大的优点就是这策略不依赖于底层数据库的类型,特别灵活;native
根据底层数据库对自动生成标识符的支持能力来选择使用identity
、hilo
和sequence
,所以它可以跨平台,这个很棒!);assigned
表示每次主键由手工赋值!
【重要】关于时间和日期一直转换易搞混的点儿,下面对这个做详细说明:在java中代表时间和日期类型的包括java.util.Date
和java.util.Calendar
,在JDBC的API中还提供了java.util.Date
的3个子类:java.sql.Date
、java.sql.Time
、java.sql.Timestamp
,在标准的SQL中,DATE
表示日期,TIME
表示时间,TIMESTAMP
表示时间戳(包含了日期和时间),所以两两对应起来应该是:java.sql.Date
–DATE
,java.sql.Time
–TIME
,java.sql.Timestamp
–TIMESTAMP
,而作为三类的父类java.util.Date
可以和DATE
、TIME
、TIMESTAMP
都对应,基于这一点考虑,在写java实体类涉及到时间日期的时候,建议使用java.util.Date
,可以和数据库中三类都兼容,具体映射为什么类型看项目需求。
<!--将java.util.Date映射成时间戳-->
<property name="date" type="timestamp">
<column name="DATE"/>
</property>
<!--将java.util.Date映射成时间-->
<property name="date" type="time">
<column name="DATE"/>
</property>
<!--将java.util.Date映射成日期-->
<property name="date" type="date">
<column name="DATE"/>
</property>
5.映射关系
映射关系有多种,Hibernate把持久化类的属性分为两种:值类型(没有OID)和实体类型(有OID),这里进行分类讨论说明:
5.1 值类型的映射
域模型:这个概念和Word中域的概念有些类型,通过细化持久类的颗粒度提高代码的复用度,有点表关联的意味了,但是这里没有涉及到主外键的关联,所以确切点又不能将其描述为表关联,但是它和表关联区别就是没有主外键。
这里可以举一个关于值类型的栗子:Worker类有id、name、Pay属性,Pay是定义的另一个类(里面有月薪、年薪、带薪休假的薪资),照着我们一般的理解,给Pay对象搞一个ID,然后Worker通过持有Pay对象的id,就是一个表之间的关联,但是现在Pay对象没有ID,所以也就没有这个说法了,现在Worker这个表就是想用Pay里面的属性做一张表,原来的Worker表想做成这样
id | name | yearPay | monthPay | restPay |
---|---|---|---|---|
xx | xx | xx | xx | xx |
xx | xx | xx | xx | xx |
但是组成这张表的java类被细化为Worker和Pay两个类,并且前者有ID,后者没有,所以这里就会把Pay对象叫做域或者组件,在做Worker的映射文件的时候,映射薪资的时候就不能简单的使用<property>
标签了,而需要使用<component>
标签引用Pay这个组件,并且只需要配置一个Worker的映射文件即可,关于Pay对象无需进行数据映射的配置(最主要辨别方式是Pay对象没有ID这个属性),详细的如下:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entity.Work" table="WORK">
<id name="id" type="int">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="string">
<column name="NAME"/>
</property>
<!--映射组成关系,将Pay对象引入-->
<component name="pay" class="com.hhu.entity.Pay">
<!--指定组成关系-->
<property name="monthPay" type="int">
<column name="MONTHPAY"/>
</property>
<property name="yearPay" type="int">
<column name="YEARPAY"/>
</property>
<property name="restPay" type="int">
<column name="RESTPAY"/>
</property>
</component>
</class>
</hibernate-mapping>
然后正常引入上述的原数据文件即可创建上面的Worker表。
5.2 一对一关联关系
一对一的关联映射有两种方式:一种按照外键的关联方式;还有一种是按主键关联映射。这里以部门(Department)和部门经理(Manager)两者为例。
第一种按外键关联的方式其实和下面说到多对一的方式相似,唯一不同的是,一对一需要对外键做唯一约束,而多对一关联映射中外键是没有约束的(可重复)。简单的将主要核心配置应该如下:外键可以放在任意一方,但是在存放外键的一端需要使用<many-to-one>
标签配置关联关系,并且需要为里面的外键增加唯一的约束unique="true"
,整体如下(这里是将外键放到了部门类Department中):
<many-to-one name="mgr" class="com.hhu.entities.Manager" column="MGRID" unique="true"/>
然后在另一方(即不存在外键的一方)使用<one-to-one>
标签配置并且添加property-ref
属性指定被关联实体主键以外的字段做为关联字段(如果不指定,将会用其主键进行关联,重要!一定不能忘),同时java类中也要声明对另一个对象的引用(即每个对象实体类中都必须持有对方实体类的引用对象)。如:
<one-to-one name="department" class="com.hhu.entities.Department" property-ref="mgr"/>
上面mgr是指department类中的这个属性对应的数据库表中的字段作为查询条件。
两者具体的实体类为:
public class Department {
private Integer depId;
private String depName;
//对另一方的引用
private Manager mgr;
//getter and setter
}
public class Manager {
private Integer mgrId;
private String mgrName;
//对另一方的引用
private Department department;
//getter and setter
}
第二种一对一映射关联关系是基于主键的方式,相对于上面基于外键的方式,Department表中少了原来的外键列MGRID,但是外键并没有直接拿去,而是直接用Department的主键作为外键。具体的配置为:一端的主键生成器使用foreign
策略,表示使用对方的主键来生成自己的主键,自己不独立生成主键,同时使用<param>
子标签指定使用当前持久化类的哪个属性做为”对方”,同时使用该生成策略的一方使用<one-to-one>
标签做映射关联,同时还要增加constrained="true"
的属性表示为当前主键生成器添加外键约束(这一点不能掉,一定要加上,否则不能为主键添加外键);在另一端也是使用<one-to-one>
标签配置关联关系。这里同样使用Department作为主,Manager作为从,实体类没有改变,所以很明显应该让Manager的主键生成策略变成foreign
,并且指定跟随的是他实体类中的Department对象来生成,所以Manager的对象映射文件配置为:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Manager" table="MANAGERS">
<id name="mgrId" type="int">
<column name="MGRID"/>
<generator class="foreign">
<!--指定主键参照该类中department对象生成-->
<param name="property">department</param>
</generator>
</id>
<property name="mgrName" type="string">
<column name="MGRNAME"/>
</property>
<one-to-one name="department" class="com.hhu.entities.Department" constrained="true"/>
</class>
</hibernate-mapping>
然后另一端(Department)使用<one-to-one>
标签做配置,具体如下:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Department" table="DEPARTMENT">
<id name="depId" type="int">
<column name="DEPID"/>
<generator class="native"/>
</id>
<property name="depName" type="string">
<column name="DEPNAME"/>
</property>
<one-to-one name="mgr" class="com.hhu.entities.Manager"/>
</class>
</hibernate-mapping>
利用上述的配置文件后可以发现在生成的两张表中,Department中应景没有外键列MGRID了,而是让manager的主键依据Department生成并添加了外键,测试正常测试就好,很常规。
5.3 一对多关联关系(单向)
关联关系是具备方向的,比如一个人有多本书,正向来看,一个人有多本书是一对多的关联关系;反向看,多本书可以同属于一个人,所以这又是多对一的关联关系,而且上述的关系从两个方向来看,也可以是双向关联。具体需要怎样的关联关系,需要看项目需求,在本小节中,一对多(1-n)就是实现了一端可以访问多端的需求,开发中只需要在人这个类中持有一个书本的集合即可,可以将下面5.4中的小栗子反过来即可。
5.4 多对一关联关系(单向)
和上面5.2中的一对多关联关系是相对的,需要在多的一端持有对一端的引用,就是在书本类中持有用户这个属性即可实现在多端对一端的访问。这种情况和前面的一种情况又不太一样,需要注意这里书本和消费者之间是两个带有ID的独立的类,并且在做映射的时候,Customer和Order都是需要进行映射文件的配置的,所需最后是生成的两张表(一张是Customer表,一张是Order表),根据java类的定义,多对一,所以在多方持有一方的引用,这里就是Order持有Customer的引用即可,所以在定义Order这个对象时需要加上Customer的引用,Order和其映射文件正常定义即可,而关多方的Customer的定义做如下定义:
package com.hhu.n21;
/**
* Created by WeiguoLiu on 2018/3/6.
*/
public class Order {
private Integer OId;
private String OName;
//一方持有对多方的引用,在数据库表中就是一个关联Customer的外键
private Customer customer;
public Integer getOId() {
return OId;
}
public void setOId(Integer OId) {
this.OId = OId;
}
public String getOName() {
return OName;
}
public void setOName(String OName) {
this.OName = OName;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}
Order的对象配置文件:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.n21.Order" table="ORDER">
<id name="OId" type="int">
<column name="OID"/>
<generator class="native"/>
</id>
<property name="OName" type="string">
<column name="ONAME"/>
</property>
<!--映射多对一的关联关系,其中name就是指的多方里面对一方的引用,在数据库的表中实际上就是创建一个外键
去关联CustomrID的外键去关联Customer这个表-->
<many-to-one name="customer" class="com.hhu.n21.Customer" column="CUSTOMER_ID"/>
</class>
</hibernate-mapping>
将上述的映射文件和Customer的映射文件和相关class加入到Hibernate.cfg.xml核心配置文件,创建基本的测试类运行即可在数据库中创建两个表并且关联主外键的关系。
在测试的时候发生一个小插曲,数据库中创建了Customer的表之后再创建Order表的时候控制台报错了:
在做了大半天排查之后,发现自己作死,请看一下订单表映射文件的表名–ORDER,这玩意儿是SQL关键字,怎么可能不出错!!行吧,随意改一下吧再跑,基础知识没有扎实的底子啊,此外用上述的配置跑出来后的,基本所有的测试都是通过的,但是在测试删除的时候发现竟然可以删除正在被Order引用的Customer对象,这个时候就知道是创建表的时候外键出问题了,可是查看控制台发现有添加外键的SQL语句,再仔细一看,创建表的时候有这么一句engine=MyISAM
,并且我尝试性直接在Navicat中添加外键,居然无法添加(一保存就消失),妈的,此时已经猜测是数据库引擎的问题,巴拉巴拉去找资料,发现MyISAM这种引擎也是和InnoDB齐名的数据库引擎(对数据库的学习还仅仅停留在增删改查的阶段(*  ̄︿ ̄)),然后发现MyISAM类型的表强调的是性能,其执行数度比InnoDB类型更快,但是不提供事务支持(应该也是不提供外键这玩意儿的),而InnoDB提供事务支持事务,外部键等高级数据库功能。这样可能就能解释为什么有添加外键的SQL却无法正常添加外键了(确实无法添加,我自己还手动试了一下,也不报错……),好了,问题的原因找出来了,接下来直接定位到Hibernate的核心配置文件中方言上:
<property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
其实很早之前学习Hibernate的时候,我记得用的是MySQLDialect,后来说专门为MySQL5版本做了优化,建议使用MySQL5Dialect,所以就用这个吧,没办法,换引擎吧,换成InnoDB,果断换成
<property name="dialect">org.hibernate.dialect.MySQL57InnoDBDialect</property>
成功,但是这种方言确实不提倡的,也不知道为啥,总之外键可以配置了,先这样,以后再做具体的研究。
通过以上的步骤终于可以创建相应的两张表了,下面就对它做一些测试:
package com.hhu.n21;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* Created by WeiguoLiu on 2018/3/6.
*/
public class TestN21 {
private Configuration configuration = null;
private SessionFactory sessionFactory = null;
private Session session = null;
private Transaction tx = null;
@Before
public void init() {
//创建配置文件
configuration = new Configuration().configure();
//搞一个SessionFactory
sessionFactory = configuration.buildSessionFactory();
session = sessionFactory.openSession();
tx = session.beginTransaction();
}
@After
public void destory() {
tx.commit();
session.close();
sessionFactory.close();
}
@Test
public void testCreate() {
}
@Test
public void testSave() {
Customer c1 = new Customer();
c1.setCName("Customer1");
Order o1 = new Order();
o1.setOName("Order1");
Order o2 = new Order();
o2.setOName("Order2");
o1.setCustomer(c1);
o2.setCustomer(c1);
//先插入customer再插入order
session.save(c1);
session.save(o1);
session.save(o2);
/*
先插入Order,再插入Customer,这样的方式会多出update语句,因为没有对应Customer
所以在插入Order时会先让CustomerID这个外键为空,等插入Customer后,在对插入的Order进行
更新,这样的方式由于多出这几条update语句会让SQL执行效率下降,不如上面的方式
*/
// session.save(o1);
// session.save(o2);
// session.save(c1);
}
@Test
public void testMany2One() {
//这里是查询关联关系,而不是简单的查询Order中Customer的外键,也要通过这个外键将对应的Customer查询出来
Order o = session.get(Order.class, 1);
System.out.println(o.getOName());
/*上述的方式只是查询了Order,并没有查询关联的Customer(仅仅有一个外键的ID),在等到需要用到的时候才会去查询这个Customer
对象在使用到这个对象的时候才会发送相应的SQL语句,这就叫延迟加载,同样的在对Customer加载时可能会出现懒加载异常(Session
关闭时)
*/
Customer c = o.getCustomer();
System.out.println(c.getCName());
}
@Test
public void testUpdate() {
Order o = session.get(Order.class, 1);
o.getCustomer().setCName("AA");
}
@Test
public void testDelete() {
//在没有设定级联关系(比如级联删除)的情况下,是不能直接删除Customer的,因为它在Order作为外键被某些Order对象引用
Customer c = session.get(Customer.class, 1);
session.delete(c);
}
}
【注意】上述测试程序主要注意两个地方:第一点,在保存的时候,建议先保存不存在外键的那个对象,再保存引用该对象的对象(如果执行顺序颠倒,那么会额外多出更新外键的SQL,影响效率);第二点,在删除操作时,测试是无法通过的,不作级联操作指定的话,如果要删除一方中的对象(该对象还正在被多方的某个对象引用),那么此时是无法删除的。此外关于外键引用的对象,Hibernate采用懒加载机制,只有使用到这个引用对象才会发送SQL去查询Customer对象,否则查到的就是一个外键的ID。
5.5 多对多关联映射(单向)
这里以类别和商品之间的关系显然可以抽象为多对多的关联关系(小电器可以包含电筒、风扇,风扇属于小电器也属于自动商品),由于多对多的关联关系每一个对象都可能和多个对象对应,所以必须存在第三张表才能搞定这种映射关系(这张表通常称之为连接表),这里以Item和Category作为栗子,在Category中定义一个包含Item对象的集合,当然这里是这样的,也可以不用这样的方式而使用在Item中搞一个Category的集合,道理也是一样的(这里不再赘述)。具体的配置如下:
public class Item {
private Integer id;
private String name;
//getter and setter
}
public class Category {
private Integer id;
private String name;
//包含Item集合的对象
private Set<Item> itemSet = new HashSet<>();
//getter and setter
}
然后Item的对象配置文件正常:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Item" table="ITEMS">
<id name="id" type="int">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="string">
<column name="NAME"/>
</property>
</class>
</hibernate-mapping>
主要是Category的关系映射文件:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Category" table="CATEGORIES">
<id name="id" type="int">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="string">
<column name="NAME"/>
</property>
<!--这里的table是指连接表表,而不是集合对象表-->
<set name="itemSet" table="CATEGORIES_ITEMS">
<key>
<!--当前持久化类Category在关联表中的外键-->
<column name="C_ID"/>
</key>
<!--many-to-many指定多对多的关联关系,column指定集合类对象在关联表中关联的列名-->
<many-to-many class="com.hhu.entities.Item" column="I_ID"/>
</set>
</class>
</hibernate-mapping>
【注意】这里需要注意的是配置集合属性的时候,需要额外配置一个连接表,配置完即可,测试通过。
5.6 双向多对多(双向)
双向多对多是上面单向多对多的扩展,需要注意的是:双方都必须使用集合属性和连接表以及<many-to-many>
映射关系,两者也还都需要指定连接表的表名(两者的连接表名必须一致)和外键的列名,除此以外,还必须设置其中一方的inverse = "true"
,否者双方都维护关联关系可能造成主键冲突。
实体类:
public class Item {
private Integer id;
private String name;
//双方都必须添加对方对象的集合属性
private Set<Category> categorySet = new HashSet<>();
//getter and setter
}
public class Category {
private Integer id;
private String name;
//添加对方对象的集合属性
private Set<Item> itemSet = new HashSet<>();
//getter and setter
}
配置双方的映射文件(都必须要配置Set和many-to-many):
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Item" table="ITEMS">
<id name="id" type="int">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="string">
<column name="NAME"/>
</property>
<!--双方都要配置Set集合,配置的关联表table必须一致,外键名字就是关联表中所在类的外键列名,同时需要配置多对对的映射关系-->
<set name="categorySet" table="categories_items">
<key>
<column name="I_ID"/>
</key>
<many-to-many class="com.hhu.entities.Category" column="C_ID"/>
</set>
</class>
</hibernate-mapping>
另一个映射文件:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Category" table="CATEGORIES">
<id name="id" type="int">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="string">
<column name="NAME"/>
</property>
<!--这里的table是指关联表,而不是集合对象表-->
<set name="itemSet" table="CATEGORIES_ITEMS">
<key>
<!--当前持久化类在关联表中的外键,即在连接表中关联Category对象的外键列名-->
<column name="C_ID"/>
</key>
<!--many-to-many指定多对多的关联关系,column指定集合类对象在关联表中关联的列名-->
<many-to-many class="com.hhu.entities.Item" column="I_ID"/>
</set>
</class>
</hibernate-mapping>
【注意】经过上面的配置后,就愉快的测试了,空代码块测试生成表结构没有问题,关联表是联合组建,每个字段都和对象通过各自的外键关联,这一点她很乖地做到了,但是在测试保存对象的时候,
@Test
public void testSave() {
Category category = new Category();
category.setName("Category1");
Item item = new Item();
item.setName("Item1");
Item item2 = new Item();
item2.setName("Item2");
//设置双方关联关系
category.getItemSet().add(item);
category.getItemSet().add(item2);
item.getCategorySet().add(category);
//这里不管先插入哪一个Hibernate总是先插入DEPARTMENT再插入MANAGERS,因为后者的主键依赖于前者
session.save(item);
session.save(item2);
session.save(category);
}
抛出了异常:
确实可怕,再想想是不是有啥地方没配呢,没有错发现了PRIMARY
,迅速定位到主键问题,一想,这不就是我渴望许久的主键冲突吗?所以记住在双向多对多关联映射过程中,必须只能由一方来对双方的关联关系进行维护,双方维护会产生主键冲突的问题,所以只需要将其中一方的维护权禁掉即可:inverse = "true"
,这一点一定要注意,禁掉一方的维护权后顺利保存<( ̄︶ ̄)↗[GO!]>
5.7 双向一对多(因为是双向,所以和双向多对一是一样的)
双向其实就是单向的扩展,这里我们要做的就是在一端添加一个对多端集合的引用,而在多段添加一个一端的引用即可,配置和上面的类似。下面是详细的流程:
多端(即Order)的java类和映射文件和多对一中的情况一样,无需做修改,一端(Customer)需要添加多端集合的引用,在Customer中添加下面的代码:
//添加多端的集合,这里一定要进行初始化,否则会发生空指针异常
private Set<Order> orders = new HashSet<>();
public Set<Order> getOrders() {
return orders;
}
public void setOrders(Set<Order> orders) {
this.orders = orders;
}
【注意】这里有两点需要注意:第一点就是集合一定要初始化,否则会报空指针异常;第二点关于这里的集合一定要定义成接口,具体初始化为哪个类随意,因为后面通过Hibernate查询获取的集合类型是Hibernate内置的集合类型而不是标准的java类型,如果这里直接定义为HashSet,后面查询返回的集合类可能两者不能兼容从而报错!
然后关于Customer的映射文件添加一对多的配置,其他一样:
<!--映射一对多的集合属性-->
<set name="orders" table="orders">
<key column="CUSTOMER_ID"></key>
<one-to-many class="com.hhu.both.Order"/>
</set>
对上述的配置属性有如下说明:name–Customer类中对多方集合引用的属性名orders
,table是指多方(Order对象在数据库中对应的表为orders)集合所在的表名(或者可以直接理解为集合元素所在的数据表名),key标签中的column必须和多方(Order)映射文件中多对一关联关系中的column一致(即对应的外键引用),one-to-many标签中的class指明多方的java类型。
测试还是常规的测试,这里是保存测试的一部分代码:
@Test
public void testSave() {
Customer c1 = new Customer();
c1.setCName("Customer1");
Order o1 = new Order();
o1.setOName("Order1");
Order o2 = new Order();
o2.setOName("Order2");
//关联多对一
o1.setCustomer(c1);
o2.setCustomer(c1);
//关联一对多
c1.getOrders().add(o1);
c1.getOrders().add(o2);
//先插入customer再插入order
session.save(c1);
session.save(o1);
session.save(o2);
}
除了上述的配置外,还有关于双向关联关系的维护也是一个比较实在的问题,到底由多方来维护,还是由一方来维护,在Hibernate中,可以通过在两者配置映射关联关系的时候利用inverse
关键字来指定关联关系的维护方,当inverse = false
那么该方为主动方(由他维护关联关系),如果配置为inverse = true
那么该方为维护的被动方,如果在双方都没有设置inverse = true
,那么一方和多方都必须维护两者的关联关系(Hibernate是默认双方维护的,即双方都是inverse = false
)。实际开发往往会把双向一对多的关联关系交给多方来维护(即在多方配置inverse = false
),这样有助于性能的改善(举个栗子在Customer和Order这个小栗子中,每个Order都会记住Customer的名字可以很轻松的找到消费者,但是让每个Customer自己去找属于自己的Order就不太现实了);这一点比较重要啊,像上面的栗子测试的SQL语句如下:
Hibernate:
insert
into
CUSTOMERS
(CNAME)
values
(?)
Hibernate:
insert
into
ORDERS
(ONAME, CUSTOMER_ID)
values
(?, ?)
Hibernate:
insert
into
ORDERS
(ONAME, CUSTOMER_ID)
values
(?, ?)
Hibernate:
update
ORDERS
set
CUSTOMER_ID=?
where
OID=?
Hibernate:
update
ORDERS
set
CUSTOMER_ID=?
where
OID=?
交给多方维护,因为默认双方维护,所以只需要将一方的维护权拿走即可,在配置Customer映射文件的关联关系时,做如下改动:
<set name="orders" table="orders" inverse="true">
再次运行上面保存的测试代码输出的SQL语句如下:
Hibernate:
insert
into
CUSTOMERS
(CNAME)
values
(?)
Hibernate:
insert
into
ORDERS
(ONAME, CUSTOMER_ID)
values
(?, ?)
Hibernate:
insert
into
ORDERS
(ONAME, CUSTOMER_ID)
values
(?, ?)
对比发现,前者明显多了两条update的SQL语句,后者则更加简洁明了,所以性能肯定提升啊,对不对。
在上面的测试之后,还有删除的操作:
@Test
public void testDelete() {
//在没有设定级联关系(比如级联删除)的情况下,是不能直接删除Customer的,因为它在Order作为外键被某些Order对象引用
Customer c = session.get(Customer.class, 4);
session.delete(c);
}
上面已经探讨过删除是无法进行的,因为这个Customer对象还在数据库Order表中被引用,但是这里可以通过cascade="delete"
设置级联删除操作来设置完成这种行为,在删除Customer这个对象时会同时删除所有引用这个对象的Order对象。具体的配置如下(Customer.hbm.xml),但是开发中一般不推荐使用级联配置,手动的更好:
<!--映射一对多的集合属性-->
<set name="orders" table="orders" inverse="true" cascade="delete">
<key column="CUSTOMER_ID"></key>
<one-to-many class="com.hhu.both.Order"/>
</set>
关于cascade
的取值还有很多,下面是详细的说明
当然除了级联的配置外,还可以对集合查询时自动排序,比如下面的配置:
<set name="orders" table="orders" inverse="true" cascade="delete" order-by="OId desc">
通过order-by
指定集合的排序方式,OId
是指orders表中的字段名而不是java中的属性名,然后desc就是按照OId降序,再次跑一下下面的测试代码:
@Test
public void testMany2One() {
//反转查找的顺序,先查一方再查多方
Customer c = session.get(Customer.class, 2);
System.out.println(c.getCName());
//引用的部分都是延迟加载
Set<Order> orders = c.getOrders();
for(Order order: orders) {
System.out.println(order.getOName());
}
}
控制台的输出SQL如下:
Hibernate:
select
customer0_.CID as CID1_0_0_,
customer0_.CNAME as CNAME2_0_0_
from
CUSTOMERS customer0_
where
customer0_.CID=?
Customer2
Hibernate:
select
orders0_.CUSTOMER_ID as CUSTOMER3_1_0_,
orders0_.OID as OID1_1_0_,
orders0_.OID as OID1_1_1_,
orders0_.ONAME as ONAME2_1_1_,
orders0_.CUSTOMER_ID as CUSTOMER3_1_1_
from
ORDERS orders0_
where
orders0_.CUSTOMER_ID=?
order by
orders0_.OID desc
Order4
Order3
可以看到发送查询SQL的时候Hibernate已经自动带上了排序的功能。
5.8 继承映射
和java中的子类父类有些类似,在Hibernate中通过继承映射在查询人这个对象的时候,可以一并将人的子类对象(学生啊、老师啊、工人啊等一系列的对象)查询出来,这种查询称之为多态查询。Hibernate支持三种继承映射策略:subclass
、joined-subclass
或者union-subclass
。
看第一种继承映射的方式subclass
,这种继承方式有一些特点:子类和父类对象是映射到数据库的同一张表中的,记录是父类还是子类对象,主要是通过辨别者列来做区分。配置相对简单,这里实体类以Person和Student为例做演示:
public class Person {
private Integer id;
private String name;
private Integer age;
//getter and setter
}
public class Student extends Person {
//子类独有属性
private String school;
//getter and setter
}
因为这种映射方式只创建一张表(子类和父类在同一张表中),所以只需要一个映射文件Person.hbm.xml,如下:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<!--通过discriminator-value标签指定Person对象在辨别者列中值为PERSON-->
<class name="com.hhu.entities.Person" table="PERSONS" discriminator-value="PERSON">
<id name="id" type="int">
<column name="ID"/>
<generator class="native"/>
</id>
<!--配置辨别者列,用于区分是父类还是子类记录,有Hibernate自己维护-->
<discriminator column="TYPE" type="string"/>
<property name="name" type="string">
<column name="NAME"/>
</property>
<property name="age" type="int">
<column name="AGE"/>
</property>
<!--映射它的子类Student,使用subclass进行映射,因为子类父类在一张表中,所以不需要指定table了,同样需要用discriminator-value指定子类对象在辨别者列中的字符-->
<subclass name="com.hhu.entities.Student" discriminator-value="Student">
<!--指定子类独有的属性名和对应的字段名-->
<property name="school" type="string">
<column name="SCHOOL"/>
</property>
</subclass>
</class>
</hibernate-mapping>
配置完上述的信息后,可以直接测试创建表,插入对象:
@Test
public void testSave() {
Person p = new Person();
p.setName("Jack");
p.setAge(25);
session.save(p);
Student s = new Student();
s.setName("Janey");
s.setSchool("HHU");
s.setAge(21);
session.save(s);
}
再来看一数据库中表的样式:
从上表可以看出有一个辨别者列TYPE,通过之前在映射文件中设置的辨别者列属性值进行区分父类对象和子类对象,但是这里可以发现,子类对象独有的字段肯定不能添加非空约束(因为仅仅是父类的话是肯定没有子类的字段的)。
第二种是joined-subclass
,使用这种策略是生成两张表,父类表正常,子类表是只子类特有的字段(子类表的ID作为外键关联父类表的ID)。主要的配置如下:
实体类不变,映射文件同样只要配置Person.hbm.xml一个即可:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Person" table="PERSONS">
<id name="id" type="int">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="string">
<column name="NAME"/>
</property>
<property name="age" type="int">
<column name="AGE"/>
</property>
<!--指定子类表,因为是两张表,所以需用table指定创建子类表的表名-->
<joined-subclass name="com.hhu.entities.Student" table="STUDENT">
<!--配置子类表的主键,注意这个主键同时也是父类表的主键-->
<key column="STUDENT_ID"></key>
<!--配置子类表的独有字段-->
<property name="school" type="string" column="SCHOOL"/>
</joined-subclass>
</class>
</hibernate-mapping>
配置完上述的文件后就可以测试了,创建了两张表,一张父类表,一张子类表(但是这张表只有子类独有的字段SCHOOL,它的主键就是它直系父类的ID),这种方式不需要使用辨别者列,同时子类的字段可以添加非空约束,也没有冗余字段,但是速度相对上面的要慢一些,因为插入子类对象时需要插入两张表中(父类表中需要插入一次共有属性,子类表中也需要插一次子类的独有属性)。
第三种就是使用union-subclass
做映射,同样只需要配置一个Person类的映射文件即可:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Person" table="PERSONS">
<id name="id" type="int">
<column name="ID"/>
<generator class="increment"/>
</id>
<property name="name" type="string">
<column name="NAME"/>
</property>
<property name="age" type="int">
<column name="AGE"/>
</property>
<!--这里需要指定子类的表名生成子表,同时指定子类独有的属性列即可-->
<union-subclass name="com.hhu.entities.Student" table="student">
<property name="school" type="string" column="SCHOOL"/>
</union-subclass>
</class>
</hibernate-mapping>
【注意】这里的主键生成策略使用native
和hilo
时都会报错:
后来搞了搞使用改成上面increment
可以成功创建表,关于这一块以及上面的继承映射做查询测试的时候,下面的代码一直无法通过编译,还望懂得同学不吝赐教( ̄︶ ̄*)):
@Test
public void testGet() {
List<Person> persons = session.createQuery("FROM Person").list();
System.out.println(persons.size());
}
编译无通过,报错信息如下:
6. Hibernate的检索策略
检索直白一点讲就是查询,在检索数据时,主要原则是:1.不浪费内存,也是由于这一点,懒加载在Hibernate中使用比较多;2. 查询效率,简单点讲,就是要尽可能少的发送SQL语句。下面对检索策略主要做如下几个说明。
类级别的检索策略包括了立即检索(立即加载检索对象)和延迟检索(延迟检索的对象的加载),Hibernate默认是延迟检索,在类级别的检索策略中可以通过class
属性中的lazy
配置是否延迟加载检索对象,默认是lazy="true"
,通常情况下,如果程序加载一个持久化对象仅仅是为了获取它的引用,这时可以使用延迟检索(load方法),也有一个坏毛病,就是容易发生懒加载异常。在一对多或多对多集合属性默认是懒加载检索策略。还有就是lazy
属性除了true
和false
外,还有第三种的赋值属性extra
(增强懒加载),所谓的增加懒加载就是Hibernate会尽可能多的不去加载实际的对象,最多的用处就是在计算集合的长度时,使用懒加载时这个时候会去加载对象,而在增强型懒加载时,Hibernate则不会去加载对象,而是利用SQL中的count()
函数去获取集合长度从而避免去加载对象。
&emap;在性能提升方面,可以在<set>
标签中通过batch-size = x
的形式来配置批量检索的数量,批量检索可以有效的减少Select语句发送的次数,从而提高检索的效率。
7. Hibernate的检索方式
在Hibernate中提供了多种检索方式,主要有以下的几种:
- 导航对象图检索方式:简单点说,就是对已经获取的对象通过getter方法导航到其他对象;
- OID检索方式:主要是指session的get方法,指定查询对象的类型和ID属性就可以进行查询,之前一直使用的是这个查询方式;
- HQL检索:即Hibernate Query Language,使用面向对象的形式进行查询;
- QBC检索:即Query By Criteria的API来检索,它是基于字符串的查询方式,更加符合OOP的思想
- 本地SQL检索:即使用普通的SQL语言进行检索。
7.1 HQL检索方式
一般的HQL的查询步骤分为三步:
- 写hql,创建Query对象,传入hql:
session.createQuery(hql)
; - 绑定参数,
query.setParameter()
; - 执行,比如
List<Entity> list = query.list()
。
下面直接以一个Department和Employee的双向一对多的小栗子进行说明,具体的属性配置如下:
实体类
public class Department {
private Integer id;
private String name;
//持有对多方的引用
private Set<Employee> employees = new HashSet<>();
//getter and setter
}
public class Employee {
private Integer id;
private String name;
//持有对一端的引用
private Department department;
//getter and setter
}
映射文件:
<!--Department-->
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Department" table="DEPARTMENTS" schema="db_studentinfo">
<id name="id" type="int">
<column name="ID"/>
<generator class="native"/>
</id>
<property name="name" type="string">
<column name="NAME" />
</property>
<!--添加一对多的映射关系-->
<set name="employees" table="EMPLOYEES" inverse="true">
<key column="DEPARTMENT_ID"></key>
<one-to-many class="com.hhu.entities.Employee"/>
</set>
</class>
</hibernate-mapping>
<!---Employee-->
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.hhu.entities.Employee" table="EMPLOYEES" schema="db_studentinfo">
<id name="id" type="int">
<column name="id"/>
<generator class="native"/>
</id>
<property name="name" type="string">
<column name="NAME"/>
</property>
<many-to-one name="department" class="com.hhu.entities.Department" column="DEPARTMENT_ID"/>
</class>
</hibernate-mapping>
第一种是基本的普通查询测试过程及语法的使用说明:
@Test
public void testHQL() {
/*这里的Employee是指的java实体对象名,而不是数据库的表名,?代表占位符,除了可以
使用?做占位符后,还可以使用“冒号 + 命名参数”的形式进行占位,命名参数随意取,后面
绑定参数的时候就根据命名参数进行绑定
*/
String hql = "FROM Employee e WHERE e.name LIKE ? and e.id > ? and e.department = ?";
// String hql2 = "FROM Employee e WHERE e.name Like :name and e.id > :id";
//甚至hql里面的参数可以是一个对象
Department department = new Department();
department.setId(1);
//1. 创建Query对象
Query query = session.createQuery(hql);
//2. 绑定参数,注意由于这里setxx()后返回的还是该Query对象,所以可以不断的在后追加set方法
query.setParameter(0, "%j%").setParameter(1, 2).setParameter(2, department);
// query.setParameter("name", "%j%").setParameter("id", 4);
//3. 执行查询
List<Employee> list = query.list();
System.out.println(list.size());
}
上面已经提供了两者查询的方式,一个是基于参数的索引(0,1,2…),另一种是基于命名参数的,当然这里基于命名参数的查询方式除了上面直接在java中写,也可以放到映射文件中写,用<query>
标签进行配置,和<class>
标签同级,比如上面的可以在Employee的映射文件中添加:
<!--如果在xml文档中大于号小于号会有歧义,必须使用<![CDATA[xxx]]>进行包裹处理-->
<query name="nameQuery">FROM Employee e WHERE e.name Like :name and e.id > :id</query>
注意使用命名参数的方式,然后在查询的时候如下操作:
@Test
public void testNameQuery() {
//根据name获取命名查询的语句
Query<Employee> query = session.getNamedQuery("nameQuery");
//设置查询的条件
query.setParameter("name", "%j%").setParameter("id", 2);
//执行查询
List<Employee> list = query.list();
System.out.println(list);
}
第二种是分页查询,值得一说的是Hibernate提供的分页查询是不依赖具体底层数据库的类型,所以具有很好的移植性,代码用法:
@Test
public void testPageQuery() {
String hql = "FROM Employee";
Query<Employee> employeeQuery = session.createQuery(hql);
//设置分页查询时的页码,这里就查询第二页的内容
int pageNo = 2;
//设置分页查询时每页显示的记录数
int pageSize = 5;
/*
关于分页查询,主要是Query对象的两个方法(自己手写过分页的同学应该很容易理解):
setFirstResult():这个就是查询页面第一条记录的索引号
setMaxResults():设置分页查询时每页显示的记录数(应该说最大记录数,因为最后一页可能不全)
*/
List<Employee> employees = employeeQuery.setFirstResult((pageNo-1)*pageSize).setMaxResults(pageSize).list();
System.out.println(employees);
}
第三种是映射查询,就是一个对象中有多个属性对吧,但是我查询的时候不需要这么多,只想要几个指定的字段,注意投影查询返回的是数组的形式,两种方法,推荐后者,好了来搞吧:
/*
下面两种是投影查询
*/
//测试返回自定义数组的集合
@Test
public void testAuSet() {
String hql = "SELECT e.id,e.name FROM Employee e WHERE e.department = ?";
//注意这里就不是返回的Employee对象了,而是自定义的由id和name组成的数组:[id, name](可以是任意你需要的字段的组合)
Query query = session.createQuery(hql);
Department d = new Department();
d.setId(1);
//主要在新版本中所有的类型设置直接全部用Parameter来取代
query.setParameter(0, d);
/*执行查询,注意这里的是查询的是数组的集合
由name和id组合成的数组是一个单独的个体,由
这个个体组成的集合就是封装到List中
*/
List<Object[]> lists = query.list();
for(Object[] l:lists) {
System.out.println(Arrays.asList(l));
}
}
/*
上面的方法是将的所需要的字段封装成一个组数的,那样的方式还不是很符合OOP的思想
最理想的就是我直接以我需要的字段给它们封装到一个对象中,提供getter和setter的方法
这样就不需要再通过数组去封装,还需要通过下标的方式去获取,其实在原来的实体类中
就提供了各个属性的setter和getter的方法,所以我们只需要在原来的实体类中为我们所需要
的字段提供一个新的构造器即可达到这样的目的。
*/
@Test
public void testEntity() {
//直接将id和name用Employee提供的构造器封装为一个Employee对象
String hql = "SELECT new Employee(e.id,e.name) FROM Employee e WHERE e.department = ?";
Department d = new Department();
d.setId(1);
Query<Employee> employeeQuery = session.createQuery(hql);
employeeQuery.setParameter(0, d);
List<Employee> employeeList = employeeQuery.list();
for(Employee e: employeeList) {
System.out.println(e);
}
}
第四种是报表查询,即可以使用最大、最小、排序等功能,好了直接看代码:
@Test
public void testXml() {
String hql = "SELECT min(e.id),max(e.id) FROM Employee e GROUP BY e.department HAVING min(id) > ?";
Query query = session.createQuery(hql).setParameter(0, 2);
//注意这里是返回的数组
List<Object[]> list = query.list();
for(Object[] object: list) {
System.out.println(Arrays.asList(object));
}
}
第五种是迫切左外连接查询,这玩意儿原谅我解释不清楚( ̄ ‘i  ̄;),知道怎么用就好了,下面直接通过代码来搞才是最实在的:
先来瞥一眼数据库中的部门表和员工表吧:
@Test
public void testLeftJoinFetch() {
//注意迫切左外连接查询的写法,左表是Department,右表是Employees
String hql = "FROM Department d LEFT JOIN FETCH d.employees";
Query query = session.createQuery(hql);
List list = query.list();
//这样查询出来的结果所有部门含有的员工数(这里的员工数是按照各个部门的ID去查询的,所以有些员工
// 没有关联部门ID的话是不算在里面的)加上空部门数(所谓的空部门就是指这个部门中没有人),简单的说
///就是满足条件的记录和左表里面不满足条件的记录综合。
System.out.println(list.size());
}
这里运行出来的结果为13,注意结合表,首先我先去查Department,有6个部门,D1、D2、D3这个3个部门员工,D3~D6的3个部门没有员工,,而在hql中我们fetch的是d.employees
就是要求部门中要有员工,D3~D6这3个部门是不满足条件的,这里先加上不满足的3个部门,然后根据D1~D3这3个部门去Employee表中去查询属于他们部门的员工,1~9和11总共10个,而12是没有部门的不算,所以3+10=13,最终结果是13,但是13个记录明显是不对的,因为我期盼的结果是6,13条记录里面部门有重复的,D1出现了4次,D2出现了4次,D3出现了2次,所以需要去重复,hql语句应该加上distinct
关键字处理如下:
String hql = "SELECT distinct d FROM Department d left join FETCH d.employees";
这样出来的结果就是不包含重复的了,结果为6。
上面对迫切左外连接做了用法的说明,类似的还有一个迫切内连接,只需要将hql中的’left join’改为inner join
即可,结果为3,这个3指的就是D1~D3,迫切内连接就把上面的左表不满足条件的D4~D6这3个部门不计算在内了。
最后做一点必要的说明,在上面的栗子中,只是对HQL的查询做了简单查询操作,当然也可以增删改查,具体的参照文档,不作赘述。
7.2 QBC检索方式
QBC通过Hibernate提供的Query By Criteria的API来检索,它是基于字符串的查询方式,更加符合OOP的思想。QBC的一般步骤如下:
- 创建Criteria对象:
session.createCriteria(Entity.class)
- 添加查询条件:
criteria.add(Restrictions.xx)
- 执行操作:
Entity e = (Entity) criteria.uniqueResult()
还是以上面的栗子数据表为例,做如下的查询操作和用法说明:
@Test
public void testQBC() {
//1. 创建Criteria对象
Criteria criteria = session.createCriteria(Employee.class);
//2. 添加查询操作
/*
eq:等于
gt:大于
*/
criteria.add(Restrictions.eq("name", "gzx"));
criteria.add(Restrictions.gt("id", 5));
//3.执行查询,注意要执行类型的强转
Employee employee = (Employee) criteria.uniqueResult();
System.out.println(employee);
}
@Test
public void testAndOr() {
Criteria criteria = session.createCriteria(Employee.class);
//1. 带And的查询
Conjunction conjunction = Restrictions.conjunction();
conjunction.add(Restrictions.like("name", "a", MatchMode.ANYWHERE));
conjunction.add(Restrictions.eq("id", 2));
System.out.println(conjunction);
//2. 带Or的查询
Disjunction disjunction = Restrictions.disjunction();
disjunction.add(Restrictions.ge("id", 4));
disjunction.add(Restrictions.isNotNull("name"));
System.out.println(disjunction);
//将条件添加到criteria中
criteria.add(disjunction);
criteria.add(conjunction);
//执行查询
criteria.list();
}
//测试统计查询
@Test
public void testReport() {
Criteria criteria = session.createCriteria(Employee.class);
criteria.setProjection(Projections.max("id"));
System.out.println(criteria.uniqueResult());
}
//排序、分页
@Test
public void testPage() {
Criteria criteria = session.createCriteria(Employee.class);
//添加排序
// criteria.addOrder(Order.asc("id"));
criteria.addOrder(Order.desc("id"));
//添加分页
int pageNo = 2;
int pageSize = 5;
criteria.setFirstResult((pageNo-1)*pageSize).setMaxResults(pageSize);
List<Employee> list = criteria.list();
System.out.println(list);
}
7.3 本地SQL检索
Hibernate除了支持上面的HQL和QBC两种高级查询和方式外,为了避免不能全覆盖的问题,Hibernate也支持本地SQL检索的方式,直接写SQL,相对比较简单,下面利用这种方式检索栗子:
@Test
public void nativeSQL() {
String sql = "select e.name from employees e where e.id = ?";
Query query = session.createSQLQuery(sql);
query.setParameter(0, 2);
String name = (String) query.uniqueResult();
System.out.println(name);
}
8. Hibernate的二级缓存
之前对Hibernate的一级缓存(即Session缓存)做了介绍,这里对二级缓存做一个介绍,二级缓存其实就是SessionFactory
缓存,之前做过这样的测试流程:
@Test
public void testCache2() {
Employee e = session.get(Employee.class, 1);
System.out.println(e.getName());
tx.commit();
session.close();
session = sessionFactory.openSession();
tx = session.beginTransaction();
Employee e2 = session.get(Employee.class, 1);
System.out.println(e2.getName());
}
通过控制台可以发现Hibernate发送了两次SQL查询,如果拿掉事务和session的相关代码,两次查询很明显只会发送一次SQL进行查询(由于Session缓存的原因),因为Session实例不一样了,那么如果就算加上事务和Session关闭和重开的代码,我们希望仍然只发送一次SQL,那么我希望这个缓存是在SessionFactory中,因为Session是由SessionFactory产生的,Session关闭会重新搞一个Session,那么里面的缓存肯定也没了,但是放在SessionFactory中就不一样了,只要SessionFactory不关闭缓存就不会不清除,自然也就能达到上面的要求了。
二级缓存默认是不开启的,但是可以通过插件的形式开启,外置缓存中的数据是数据库中的数据的复制,这些缓存数据可以放在内存中或者硬盘上。二级缓存的使用长场景:读多写少,并发量很小的。值得注意的是Hibernate并没有实现二级缓存,所以使用它的二级缓存必须需要有如EH、OS Cache第三方插件来实现。这里使用EH来做测试,需要额外的jar有:hibernate-ehcache-5.2.12.Final.jar、slf4j-api-1.7.7.jar、ehcache-2.10.3.jar。加入上述的jar包后,做如下的配置步骤:
- 添加jar包:将Hibernate根目录下lib\optional\ehcache中的jar复制到Moudle中;
- 添加EH的配置文件:Hibernate根目录下的project\etc\ehcache.xml为默认配置文件;
- 配置Hibernate的核心配置文件中二级缓存的内容:启用二级缓存、配置使用二级缓存的产品、配置使用二级缓存的类。
下面是具体的配置文件:
EH配置文件和对应参数说明如下
<!--ehcache.xml-->
<!--
~ Hibernate, Relational Persistence for Idiomatic Java
~
~ License: GNU Lesser General Public License (LGPL), version 2.1 or later.
~ See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
-->
<ehcache>
<!--配置磁盘的存储路径,默认是存储到缓存中,但是数据量比较多的时候会比较耗费内存,所以
比较常规的做法是搞一个临界值,缓存量小于临界值可以直接放到内存中,超出临界值就将它
放到硬盘上,这个路径就是配置的缓存到硬盘的某个路径下-->
<diskStore path="F:\\testTemp"/>
<!--默认的缓存策略,具体解释参照下面的自定缓存策略-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
/>
<!-- 设定具体的命名缓存的数据的过期策略,每个命名缓存代表一个缓存区域,
缓存区域:是一个具具有名称的缓存块,可以给每个缓存块设置不同的缓存
策略,如果没有设置任何缓存区域,则所有的缓存对象都将使用默认的缓存策略(就是上面的配置)
Hibernate在不同的缓存区保存不同的类/集合
对于类,区域的名称就是全类名
对于集合,区域的名称就是集合所在类的全类名+集合属性名
内部属性的各个具体含义:
name - 设置缓存区的名字,就是上面的缓存区域名
maxInMemory - 设置内存中最大可缓存的对象数量
eternal - 设置对象是否永久缓存,true表示永不过期(timeToIdleSeconds和timeToLiveSeconds属性将失效),默认为false
timeToIdleSeconds - 设置对象空闲的最长时间(单位为秒),缓存对象超出该时间会自动被清除,默认为0,表示缓存对象可以无限时长存活在缓存中
timeToLiveSeconds - 设置对象最长的生存周期(单位为秒),缓存对象超出该时间会自动被清除,默认为0,表示缓存对象可以无限时长存活在缓存中,该值必须大于timeToIdleSeconds
overflowToDisk - 设置缓存对象在达到内存可允许存储的最大数量(即maxInMemory)后,是否将溢出的缓存对象写入硬盘上
-->
<!--配置类的缓存区-->
<cache name="com.hhu.entities.Employee"
maxElementsInMemory="1"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
/>
<!--配置集合的缓存区-->
<cache name="com.hhu.entities.Department.employees"
maxElementsInMemory="1000"
eternal="true"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
/> -->
</ehcache>
然后hibernate的核心配置文件中需要添加如下的配置:
<!--启用二级缓存-->
<property name="cache.use_second_level_cache">true</property>
<!--配置二级缓存的产品,注意EhCacheRegionFactory的类路径,不同的版本可能不一样-->
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
<!--配置哪个类使用二级缓存,这里配置的是对Employee这个类使用二级缓存,其他类是无法使用二级缓存的-->
<class-cache class="com.hhu.entities.Employee" usage="read-write"/>
上面是一般的类级别的二级缓存配置方式,需要注意的还有集合级别的二级缓存级别的配置,先看测试代码:
@Test
public void Cache2() {
Department department = session.get(Department.class, 1);
System.out.println(department.getName());
System.out.println(department.getEmployees().size());
//重开session
tx.commit();
session.close();
session = sessionFactory.openSession();
tx = session.beginTransaction();
Department department2 = session.get(Department.class, 1);
System.out.println(department2.getName());
System.out.println(department2.getEmployees().size());
}
很明显要配置一下让Department类使用二级缓存才可以,因为用到了Department中的employees集合,所以还需要对集合进行二级缓存的配置,最重要的是还必须对集合里面的元素对象进行而二级缓存的配置,这一点比较容易忘,配置如下:
<!--配置哪个类使用二级缓存-->
<!--这个集合里面的元素配置不能丢弃-->
<class-cache class="com.hhu.entities.Employee" usage="read-write"/>
<class-cache class="com.hhu.entities.Department" usage="read-write"/>
<!--给集合类配置二级缓存,注意是集合所在类名+集合属性名的方式,注意配置集合的二级缓存时不但要配置集合本身的二级缓存,还必须配置集合里面的元素对象的二级缓存(这里指Employee)才能生效,否则只会更加增加SQL的发送频率
-->
<collection-cache collection="com.hhu.entities.Department.employees" usage="read-write"/>
注意集合collection
属性的写法,集合所在的全类名+集合属性名。
配置完上述的二级缓存后,利用上面的Junist代码从新跑一遍(EH的配置文件先用默认的去跑,然后再用上面我的配置去跑),由于我的Employee类缓存在内存中只允许存放1个对象,所以超出的肯定会写到我的F盘的路径下,但是要看到缓存文件必须在SessionFactory关闭处设置一个断点或者写一个线程睡眠函数才能看到,因为既然是二级缓存(SessionFactory缓存),在SessionFactory关闭后,缓存文件会自动被删除,现然不可见。
8.1 关于查询缓存(HQL和QBC的二级缓存的配置)
上面是简单的Session.get()
通过类的类型和实体类的ID进行查询,通过上面基础的配置可以达到二级缓存的目的,但是对于HQL和QBC查询时是无用的,需要额外的配置才能使用二级缓存。注:这里对于HQL和QBC的查询方说实话我分不太清,这里可以直接看关键字做简要区分,HQL查询会用到Query对象(具体点说肯定会有session.createQuery()
这样的语句出现),而QBC查询方式必定会有Criteria对象(具体点说肯定会有session.createCriteria
这样的语句出现)。注意如果需要使用查询缓存就必须使用二级缓存,对于HQL和QBC两种查询方式额外的配置是:
- 在Hibernate的核心配置文件hibernate.cfg.xml文件中必须配置启用查询缓存:
<!--配置启用查询缓存-->
<property name="cache.use_query_cache">true</property>
- 在创建
Query
或者Criteria
对象时必须设定它们的查询缓存是可用的
//Query的可缓存
query.setCacheable(true);
//Criteria的可缓存
criteria.setCacheable(true);
测试代码如下:
//测试查询缓存使用HQL
@Test
public void queryCache() {
Query query = session.createQuery("from Employee");
//设置查询缓存,而且需要设置启用查询缓存
query.setCacheable(true);
Criteria criteria = session.createCriteria("FROM Employee");
criteria.setCacheable(true);
List<Employee> list = query.list();
System.out.println(list);
Query query2 = session.createQuery("from Employee");
List<Employee> list2 = query.list();
System.out.println(list2);
}
//测试查询缓存使用QBC
@Test
public void testQBCCACHE() {
//创建Criteria对象
Criteria criteria = session.createCriteria(Employee.class);
criteria.setCacheable(true);
//设置查询条件
//执行查询操作
List<Employee> list = criteria.list();
System.out.println(list);
//创建Criteria对象
Criteria criteria2 = session.createCriteria(Employee.class);
criteria2.setCacheable(true);
//设置查询条件
//执行查询操作
List<Employee> list2 = criteria.list();
System.out.println(list2);
}
上面如果不设置Query的可缓存属性为true
那么将会发送两条SQL,设置之后只发送一条SQL,这里我有一个疑问,是不是Session只能缓存是实体类的对象,而不能缓存List这样的集合属性,不然怎么这里就不缓存只查询一次而是两次呢,希望懂的同学指点一下迷津。
8.2 时间戳缓存
对于时间戳缓存先不说多,直接先上一个上面做了小改动的代码(但是配置不变,仍然是二级缓存):
@Test
public void testQBCCACHE() {
//创建Criteria对象
Criteria criteria = session.createCriteria(Employee.class);
criteria.setCacheable(true);
//设置查询条件
//执行查询操作
List<Employee> list = criteria.list();
System.out.println(list.size());
Employee e = session.get(Employee.class, 2);
e.setName("test2");
//创建Criteria对象
Criteria criteria2 = session.createCriteria(Employee.class);
criteria2.setCacheable(true);
//设置查询条件
//执行查询操作
List<Employee> list2 = criteria.list();
System.out.println(list2.size());
}
控制台的SQL语句发送情况如下:
Hibernate:
select
this_.id as id1_1_0_,
this_.NAME as NAME2_1_0_,
this_.DEPARTMENT_ID as DEPARTME3_1_0_
from
EMPLOYEES this_
11
Hibernate:
update
EMPLOYEES
set
NAME=?,
DEPARTMENT_ID=?
where
id=?
Hibernate:
select
this_.id as id1_1_0_,
this_.NAME as NAME2_1_0_,
this_.DEPARTMENT_ID as DEPARTME3_1_0_
from
EMPLOYEES this_
11
可以发现是一个select中间夹了一个update,我们可以容易看出这个逻辑,改动之后肯定需要再进行查询一次,但是内部他怎么知道要重新查询一次呢,这就是时间戳缓存的功能了。
时间戳缓存存放了对于查询结果相关的表进行插入、更新或删除操作的时间戳,Hibernate通过时间戳缓存判断查询结果是否过期,运行流程如下:
- T1时刻查询时将查询结果放到QueryCache中,记录该缓存区的时间戳为T1;
- T2时刻对查询结果相关的表进行了更新操作,Hibernate将T2放在UpdateTimestampCache区域
- T3时刻再次查询时,会先比较T1和T2,如果T1