1 验证一级缓存
1.2 方案
- 用同一个Session查询同一条数据2次,如果只查询一次数据库,则验证了一级缓存的存在。
- 用2个不同的Session,分别查询同一条数据,如果查询2次数据库,则验证了一级缓存是Session独享的。
- 在com.tarena.test包下创建测试类TestFirstCache,在这个测试类中分别写出方案中提到的2个测试方法,用以验证一级缓存的存在及特性,代码如下:
- package com.tarena.test;
- import org.hibernate.Session;
- import org.junit.Test;
- import com.tarena.entity.Emp;
- import com.tarena.util.HibernateUtil;
11.public class TestFirstCache {
- /**
- * 用同一个Session查询同一条数据2次,
- * 如果只查询一次数据库,则验证了一级缓存的存在。
- */
- @Test
- public void test1() {
- Session session = HibernateUtil.getSession();
- Emp e1 = (Emp) session.get(Emp.class, 321);
- System.out.println(e1.getName());
- System.out.println("----------------");
- Emp e2 = (Emp) session.get(Emp.class, 321);
- System.out.println(e2.getName());
- session.close();
- }
- /**
- * 用2个不同的Session,分别查询同一条数据,
- * 如果查询2次数据库,则验证了一级缓存是Session独享的。
- */
- @Test
- public void test2() {
- Session session1 = HibernateUtil.getSession();
- Emp e1 = (Emp) session1.get(Emp.class, 321);
- System.out.println(e1.getName());
- Session session2 = HibernateUtil.getSession();
- Emp e2 = (Emp) session2.get(Emp.class, 321);
- System.out.println(e2.getName());
- session1.close();
- session2.close();
- }
45.}
2 管理一级缓存
2.1 问题
掌握一级缓存管理的2种方式:
- 使用evict方法,从一级缓存中移除一个对象。
- 使用clear方法,将一级缓存中的对象全部移除。
设计出案例,来使用并验证一级缓存管理方法。
2.2 方案
设计2个案例,使用同一个Session查询同一条数据2次,由于一级缓存的存在,第二次查询时将从一级缓存中取数,而不会查询数据库。
那么,如果在第二次查询之前将数据从缓存中移除,第二次查询时就会访问数据库。在这两个案例中,我们分别使用evict和clear方法将数据从缓存中移除。
2.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:在TestFirstCache中增加测试案例代码
在TestFirstCache中增加2个方法,均使用同一个Session查询同一条数据2次,在第二次查询之前,分别使用evice和clear方法移除缓存数据,代码如下:
package com.tarena.test;
import org.hibernate.Session;
import org.junit.Test;
import com.tarena.entity.Emp;
import com.tarena.util.HibernateUtil;
public class TestFirstCache {
/**
* 验证缓存管理的方法evict
*/
@Test
public void test3() {
Session session = HibernateUtil.getSession();
Emp e1 = (Emp) session.get(Emp.class, 321);
System.out.println(e1.getName());
session.evict(e1);
Emp e2 = (Emp) session.get(Emp.class, 321);
System.out.println(e2.getName());
session.close();
}
/**
* 验证缓存管理的方法clear
*/
@Test
public void test4() {
Session session = HibernateUtil.getSession();
Emp e1 = (Emp) session.get(Emp.class, 321);
System.out.println(e1.getName());
session.clear();
Emp e2 = (Emp) session.get(Emp.class, 321);
System.out.println(e2.getName());
session.close();
}
3 验证持久态对象的特性
3.1 问题
设计出案例,验证持久态对象的特性:
- 持久态对象存在于一级缓存中。
- 持久态对象可以自动更新至数据库。
- 持久态对象自动更新数据库的时机是session.flush()。
3.2 方案
设计3个案例,分别验证持久态对象的3个特性:
- 新增一条数据,在新增后该数据对象为持久态的,然后根据对象的ID再次查询数据。执行时如果控制台不重新输出SQL则验证了持久态对象存在于一级缓存。
- 新增一条数据,在新增后该对象为持久态的,然后修改这个对象的任意属性值,并提交事务。在执行时,如果发现数据库中的数据是修改后的内容,则验证了持久态对象可以自动更新至数据库。
- 查询一条数据,该数据对象为持久态的,然后修改对象的任意属性值,再调用session.flush()方法,并且不提交事务。如果执行时控制台输出更新的SQL,则验证了一级缓存对象更新至数据库的时机为session.flush()。
3.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:创建验证对象持久性测试类
在com.tarena.test包下,创建一个测试类TestPersistent,在其中写出验证对象持久性的3个测试方法,用以验证对象持久性的3个特性,代码如下:
package com.tarena.test;
import java.sql.Date;
import java.sql.Timestamp;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.junit.Test;
import com.tarena.entity.Emp;
import com.tarena.util.HibernateUtil;
public class TestPersistent {
/**
* 持久态对象存在于一级缓存中
*/
@Test
public void test1() {
Emp e = new Emp();
e.setName("唐僧");
e.setAge(29);
e.setMarry(false);
e.setSalary(12000.00);
e.setBirthday(
Date.valueOf("1983-10-20"));
e.setLastLoginTime(
new Timestamp(System.currentTimeMillis()));
Session session = HibernateUtil.getSession();
Transaction ts = session.beginTransaction();
try {
session.save(e);
ts.commit();
} catch (HibernateException e1) {
e1.printStackTrace();
ts.rollback();
}
Emp emp = (Emp) session.get(Emp.class, e.getId());
System.out.println(emp.getId() + " " + emp.getName());
session.close();
}
/**
* 持久态对象可以自动更新至数据库
*/
@Test
public void test2() {
Emp e = new Emp();
e.setName("孙悟空");
e.setAge(29);
e.setMarry(false);
e.setSalary(12000.00);
e.setBirthday(
Date.valueOf("1983-10-20"));
e.setLastLoginTime(
new Timestamp(System.currentTimeMillis()));
Session session = HibernateUtil.getSession();
Transaction ts = session.beginTransaction();
try {
session.save(e);
e.setName("猪八戒");
ts.commit();
} catch (HibernateException e1) {
e1.printStackTrace();
ts.rollback();
}
session.close();
}
/**
* 持久态对象自动更新数据库的时机
*/
@Test
public void test3() {
Session session = HibernateUtil.getSession();
Emp e = (Emp) session.load(Emp.class, 201);
e.setName("太上老君");
session.flush(); //同步但未提交事务
session.close();
}
}
4 验证延迟加载
4.1 问题
设计案例,验证session.load()方法和query.iterate()方法在查询时是采用延迟加载机制的。
4.2 方案
- 验证session.load()的延迟加载,可以先查询某条EMP数据,然后输出一个分割线,在分割线的后面再使用这个对象,输出它的一些属性值。在运行时,如果查询EMP的SQL输出在分割线的后面,则验证了该方法是采用延迟加载机制的。
- 验证query.iterate()的延迟加载,可以先查询出全部的EMP数据,然后输出一个分割线,在分割线的后面再遍历查询结果并输出每个对象的一些属性值。在运行时,如果查询EMP的SQL输出在分割线的后面,则验证了该方法是采用延迟加载机制的。
public class TestLazy {
/**
* 验证load方法是延迟加载的
*/
@Test
public void test1() {
Session session = HibernateUtil.getSession();
// load方法并没有触发访问数据库
Emp emp = (Emp) session.load(Emp.class, 321);
System.out.println("-----------------");
// 使用emp对象时才真正访问数据库
System.out.println(emp.getName());
session.close();
}
/**
* 验证iterate方法是延迟加载的
*/
@Test
public void test2() {
String hql = "from Emp";
Session session = HibernateUtil.getSession();
Query query = session.createQuery(hql);
// iterate方法访问了数据库,但只查询了ID列
Iterator<Emp> it = query.iterate();
System.out.println("-----------------");
while (it.hasNext()) {
Emp emp = it.next();
// 使用emp对象时才将其他列全部加载
System.out.println(emp.getName());
}
session.close();
}
}
5 在NETCTOSS中使用延迟加载
5.1 问题
请将NETCTOSS中资费模块DAO的实现,改用Hibernate中延迟加载的方法。
5.2 方案
将CostDaoImpl中的findById方法的实现,改为session.load()。
为了避免session提前关闭导致延迟加载出现问题,需要在findById方法中不关闭session,而是自定义拦截器,在拦截器中调用完action之后关闭session。自然地,资费模块的action都应该引用这个拦截器。
5.3 步骤
实现此案例需要按照如下步骤进行。
步骤一:使用延迟加载方法
将CostDaoImpl中的findById方法中,session.get()改为session.load(),代码如下:
package com.netctoss.dao;
import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Transaction;
import com.netctoss.entity.Cost;
import com.netctoss.util.HibernateUtil;
/**
* 当前阶段学习重点是Struts2,对于DAO的实现就模拟实现了。
* 同学们可以使用JDBC/MyBatis自行实现该DAO。
*/
public class CostDaoImpl implements ICostDao {
@Override
public List<Cost> findAll() {
String hql = "from Cost";
Session session = HibernateUtil.getSession();
Query query = session.createQuery(hql);
List<Cost> list = query.list();
session.close();
return list;
}
@Override
public void delete(int id) {
Cost cost = new Cost();
cost.setId(id);
Session session = HibernateUtil.getSession();
Transaction ts = session.beginTransaction();
try {
session.delete(cost);
ts.commit();
} catch (HibernateException e) {
e.printStackTrace();
ts.rollback();
} finally {
session.close();
}
}
@Override
public Cost findByName(String name) {
// 模拟根据名称查询资费数据,假设资费表中只有一条名为tarena的数据
if("tarena".equals(name)) {
Cost c = new Cost();
c.setId(97);
c.setName("tarena");
c.setBaseDuration(99);
c.setBaseCost(9.9);
c.setUnitCost(0.9);
c.setDescr("tarena套餐");
c.setStatus("0");
c.setCostType("2");
return c;
}
return null;
}
@Override
public Cost findById(int id) {
Session session = HibernateUtil.getSession();
#cold_bold Cost cost = (Cost) session.load(Cost.class, id);
session.close();
return cost;
}
}
步骤2:测试
重新部署项目,并重启tomcat,访问资费修改页面,效果如下图:
可以看出要修改的数据,只有ID显示正确,其他字段都为空。原因是我们的findById方法中直接关闭了session,而该方法返回的对象是在JSP中使用的,在使用时session已经关闭,由于延迟加载机制的存在,导致了这个问题的发生。要想解决这个问题,我们需要继续如下的步骤。
步骤三:重构HibernateUtil,使用ThreadLocal管理Session
重构HibernateUtil,引入ThreadLocal来管理Session。ThreadLocal对象是与线程有关的工具类,它的目的是将管理的对象按照线程进行隔离,以保证一个线程只对应一个对象。
由于后面我们要在拦截器中关闭连接,因此需要准确的取出DAO中使用的连接对象,为了便于实现在一个线程(一次客户端请求,就是一个线程)中,不同的代码位置获取同一个连接,那么使用ThreadLocal来管理session就再合适不过了。
注意,引入ThreadLocal管理session,不仅仅是在创建session时将其加入到ThreadLocal中,在关闭session时也需要将其从ThreadLocal中移除,因此HibernateUtil中还需要提供一个关闭session的方法
重构以后,HibernateUtil代码如下:
package com.netctoss.util;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtil {
private static SessionFactory sessionFactory;
#cold_bold /**
#cold_bold * 使用ThreadLocal管理Session,可以保证一个线程中只有唯一的一个连接。
#cold_bold * 并且我们在获取连接时,它会自动的给我们返回当前线程对应的连接。
#cold_bold */
#cold_bold private static ThreadLocal<Session> tl =
#cold_bold new ThreadLocal<Session>();
static {
// 加载Hibernate主配置文件
Configuration conf = new Configuration();
conf.configure("/hibernate.cfg.xml");
sessionFactory = conf.buildSessionFactory();
}
/**
* 创建session
*/
public static Session getSession() {
#cold_bold // ThreadLocal会以当前线程名为key获取连接
#cold_bold Session session = tl.get();
#cold_bold // 如果取到的当前线程的连接为空
#cold_bold if(session == null) {
#cold_bold // 使用工厂创建连接
#cold_bold session = sessionFactory.openSession();
#cold_bold // ThreadLocal会以当前线程名为key保存session
#cold_bold tl.set(session);
#cold_bold }
#cold_bold return session;
}
#cold_bold /**
#cold_bold * 关闭session
#cold_bold */
#cold_bold public static void close() {
#cold_bold // ThreadLocal会以当前线程名为key获取连接
#cold_bold Session session = tl.get();
#cold_bold // 如果取到的当前线程的连接不为空
#cold_bold if(session != null) {
#cold_bold // 关闭session
#cold_bold session.close();
#cold_bold // 将当前线程对应的连接从ThreadLocal中移除
#cold_bold tl.remove();
#cold_bold }
#cold_bold }
public static void main(String[] args) {
System.out.println(getSession());
close();
}
}
步骤四:创建保持session在视图层开启的拦截器
在com.netctoss.interceptor包下,创建一个拦截器OpenSessionInViewInterceptor,在拦截方法中,先调用action和result,之后再关闭本次访问线程对应的session,代码如下:
package com.netctoss.interceptor;
import com.netctoss.util.HibernateUtil;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.Interceptor;
/**
* 保持Session在视图层开启的拦截器,主要作用
* 是在执行完JSP之后再统一关闭session。
*/
public class OpenSessionInViewInterceptor
implements Interceptor {
@Override
public void destroy() {
}
@Override
public void init() {
}
@Override
public String intercept(ActionInvocation ai)
throws Exception {
// 调用action和result
ai.invoke();
/*
* result会把请求转发到页面,因此调用result,
* 就相当于调用JSP,因此此处的代码是在JSP之后执行。
* */
HibernateUtil.close();
return null;
}
}
步骤五:注册并引用拦截器
在struts.xml中,注册并引用这个拦截器,代码如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.1.7//EN"
"http://struts.apache.org/dtds/struts-2.1.7.dtd">
<struts>
<!-- 公共的包,封装了通用的拦截器、通用的result -->
<package name="netctoss" extends="json-default">
<interceptors>
<!-- 登录检查拦截器 -->
<interceptor name="loginInterceptor"
class="com.netctoss.interceptor.LoginInterceptor"/>
#cold_bold <!-- 保持session开启拦截器 -->
#cold_bold <interceptor name="openSessionInterceptor"
#cold_bold class="com.netctoss.interceptor.OpenSessionInViewInterceptor"/>
<!-- 登录检查拦截器栈 -->
<interceptor-stack name="loginStack">
<interceptor-ref name="loginInterceptor"/>
#cold_bold <interceptor-ref name="openSessionInterceptor"/>
<!-- 不要丢掉默认的拦截器栈,里面有很多Struts2依赖的拦截器 -->
<interceptor-ref name="defaultStack"/>
</interceptor-stack>
</interceptors>
<!-- 设置action默认引用的拦截器 -->
<default-interceptor-ref name="loginStack"/>
<!-- 全局的result,包下所有的action都可以共用 -->
<global-results>
<!-- 跳转到登录页面的result -->
<result name="login" type="redirectAction">
<param name="namespace">/login</param>
<param name="actionName">toLogin</param>
</result>
</global-results>
</package>
<!--
资费模块配置信息:
一般情况下,一个模块的配置单独封装在一个package下,
并且以模块名来命名package的name和namespace。
-->
<package name="cost" namespace="/cost" extends="netctoss">
<!-- 查询资费数据 -->
<action name="findCost" class="com.netctoss.action.FindCostAction">
<!--
正常情况下跳转到资费列表页面。
一般一个模块的页面要打包在一个文件夹下,并且文件夹以模块名命名。
-->
<result name="success">
/WEB-INF/cost/find_cost.jsp
</result>
<!--
错误情况下,跳转到错误页面。
错误页面可以被所有模块复用,因此放在main下,
该文件夹用于存放公用的页面。
-->
<result name="error">
/WEB-INF/main/error.jsp
</result>
</action>
<!-- 删除资费 -->
<action name="deleteCost"
class="com.netctoss.action.DeleteCostAction">
<!-- 删除完之后,重定向到查询action -->
<result name="success" type="redirectAction">
findCost
</result>
<result name="error">
/WEB-INF/main/error.jsp
</result>
</action>
<!-- 打开资费新增页 -->
<action name="toAddCost">
<result name="success">
/WEB-INF/cost/add_cost.jsp
</result>
</action>
<!-- 资费名唯一性校验 -->
<action name="checkCostName"
class="com.netctoss.action.CheckCostNameAction">
<!-- 使用json类型的result把结果输出给回调函数 -->
<result name="success" type="json">
<param name="root">info</param>
</result>
</action>
<!-- 打开修改页面 -->
<action name="toUpdateCost"
class="com.netctoss.action.ToUpdateCostAction">
<result name="success">
/WEB-INF/cost/update_cost.jsp
</result>
<result name="error">
/WEB-INF/main/error.jsp
</result>
</action>
</package>
<!-- 登录模块 -->
<package name="login" namespace="/login" extends="struts-default">
<!--
打开登录页面:
1、action的class属性可以省略,省略时Struts2
会自动实例化默认的Action类ActionSupport,
该类中有默认业务方法execute,返回success。
2、action的method属性可以省略,省略时Struts2
会自动调用execute方法。
-->
<action name="toLogin">
<result name="success">
/WEB-INF/main/login.jsp
</result>
</action>
<!-- 登录校验 -->
<action name="login" class="com.netctoss.action.LoginAction">
<!-- 校验成功,跳转到系统首页 -->
<result name="success">
/WEB-INF/main/index.jsp
</result>
<!-- 登录失败,跳转回登录页面 -->
<result name="fail">
/WEB-INF/main/login.jsp
</result>
<!-- 报错,跳转到错误页面 -->
<result name="error">
/WEB-INF/main/error.jsp
</result>
</action>
<!-- 生成验证码 -->
<action name="createImage" class="com.netctoss.action.CreateImageAction">
<!-- 使用stream类型的result -->
<result name="success" type="stream">
<!-- 指定输出的内容 -->
<param name="inputName">imageStream</param>
</result>
</action>
</package>
</struts>
步骤六:重构资费DAO实现类,去掉关闭session
重构CostDaoImpl,将session关闭的代码注释掉,统一由拦截器关闭。重构后代码如下:
package com.netctoss.dao;
import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Transaction;
import com.netctoss.entity.Cost;
import com.netctoss.util.HibernateUtil;
/**
* 当前阶段学习重点是Struts2,对于DAO的实现就模拟实现了。
* 同学们可以使用JDBC/MyBatis自行实现该DAO。
*/
public class CostDaoImpl implements ICostDao {
@Override
public List<Cost> findAll() {
String hql = "from Cost";
Session session = HibernateUtil.getSession();
Query query = session.createQuery(hql);
List<Cost> list = query.list();
#cold_bold// session.close();
return list;
}
@Override
public void delete(int id) {
Cost cost = new Cost();
cost.setId(id);
Session session = HibernateUtil.getSession();
Transaction ts = session.beginTransaction();
try {
session.delete(cost);
ts.commit();
} catch (HibernateException e) {
e.printStackTrace();
ts.rollback();
} finally {
#cold_bold// session.close();
}
}
@Override
public Cost findByName(String name) {
// 模拟根据名称查询资费数据,假设资费表中只有一条名为tarena的数据
if("tarena".equals(name)) {
Cost c = new Cost();
c.setId(97);
c.setName("tarena");
c.setBaseDuration(99);
c.setBaseCost(9.9);
c.setUnitCost(0.9);
c.setDescr("tarena套餐");
c.setStatus("0");
c.setCostType("2");
return c;
}
return null;
}
@Override
public Cost findById(int id) {
Session session = HibernateUtil.getSession();
Cost cost = (Cost) session.load(Cost.class, id);
#cold_bold// session.close();
return cost;
}
}