透过测试用例和执行结果,让你正确推测和理解Session中Load和get的区别,不再困惑
通过测试用例和执行结果,让你正确推测和理解Session中Load和get的区别,不再困惑
如果设置lazy="true",执行结果是:
好累啊!终于测完了这几种情况,也弄懂了load和get的区别了。发出来跟大家共享下,如果理解的有错误,欢迎大牛们指正。
我们知道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>现在我们来介绍load和get的区别吧
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()
{
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-o-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的区别了。发出来跟大家共享下,如果理解的有错误,欢迎大牛们指正。
- 2楼cjr15233661143昨天 08:58
- 好文章,收藏了
- 1楼5iasp昨天 08:24
- 不错,学习了。