1 验证一级缓存

1.2 方案

  1. 用同一个Session查询同一条数据2次,如果只查询一次数据库,则验证了一级缓存的存在。
  2. 用2个不同的Session,分别查询同一条数据,如果查询2次数据库,则验证了一级缓存是Session独享的。
  3. 在com.tarena.test包下创建测试类TestFirstCache,在这个测试类中分别写出方案中提到的2个测试方法,用以验证一级缓存的存在及特性,代码如下:
  4. package com.tarena.test;
  5.  
  6. import org.hibernate.Session;
  7. import org.junit.Test;
  8. import com.tarena.entity.Emp;
  9. import com.tarena.util.HibernateUtil;

11.public class TestFirstCache {

  1. /**
  2. * 用同一个Session查询同一条数据2次,
  3. * 如果只查询一次数据库,则验证了一级缓存的存在。
  4. */
  5. @Test
  6. public void test1() {
  7. Session session = HibernateUtil.getSession();
  8. Emp e1 = (Emp) session.get(Emp.class, 321);
  9. System.out.println(e1.getName());
  10. System.out.println("----------------");
  11. Emp e2 = (Emp) session.get(Emp.class, 321);
  12. System.out.println(e2.getName());
  13. session.close();
  14. }
  15. /**
  16. * 用2个不同的Session,分别查询同一条数据,
  17. * 如果查询2次数据库,则验证了一级缓存是Session独享的。
  18. */
  19. @Test
  20. public void test2() {
  21. Session session1 = HibernateUtil.getSession();
  22. Emp e1 = (Emp) session1.get(Emp.class, 321);
  23. System.out.println(e1.getName());
  24. Session session2 = HibernateUtil.getSession();
  25. Emp e2 = (Emp) session2.get(Emp.class, 321);
  26. System.out.println(e2.getName());
  27. session1.close();
  28. session2.close();
  29. }

45.}

 

2 管理一级缓存

2.1 问题

掌握一级缓存管理的2种方式:

  1. 使用evict方法,从一级缓存中移除一个对象。
  2. 使用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 问题

设计出案例,验证持久态对象的特性:

  1. 持久态对象存在于一级缓存中。
  2. 持久态对象可以自动更新至数据库。
  3. 持久态对象自动更新数据库的时机是session.flush()。

 

 

3.2 方案

设计3个案例,分别验证持久态对象的3个特性:

  1. 新增一条数据,在新增后该数据对象为持久态的,然后根据对象的ID再次查询数据。执行时如果控制台不重新输出SQL则验证了持久态对象存在于一级缓存。
  2. 新增一条数据,在新增后该对象为持久态的,然后修改这个对象的任意属性值,并提交事务。在执行时,如果发现数据库中的数据是修改后的内容,则验证了持久态对象可以自动更新至数据库。
  3. 查询一条数据,该数据对象为持久态的,然后修改对象的任意属性值,再调用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 方案

  1. 验证session.load()的延迟加载,可以先查询某条EMP数据,然后输出一个分割线,在分割线的后面再使用这个对象,输出它的一些属性值。在运行时,如果查询EMP的SQL输出在分割线的后面,则验证了该方法是采用延迟加载机制的。
  2. 验证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;

        }

}