我们知道Session是Hibernate框架的核心类,也是初学hibernate时最重要、接触最多的类,它提供了load()和get()方法,根据主键从数据库中查询记录。这2个方法存在一些特性和差别,需要开发者注意,否则很容易出错。网上有很多介绍load和get区别的帖子,虽然很多帖子介绍的都很好,但遗憾的是缺少对应单元测试和执行结果的佐证,而且有些博客之间还是相互矛盾的。出现矛盾,往往是大家使用的Hibernate版本不一致,或者不同的人的理解不一致。如果出现这种情况,不知道该相信谁,最好的解决方式就是:自己去搭建测试环境,按照自己的理解去编写单元测试,看看实际的执行结果是否符合预期。在这种推测-测试-理解中,我们能够加深对所学知识的理解。扯的有点远呵呵,进入正题吧。
我使用是hibernate4.1.6版本和mysql-essential-5.0.87数据库,测试是基于student表的,只有3条记录.
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>
<!-- Database connection settings -->
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost/hibernate</property>
<property name="connection.username">root</property>
<property name="connection.password">root</property>
<!-- JDBC connection pool (use the built-in) -->
<property name="connection.pool_size">1</property>
<!-- SQL dialect -->
<property name="dialect">org.hibernate.dialect.MySQLDialect</property>
<!-- Enable Hibernate's automatic session context management -->
<property name="current_session_context_class">thread</property>
<!-- Disable the second-level cache -->
<property name="cache.use_second_level_cache">true</property>
<property name="hibernate.cache.use_query_cache">true</property>
<property name="cache.region.factory_class">org.hibernate.cache.EhCacheRegionFactory</property>
<!-- Echo all executed SQL to stdout -->
<property name="show_sql">true</property>
<property name="format_sql">true</property>
<!-- Drop and re-create the database schema on startup -->
<property name="hbm2ddl.auto">update</property>
<mapping resource="hibernate/Student.hbm.xml" />
</session-factory>
</hibernate-configuration>
Student的实体映射文件如下,默认使用了延迟加载,如果不需要,对应的单元测试会提示关闭.
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="hibernate.Student" lazy="true">
<cache usage="read-only" />
<id name="id" />
<property name="name" />
<property name="age" />
</class>
</hibernate-mapping>
1、如果没有查询到记录,load会报异常,get返回null
public class TestLoadAndGetOfSession
{
private SessionFactory sessionFactory = new Configuration().configure()
.buildSessionFactory();
@Test
public void testNotExsitUseGet()
{
Session session = sessionFactory.openSession();
Student student = (Student) session.get(Student.class, 100);
System.out.println(student);// null
session.close();
}
@Test
public void testNotExsitUseLoad()
{
Session session = sessionFactory.openSession();
// org.hibernate.ObjectNotFoundException: No row with the given
// identifier exists
Student student = (Student) session.load(Student.class, 100);
System.out.println(student);
session.close();
}
}
数据中没有主键100对应的记录,load会抛异常,get返回null。显然这种情况下,get方法的表现更符合调用者的预期。为什么找不到数据,load会抛异常呢?这主要跟load使用到代理有关,后面在介绍吧。
2、load可以使用实体对象的延迟加载,get不能使用延迟加载
@Test
// 测试该方法需要将Student.hbm.xml中lazy设置成false或true
public void testProxyWhetherNotLazy()
{
Session session = sessionFactory.openSession();
// 1.没有延迟加载的情况下,load和get都会发出sql语
// 2.使用延迟加载,load不会发出sql,get会发出sql查询
session.get(Student.class, 1);
session.load(Student.class, 2);
session.close();
}
如果实体对象配置了lazy="true",则load会使用延迟加载,真正需要使用到数据的时候才会发出sql查询,而get方法不依赖与是否使用延迟加载,一旦调用get,就会发出sql语句。注意:这里不考虑一级缓存和二级缓存的影响,因为如果有缓存的话且能够命中,load和get都不会发出sql查询。
3、load一定返回实体对象的代理,get一定返回实体对象本身吗?不一定,从一级缓存来看
@Test
// 需要设置Student.hbm.xml中lazy属性,是否使用延迟加载
public void testClassWhetherUseLazy()
{
<span style="white-space:pre"> </span>Session session = sessionFactory.openSession();
Student student1 = (Student) session.get(Student.class, 1);
Student student2 = (Student) session.load(Student.class, 1);
System.out.println("student1==" + student1.getClass());
System.out.println("student2==" + student2.getClass());
Student student3 = (Student) session.load(Student.class, 2);
Student student4 = (Student) session.get(Student.class, 2);
System.out.println("student3==" + student3.getClass());
System.out.println("student4==" + student4.getClass());
session.close();
}
如果设置lazy="false",执行结果是:
Hibernate:
select
student0_.id as id0_0_,
student0_.name as name0_0_,
student0_.age as age0_0_
from
Student student0_
where
student0_.id=?
student1==class hibernate.Student
student2==class hibernate.Student
Hibernate:
select
student0_.id as id0_0_,
student0_.name as name0_0_,
student0_.age as age0_0_
from
Student student0_
where
student0_.id=?
student3==class hibernate.Student
student4==class hibernate.Student
如果设置lazy="true",执行结果是:
Hibernate:
select
student0_.id as id0_0_,
student0_.name as name0_0_,
student0_.age as age0_0_
from
Student student0_
where
student0_.id=?
student1==class hibernate.Student
student2==class hibernate.Student
Hibernate:
select
student0_.id as id0_0_,
student0_.name as name0_0_,
student0_.age as age0_0_
from
Student student0_
where
student0_.id=?
student3==class hibernate.Student_$$_javassist_0
student4==class hibernate.Student_$$_javassist_0
对比以上执行结果,我们可以得到以下结论:
- 如果实体对象不使用延迟加载,load和get返回的都是对象本身,两者没有区别。也就说是否使用代理,取决于是否配置了延迟加载。
- load和get执行结果都会存入session缓存(一级缓存)。先执行get,后执行load,由于get已经将全部实体信息存入了缓存,之后load的时候就没有必要再去创建代理了,直接使用get存入的缓存即可,这种情况下,get和load返回的都是实体对象本身,如student1和student2。
- 先执行load,后执行get。由于开启了延迟加载,load会返回代理对象并将代理对象存入缓存,不过由于我们没有真正使用到实体对象的值,所以缓存的对象中,除了主键外,其余属性值都是空的,这一点可以通过执行load并没有发出sql来证明。之后使用get的时候,由于session缓存中已经有代理对象了,所以get方法直接使用代理。但是因为代理对象属性值都是空的,所以get会发出sql查询获取对应的值,然后更新到代理对象中。这种情况下load和get返回的都是代理对象。也就是说:load返回了空的代理对象,get发出sql为代理对象的属性赋值。
4、如果开启了二级缓存,load和get都会使用二级缓存
很多帖子说,get不会利用二级缓存,load会使用二级缓存。这其实是不对,通过我的测试,发现load和get都能够有效的使用二级缓存。
@Test
public void testSecondCache()
{
Session anotherSession = sessionFactory.openSession();
Student anotherStu = (Student) anotherSession.get(Student.class, 2);
System.out.println("模拟别的session查询:" + anotherStu);
anotherSession.close();
System.out.println("----------测试load-----------");
// 在新的session中使用load,开启了二级缓存,不会再发出sql
Session loadSession = sessionFactory.openSession();
Student loadStu = (Student) loadSession.load(Student.class, 2);
System.out.println("load查询,session中无缓存" + loadStu);
loadSession.close();
System.out.println("----------测试get-----------");
// 在新的session中使用get,开启了二级缓存,不会再发出sql
Session getSession = sessionFactory.openSession();
Student getStu = (Student) getSession.get(Student.class, 2);
System.out.println("get查询,session中无缓存" + getStu);
getSession.close();
}
执行该方法需要开启hibernate的二级缓存配置,执行结果如下:
Hibernate:
select
student0_.id as id0_0_,
student0_.name as name0_0_,
student0_.age as age0_0_
from
Student student0_
where
student0_.id=?
模拟别的session查询:hibernate.Student@40b181[id=2, name=zhangsan111, age=18]
----------测试load-----------
load查询,session中无缓存hibernate.Student@14ed577[id=2, name=zhangsan111, age=18]
----------测试get-----------
get查询,session中无缓存hibernate.Student@a09e41[id=2, name=zhangsan111, age=18]
很显然,3个不同的session,都查询id=2的student,只发出了一条sql语句。这就是说load和get都会使用二级缓存。
5、load一定返回实体对象的代理,get一定返回实体对象本身吗?不一定,从二级缓存来看
现在我们来讨论下,第3条结论中的遗留问题:二级缓存和延迟加载对load和get的影响,主要是生成代理对象还是对象本身的问题。@Test
// 测试二级缓存、延迟加载对load和get是否生成代理对象的差别,与testClassWhetherUseLazy对应
public void testProxyWhenUsetSecondCache()
{
// 使用二级缓存
Session oneSession = sessionFactory.openSession();
Student s1 = (Student) oneSession.get(Student.class, 1);
Student s2 = (Student) oneSession.load(Student.class, 2);
System.out.println("s1==" + s1.getClass());
System.out.println("s2==" + s2.getClass());
oneSession.close();
Session twoSession = sessionFactory.openSession();
Student student1 = (Student) twoSession.load(Student.class, 1);
System.out.println("student1 == " + student1.getClass());
Student student2 = (Student) twoSession.get(Student.class, 2);
System.out.println("student2 == " + student2.getClass());
twoSession.close();
}
如果lazy=false,效果跟第三条结论一样:无论是get还是load返回的都是实体对象本身,因为这时返回代理已经没有什么意义,只能是浪费空间和性能。
如果lazy=true,执行结果如下:
Hibernate:
select
student0_.id as id0_0_,
student0_.name as name0_0_,
student0_.age as age0_0_
from
Student student0_
where
student0_.id=?
s1==class hibernate.Student
s2==class hibernate.Student_$$_javassist_0
student1 == class hibernate.Student_$$_javassist_0
Hibernate:
select
student0_.id as id0_0_,
student0_.name as name0_0_,
student0_.age as age0_0_
from
Student student0_
where
student0_.id=?
student2 == class hibernate.Student
通过执行结果可以看出,我们在第3条结论中的分析结果不适用于二级缓存的情况。在使用二级缓存和延迟加载的情况下,load永远返回的是代理对象,而get返回的是实体对象本身。
不知道为什么会这样?感觉有点奇怪,一级缓存和二级缓存的这种特性,是hibernate的bug,还是有意为之呢?
6、一级缓存,直接获取内存中的缓存对象
@Test
public void testSameObjectInSession()
{
// 在新的session中使用get,开启了二级缓存,不会再发出sql
Session getSession = sessionFactory.openSession();
Student getStu1 = (Student) getSession.get(Student.class, 2);
System.out.println("getStu1==" + getStu1);
Student getStu2 = (Student) getSession.get(Student.class, 2);
System.out.println("getStu2==" + getStu2);
getSession.close();
System.out.println(getStu1 == getStu2);// true
System.out.println(getStu1.hashCode() == getStu2.hashCode());// true
}
@Test
public void testSameObjectInSession2()
{
// 在新的session中使用get,开启了二级缓存,不会再发出sql
Session getSession = sessionFactory.openSession();
Student getStu1 = (Student) getSession.load(Student.class, 2);
//System.out.println("getStu1==" + getStu1);
Student getStu2 = (Student) getSession.get(Student.class, 2);
System.out.println("getStu2==" + getStu2);
getSession.close();
System.out.println(getStu1 == getStu2);// true
System.out.println(getStu1.hashCode() == getStu2.hashCode());// true
}
无论是否配置延迟加载效果都是一样的。这说明session级别的缓存,是共享同一个内存对象的。
7、二级缓存,具有copy-on-write特征
@Test
// 缓存确实命中了,但返回的对象hashcode不相同,这说明获取缓存的时候,使用了copy-on-write.
// 这样是合理的,防止1个session改动了数据,影响到其他session中对应的数据
public void testNotSameObject()
{
// 在新的session中使用get,开启了二级缓存,不会再发出sql
Session getSession = sessionFactory.openSession();
Student getStu = (Student) getSession.get(Student.class, 2);
System.out.println("getStu==" + getStu);
getSession.close();
// 使用了二级缓存,不会发出sql语句
Session getSession2 = sessionFactory.openSession();
Student getStu2 = (Student) getSession2.get(Student.class, 2);
System.out.println("getStu2==" + getStu2);
getSession2.close();
System.out.println(getStu == getStu2);// false
System.out.println(getStu.hashCode() == getStu2.hashCode());// false
}
执行结果如下:
Hibernate:
select
student0_.id as id0_0_,
student0_.name as name0_0_,
student0_.age as age0_0_
from
Student student0_
where
student0_.id=?
getStu==hibernate.Student@31ff23[id=2, name=zhangsan111, age=18]
getStu2==hibernate.Student@1a3aa2c[id=2, name=zhangsan111, age=18]
false
false
发现不同的session直接,的确命中了二级缓存,因为只查了一次数据库。但是不同session中获取的对象是不同的,这也就是说,二级缓存具有cpoy-on-write特征。如果session发现了二级缓存中有需要的数据,那么会直接新建1个对象,然后用二级缓存中对应的实体对象,对新创建的对象进行赋值。好累啊!终于测完了这几种情况,也弄懂了load和get的区别了。发出来跟大家共享下,如果理解的有错误,欢迎大牛们指正。