阅读 278

拉勾教育学习-笔记分享の"斩杀"框架篇(持久层)

【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---

〇、 脑图总览

图片可能存在更新延迟,最新脑图会在“传送门”中实时更新。

模块一 脑图传送门

一、 从持久层开始

:) Hello Mybatis
复制代码

1.1 持久层定义

From360百科: 系统逻辑层面上,专注于实现数据持久化的一个相对独立的领域(Domain),把数据保存到可掉电式存储设备中。持久层是负责向(或者从)一个或者多个数据存储器中存储(或者获取)数据的一组类和组件

1.2 早期JDBC

JDBC虽然是最接近数据库的访问方式,但也是最“僵硬”的,随着项目庞大起来,响应的维护量巨大。
它的问题非常明显:

  • 数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能
  • Sql语句在代码中硬编码,造成代码不易维护
  • 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护
  • 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据 库记录封装成pojo对象解析比较方便
public static void main() {
        Connection connection = null; // 准备连接实体
        PreparedStatement preparedStatement = null; // 准备statement
        ResultSet resultSet = null; // 准备结果集
        try {
            /* 1. 加载数据库驱动 (适用Mysql5+,8+请使用新驱动com.mysql.cj.jdbc.Driver)*/
            Class.forName("com.mysql.jdbc.Driver");
            /* 2. 通过驱动管理类获取数据库链接 */
            connection = DriverManager
                    .getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root");
            /* 3. 定义sql语句 ? 占位符 */
            String sql = "SELECT * FROM user WHERE user_name = ?";
            /* 4. 获取预处理statement */
            preparedStatement = connection.prepareStatement(sql);
            /* 5. 设置参数,第一个参数为sql语句中参数的序号(从1开始),第二个参数为设置的参数值 */
            preparedStatement.setString(1, "Archie");
            /* 6. 向数据库发出sql查询,并接收结果集 */
            resultSet = preparedStatement.executeQuery();
            /* 7. 遍历查询结果集 */
            while (resultSet.next()) {
                // Do something ...
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            /* 8. 释放资源 */
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
复制代码

于是一个个持久层框架——HibernateMyBatisSpring Data ……横空出世

1.3 自定义持久层框架(借鉴Mybatis)

1.3.1 我们对于JDBC的问题,可以有以下解决思路:

  1. 使用数据库连接池 初始化 资源;
  2. 将“僵硬的”sql语句抽取到xml配置文件中;
  3. 使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射

1.3.2 框架设计

传送门


具体实现代码

有心者可以使用这份代码认真研读,这对后期“手撕”Mybatis源码很有帮助!

二、 Mybatis由浅入深 (代码附小节末)

2.1 Mybatis基本概念

2.1.1 ORM(Object/Relation Mapping)--对象/关系数据库映射:

编程人员只需利用面向对象的编程语言,把对持久化对象的CRUD操作,转化为对数据库底层的操作。

2.1.2 Mybatis简介与历史:

  • 是基于ORM的半自动/轻量级/持久层框架;
  • 支持定制化SQL、高级映射;
  • 可以使用简单的 XML/注解 来 配置/映射 接口、POJO.

原是apache的一个开源项目iBatis, 2010年6月这个项目由apache software foundation 迁移到了google code,随着开发团队转投Google Code旗下,ibatis3.x正式更名为Mybatis ,代码于2013年11月迁移到Github。iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。 iBATIS提供的持久层框架包括SQL Maps和Data Access Objects(DAO)

2.1.3 Mybatis优势:

核心SQL可以由程序员自己定制以及优化,且SQL和java编码分离,使得功能边界清晰———前者专注数据、后者专注业务逻辑

2.2 踏出第一步

首先要明确,无论是何种技术,我们最好的学习文档就是 官方网址 --> Mybatis官网

-----鉴于大部分人在工作中经常使用Mybatis,基本的使用在此简要带过,详细步骤可以参考各大教程博客------

2.2.1 开发顺序

  1. 确认核心文件SqlMapConfig.xml
  2. pom.xml中添加mybatis依赖
  3. 确认数据库的目标table
  4. 确认与之对应的POJO类(属性和表对应)
  5. 编写“桥梁”——XXMapper.xml
  6. 自测

Mybatis的代理开发方式:程序员只编写Mapper接口(相当于Dao接口),由Mybatis识别这些接口并创建动态代理对象。

2.2.2 编写Mapper接口的基本规范:

  1. Mapper.xml文件中的namespacemapper接口的全限定名相同
  2. Mapper接口方法名和Mapper.xml中定义的每个statement的id相同
  3. Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同
  4. Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同

2.3 核心配置文件sqlMapConfig.xml

官方说明

2.3.1 基本配置

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url"
                          value="jdbc:mysql://localhost:3306/springdb?useSSL=false&amp;serverTimezone=UTC&amp;allowPublicKeyRetrieval=true"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="UserMapper.xml"/>
    </mappers>

</configuration>
复制代码

2.3.2 mybatis支持的三种数据源(DataSource)

  1. UNPOOLED:每次被请求时打开和关闭连接
  2. POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来
  3. JDNI:为了能在如 EJB 或应用服务器这类容器中使用而实现的数据源

2.3.3 mybatis自生产常用类型别名(TypeAlias)

别名数据类型
stringString
longLong
intInteger
_intint
doubleDouble
booleanBoolean

2.3.4 动态SQL

标签元素作用备注
if对输入参数进行判定,实现按条件拼接语句单条件分支
choose / when / otherwise相当于JAVA中的 if...else...多条件分支
trim辅助(前置/后置)拼接处理 SQL 拼接问题
where内嵌sql职能处理至'where'后拼接1.若输出是以and开头,会把首个'and'忽略掉
2.内部条件可以全空
set和where类似内部条件不可全空
foreach在sql中迭代一个集合collection情况:
1. List -> "list"
2. Array -> "array"
3. Map -> 对应key
script注解中使用动态sql
bind在OGNL表达式外创建一个变量并绑定到当前上下文

2.4 复杂映射开发(基于配置)

2.4.1 一对一查询

业务描述:每一个订单包含唯一一个用户信息
分析语句:SELECT * FROM orders o, user u WHERE o.uid = u.id;

2.4.1.1 orders表 + user表

2.4.1.2 Order实体 + User实体
public class User {

    private Integer id;
    
    private String userName;
    
    private Integer age;
    
    // ...省略 get/set/toString
}
复制代码
public class Order {
    
    private Integer id;
    private Date orderTime;
    private Double total;
    
    // 表明该订单属于哪个用户
    private User user;
    
   	// ......省略 get/set/toString
}
复制代码
2.4.1.3 OrderMapper.xml + OrderMapper.java
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="orderMapper">
    <resultMap id="orderMapper" type="com.archie.pojo.Order">
        <result column="id" property="id"/>
        <result column="ordertime" property="orderTime"/>
        <result column="total" property="total"/>
        <association property="user" javaType="com.archie.pojo.User">
            <result column="uid" property="id"/>
            <result column="username" property="userName"/>
            <result column="age" property="age"/>
        </association>
    </resultMap>
    
    <select id="listAll" resultMap="orderMapper">
        SELECT * FROM orders o, user u WHERE o.uid = u.id;
    </select>
</mapper>
复制代码
public interface OrderMapper {
    
    List<Order> listAll();
    
}
复制代码
2.4.1.4 自测 & 结果
    @Test
    public void testDemo() {
        SqlSession sqlSession = getFactory().openSession();
        List<Order> orderList = sqlSession.selectList("orderMapper.listAll");
        Assert.assertNotNull(orderList);
        for (Order order : orderList) {
            System.out.println(order);
        }
        sqlSession.close();
    }
复制代码

2.4.2 一对多查询

业务描述:一个用户有多个订单,一个订单只从属于一个用户
分析语句:SELECT *,o.id oid FROM user u LEFT JOIN orders o ON u.id=o.uid;

2.4.2.1 修改User实体
public class User {

    private Integer id;
    
    private String userName;
    
    private Integer age;
    
    //【新增】表示用户关联的订单
    private List<Order> orderList;

	// ......省略 get/set/toString

}
复制代码
2.4.2.2 UserMapper.xml + UserMapper.java
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="userMapper">
    <resultMap id="userMapper" type="com.archie.pojo.User">
        <result property="id" column="id"/>
        <result property="userName" column="username"/>
        <result property="age" column="age"/>
        <!--    使用collection接收List<Order>    -->
        <collection property="orderList" ofType="com.archie.pojo.Order">
            <result column="oid" property="id"/>
            <result column="ordertime" property="orderTime"/>
            <result column="total" property="total"/>
        </collection>
    </resultMap>

    <select id="listAll" resultMap="userMapper">
        SELECT *,o.id oid FROM user u LEFT JOIN orders o ON u.id=o.uid;
    </select>

</mapper>
复制代码
public interface UserMapper {
    
    List<User> listAll();
    
}
复制代码
2.4.2.3 自测 & 结果
    @Test
    public void test003() {
        SqlSession sqlSession = getFactory().openSession();
        List<User> userList = sqlSession.selectList("userMapper.listAllWithOrders");
        Assert.assertNotNull(userList);
        for (User user : userList) {
            System.out.println(user);
        }
        sqlSession.close();
    }
复制代码

2.4.3 多对多查询

业务描述:一个用户有多个角色,一个角色被多个用户使用
语句分析:SELECT * FROM user u LEFT JOIN sys_user_role ur ON u.id = ur.userid LEFT JOIN sys_role r ON r.id = ur.roleid

2.4.3.2 sys_role表 + sys_user_role表

2.4.3.2 创建Role实体 + 修改User实体
public class Role {
    
    private Integer id;
    private String roleName;
    private String roleDesc;

	// ...省略set/get/toString
}
复制代码
public class User {

    private Integer id;
    
    private String userName;
    
    private Integer age;
    
    //表示用户关联的订单
    private List<Order> orderList;

    //【新增】表示用户关联的角色
    private List<Role> roleList = new ArrayList<>();

    // ...省略set/get/toString
}
复制代码
2.4.3.3 在UserMapper接口中添加方法
public interface UserMapper {
    
    List<User> listAll(); // 获取所有用户(自身)
    
    List<User> listAllWithOrders(); // 获取所有用户和对应的订单信息(一对多)
    
    List<User> listAllWithRole(); // 获取所有用户和他们的角色(多对多)
    
}
复制代码
2.4.3.4 修改UserMapper.xml中的配置(多加一个resultMap)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="userMapper">
    <resultMap id="userMapper01" type="com.archie.pojo.User">
        <result property="id" column="id"/>
        <result property="userName" column="username"/>
        <result property="age" column="age"/>
        <!--    使用collection接收List<Order>    -->
        <collection property="orderList" ofType="com.archie.pojo.Order">
            <result column="oid" property="id"/>
            <result column="ordertime" property="orderTime"/>
            <result column="total" property="total"/>
        </collection>
    </resultMap>

    <resultMap id="userMapper02" type="com.archie.pojo.User">
        <result property="id" column="id"/>
        <result property="userName" column="username"/>
        <result property="age" column="age"/>
        <collection property="roleList" ofType="com.archie.pojo.Role">
            <result property="id" column="roleid"/>
            <result property="roleName" column="rolename"/>
            <result property="roleDesc" column="roleDesc"/>
        </collection>
    </resultMap>

	<!-- 自身查询 -->
    <select id="listAll" resultType="com.archie.pojo.User">
        SELECT * FROM user;
    </select>

	<!-- 获取所有用户和对应的订单信息(一对多) -->
    <select id="listAllWithOrders" resultMap="userMapper01">
        SELECT *,o.id oid FROM user u LEFT JOIN orders o ON u.id=o.uid;
    </select>

	<!-- 获取所有用户和他们的角色(多对多) -->
    <select id="listAllWithRole" resultMap="userMapper02">
        SELECT * FROM user u LEFT JOIN sys_user_role ur ON u.id = ur.userid LEFT JOIN sys_role r ON r.id = ur.roleid
    </select>

</mapper>
复制代码
2.4.3.5 自测 & 结果
    @Test
    public void test004() {
        SqlSession sqlSession = getFactory().openSession();
        List<User> userList = sqlSession.selectList("userMapper.listAllWithRole");
        Assert.assertNotNull(userList);
        for (User user : userList) {
            System.out.println(user);
        }
        sqlSession.close();
    }
复制代码

2.5 复杂映射开发(基于注解)

暂略......

2.6 Mybatis一级缓存

Mybatis的一级缓存默认打开,底层的实现是HashMap,生命周期在每一条SqlSession

2.6.1 测试 & 结果

    @Test
    public void test005() {
        SqlSession sqlSession = getFactory().openSession();
        List<Object> userList01 = sqlSession.selectList("userMapper.listAll");
        System.out.println(userList01.get(0));
        System.out.println("---------------------------------------------------");
        List<Object> userList02 = sqlSession.selectList("userMapper.listAll");
        System.out.println(userList02.get(0));
        Assert.assertEquals(userList01, userList02); // 断言:两个对象地址是否一样
    }
复制代码

再试试如果在两次相同操作过程中,执行了其他RUD操作,是否会导致缓存被刷新

    @Test
    public void test005() {
        SqlSession sqlSession = getFactory().openSession();
        List<Object> userList01 = sqlSession.selectList("userMapper.listAll");
        System.out.println(userList01.get(0));
        System.out.println("---------------------------------------------------");
        User user = new User();
        user.setId(1);
        user.setUserName("Archie(2.0)");
        sqlSession.update("userMapper.updateById", user);
        sqlSession.commit();
        System.out.println("---------------------------------------------------");
        List<Object> userList02 = sqlSession.selectList("userMapper.listAll");
        System.out.println(userList02.get(0));
        Assert.assertEquals(userList01, userList02); // 断言:两个对象地址是否一样
        sqlSession.close();
    }
复制代码

2.7 Mybatis二级缓存

Mybatis的二级缓存是跨SqlSession的,它基于mapper文件中的namespace——
所以:若两个mapper的namespace相同,就算mapper不同,也共享同一个二级缓存。

2.7.1 打开二级缓存

和一级缓存默认开启不一样,二级缓存需要我们手动开启

    <settings>
        <setting name="cacheEnable" value="true"/>
    </settings>
复制代码

还得再UserMapper.xml中开启缓存

<mapper namespace="userMapper">
    <cache/>
    ...
<mapper/>
复制代码

最后,因为二级缓存的存储介质多种多样(内存/硬盘...),所以我们要将对应POJO类实现Serializable接口,从而实现序列/反序列化。

public class User implements Serializable
复制代码

2.7.2 测试 & 结果

A. 二级缓存和sqlSession无关
    @Test
    public void test006() {
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
    
        List<User> userList1 = mapper1.listAll();
        System.out.println(userList1.get(0));
        sqlSession1.close(); // 关闭 1号SqlSession ,清空一级缓存
        System.out.println("---------------------------------------------------");
        List<User> userList2 = mapper2.listAll();
        System.out.println(userList2.get(0));
        sqlSession2.close();
        Assert.assertNotEquals(userList1, userList2); // 断言:两个对象地址是否不一样
    }
复制代码

B. 执行commit()操作,二级缓存数据清空
    @Test
    public void test007() {
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();
    
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
    
        List<User> userLis1 = mapper1.listAll();
        sqlSession1.close(); // 清空一级缓存
        System.out.println("-----------------执行commit操作-------------------");
        User user = new User();
        user.setId(1);
        user.setUserName("Archie(3.0)");
        mapper2.updateById(user);
        sqlSession2.commit();
        System.out.println("------------------------------------");
        List<User> userList2 = mapper3.listAll();
        Assert.assertNotEquals(userLis1, userList2); // 断言:两个对象地址是否不一样
    }
复制代码

2.7.3 二级缓存整合Redis

由于mybatis自带的二级缓存是单服务器工作,无法在分布式系统上工作,因此需要借助分布式缓存:【Redis/memcached/ehcache】

2.7.3.1 准备
  1. 准备pom依赖
  <dependency>
      <groupId>org.mybatis.caches</groupId>
      <artifactId>mybatis-redis</artifactId>
      <version>1.0.0-beta2</version>
  </dependency>
复制代码
  1. sqlMapConfig.xml指定分布式缓存类型 + 对应接口是否启用
    <!-- 整合Redis加入分布式缓存 -->
    <cache type="org.mybatis.caches.redis.RedisCache"/>
复制代码
    <!-- useCache="true" -->
    <select id="listAll" resultType="com.archie.pojo.User" useCache="true">
        SELECT * FROM user;
    </select>
复制代码
  1. 加入redis.properties文件(若无指定修改,可不添加该文件,底层有默认值)
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
复制代码
  1. 测试前 开启Redis服务

Redis快速运行脚本可以自己写:redis-server redis.windows.conf

2.7.3.1 测试
    @Test
    public void test008() {
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        
        List<User> userList1 = mapper1.listAll();
        System.out.println(userList1.get(0));
        sqlSession1.close(); // 清空一级缓存
        System.out.println("---------------------------------------------------");
        List<User> userList2 = mapper2.listAll();
        System.out.println(userList2.get(0));
        sqlSession2.close();
        Assert.assertNotEquals(userList1, userList2); // 断言:两个对象地址是否不一样
    }
复制代码

2.8 MyBatis插件(plugin)

暂略...


代码传送门

三、Mybatis源码“斩杀”

攻坚战开始了!O.O
P.S. 之前的自定义持久层框架一定要熟烂于心!

3.1 流程总览

暂略...

3.2 传统方式源码剖析

      // 1.使用ClassLoader对文件进行 “流化” !(但还未具体解析)
      InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
      // 2. 使用XPathParse 解析出 XMLConfigBuilder 对象,调用parse()方法
      // -- 2.1. parse()方法内部  利用 XNode 解析出 每个标签并将它们封装在 [JavaBean]Configuration中
      // -- 2.2. 构建者模式创建 DefaultSqlSessionFactory 对象
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
      // 3. 生成DefaultSqlsession实例对象了,并将 Executor 对象封装在其中
      // -- 3.1. 其中默认设置了 事务提交方式为:[非自动提交]
      sqlSession = sqlSessionFactory.openSession();
      // 4. 根据statementid来从Configuration中map集合中获取到了指定的MappedStatement对象
      // -- 4.1. 执行 executor.query() 从而 将查询任务委派到了executor执行器
      // --- 4.1.2. 顶层query() -> 设置分页值、设置CacheKey
      // --- 4.1.2. 判断 执行器是否被关闭、是否需要刷新缓存, 若否 -> 调用queryFromDatabase()方法
      // --- 4.1.3. 核心调用SimpleExecutor.doQuery()
      // ---- 4.1.3.1. 创建StatementHandler对象
      // ---- 4.1.3.2. 构建者模式生成ParameterHandler
      // ---- 4.1.3.3. 获取TypeHandler
      // ---- 4.1.3.4. 调用ResultSetHandler.handleResultSets() 处理statement
      // ***** 底层JDBC操作 *****
      // 5. 返回List<E> 的结果
      List<User> userList = sqlSession.selectList("com.archie.dao.UserMapper.listAll");
      // 6. 释放资源
      sqlSession.close();
复制代码

3.3 Mapper代理方式源码剖析

获取sqlSession前的步骤和 传统方式一致;

      // ......
      SqlSession sqlSession = sqlSessionFactory.openSession();
      // 4. 通过mapperRegistry 调用 getMapper()方法
      // -- 4.1. 从mapperRegistry中的 HashMap 拿 MapperProxyFactory
      // -- 4.2. 通过动态代理工厂 生成实例
      // --- 4.2.1. 实例的生成过程中实现了对每一个SqlSession 的动态代理
      UserMapper mapper = sqlSession.getMapper(UserMapper.class);
      // 5. 获取到 代理对象后,就可以直接调用被代理接口的方法了
      // -- 5.1. 代理对象,调用方法时都是执行MapperPorxy中的invoke()方法
      // ---- 5.1.1. invoke()中,若非Object的方法,则获取MapperMethod对象,【重点】它来最终调用execute()方法
      // ---- 5.1.2. execute()中,判断方法类型-->CRUD, 最终交由ResultHandler处理,返回Object型的result值
      List<User> userList01 = mapper.listAll();
复制代码

3.4 二级缓存源码剖析

查询请求优先级:【 二级缓存 > 一级缓存 > 数据库 】

二级缓存和具体namespace绑定,一个Mapper中有一个Cache,相同Mapper的MapperStatement共用一个Cache

3.4.1 思考A - ‘为什么说一个Mapper中的Cache被共用?’

在3.2小节中,我们分析步骤2时,sqlMapConfig.xml 的具体解析是由XMLConfigBuilder调用parse()解析的,它中途需要解析到<cache/>标签,这个标签也是我们打开二级缓存的标志。所以我们得分析从这里开始。

高清传送门

3.4.2 思考B - ‘为什么优先级 :二级缓存 > 一级缓存?’

P.S. 二级缓存开启时,走的是CachingExecutor实现类

3.4.3 思考C - ‘为何只有SqlSession提交或者关闭后,二级缓存才会生效?’

暂略...

3.5 延迟加载源码剖析

暂略...

3.5.1 使用场景

当需要用到数据的时候才进行多表关联查询。

  • 优点:大大提高数据库性能;
  • 缺点:因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降

建议使用场景:

  • 一对一/多对一: 立即加载
  • 一对多/多对多: 延迟加载

懒加载是基于 嵌套查询的!

3.5.2 实现

3.5.2.1 UserMapper.xml + UserMapper.java
  1. 指明当前 collection标签 需要定位的内嵌sql select="com.archie.dao.OrderMapper.listByUid"

  2. 传给内嵌sql的参数 column="id"

  3. 实现局部懒加载 fetchType="lazy"

  <resultMap id="userMapper01" type="com.archie.pojo.User">
      <id property="id" column="id"/>
      <result property="userName" column="username"/>
      <result property="age" column="age"/>
      
      <collection property="orderList" ofType="com.archie.pojo.Order"
      select="com.archie.dao.OrderMapper.listByUid" column="id" 
      fetchType="lazy"> 
          <result column="uid" property="id"/>
          <result column="ordertime" property="orderTime"/>
          <result column="total" property="total"/>
      </collection>
  </resultMap>
  
  <select id="listById" resultMap="userMapper01" parameterType="com.archie.pojo.User">
      SELECT * FROM user WHERE id = #{id};
  </select>
复制代码
User listById(User user);
复制代码
3.5.2.2 OrderMapper.xml + OrderMapper.java
  <resultMap id="orderMapper" type="com.archie.pojo.Order">
      <id column="id" property="id"/>
      <result column="ordertime" property="orderTime"/>
      <result column="total" property="total"/>
      <association property="user" javaType="com.archie.pojo.User">
          <result column="uid" property="id"/>
          <result column="username" property="userName"/>
          <result column="age" property="age"/>
      </association>
  </resultMap>
  
  <select id="listByUid" resultType="com.archie.pojo.Order">
      SELECT * FROM orders WHERE uid = #{uid};
  </select>
复制代码
List<Order> listByUid(Integer uid);
复制代码
3.5.2.3 自测 & 结果
    @Test
    public void test009() {
        SqlSession sqlSession = sqlSessionFactory.openSession();
        User user = new User();
        user.setId(1);
        User resultUser = sqlSession.selectOne("com.archie.dao.UserMapper.listById", user);
        System.out.println(resultUser.getUserName());
        System.out.println("****** 还未使用到订单信息 *******");
        System.out.println("**********  现在想要获取对应用户的订单信息了  ************");
        System.out.println(resultUser.getOrderList());
    }
复制代码

3.5.2.4 全局配置懒加载

在sqlMapConfig.xml中加入<setting name="lazyLoadingEnabled" value="true">即可实现全局懒加载

懒加载的优先级: 局部 高于 全局

因此——当全局开启时,若有部分接口不想懒加载,可以直接 fetchType="eager"实现局部立即加载

3.5.3 源码剖析

暂略...

四、 设计模式

4.1 Builder构建者模式

4.1.1 通俗定义

使用多个简单对象 --构建--> 复杂对象 的过程。

4.1.2 简单实现

Demo【构建一个Computer复杂对象】

  • 简单对象:键盘、鼠标、显示器、主机……
  • 复杂对象:Computer
  • 顺序:创建构造类 --> 依次创建部件 --> 将部件组装成target对象

【step-A】 定义目标对象Computer

public class Computer {

    /* 显示器部件 */
    private String disPlayer;
    /* 主机部件 */
    private String mainEngine;
    /* 鼠标部件 */
    private String mouse;
    /* 键盘部件 */
    private String keyBoard;
    
    // ...省略 set、get、toString
}
复制代码

【step-B】 定义构建者ComputerBuilder

public class ComputerBuilder {
    
    /* 定义目标对象 */
    private Computer target = new Computer();
    
    /**
     * 安装显示器部件给目标对象
     * @param disPlayer
     */
    public void installDisPlayer(String disPlayer) {
        target.setDisPlayer(disPlayer);
    }
    
    /**
     * 安装主机部件给目标对象
     * @param mainEngine
     */
    public void installMainEngine(String mainEngine) {
        target.setMainEngine(mainEngine);
    }
    
    /**
     * 安装鼠标部件给目标对象
     * @param mouse
     */
    public void installMouse(String mouse) {
        target.setMouse(mouse);
    }
    
    /**
     * 安装键盘部件给目标对象
     * @param keyBoard
     */
    public void installKeyBoard(String keyBoard) {
        target.setKeyBoard(keyBoard);
    }
    
    /**
     * 实现复杂对象构建
     * @return
     */
    public Computer build() {
        return target;
    }
    
}
复制代码

测试

public class realizeBuildingComputer {
    
    public static void main(String[] args) {
        ComputerBuilder builder = new ComputerBuilder();
        builder.installDisPlayer("A-1001-显示器");
        builder.installMainEngine("B-2344-主机");
        builder.installMouse("C-6617-鼠标");
        builder.installKeyBoard("D-4990-键盘");
        /* 安装部件完毕,生产电脑 */
        Computer computer = builder.build();
        /* 测试打印 */
        System.out.println("computer = \n" + computer);
    }
    
}
复制代码

4.1.3 mybatis中的体现

  • Mybatis的初始化流程非常复杂,在创建SqlSessionFactory的过程中,利用了大量的Builder进行分层构建;
  • 核心javaBean-'Configuration' 也是使用了XmlConfigBuilder来构建的;

高清传送门

4.2 工厂模式

(目前只需掌握 /简单工厂模式/ 即可)

4.2.1 通俗定义

简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于创建型模式。
专门有一个类 ——> 根据参数的不同返回不同类的实例(这些实例往往是同一个父类)

4.2.2 简单实现

Demo 【一个厂商生产不同品牌的电脑】


【Step-A】创建产品抽象类

public abstract class Computer {
    
    /* 启动电脑方式 :由具体产品实现 */
    public abstract void start();
    
}
复制代码

【Step-B】创建具体产品

创建不同品牌的电脑,都继承抽象父类->Computer

public class HPComputer extends Computer{
    @Override
    public void start() {
        System.out.println("惠普电脑启动");
    }
}

public class LenovoComputer extends Computer {
    @Override
    public void start() {
        System.out.println("联想电脑开机");
    }
}
复制代码

【Step-C】创建工厂类

提供静态方法createComputer用来生产电脑(用户只需要传输想要的品牌,响应对象即刻实例化)

public class ComputerFactory {
    
    public static Computer createComputer(String brand) {
        Computer computer = null;
        switch (brand) {
            case "lenovo":
                computer = new LenovoComputer();
            case "hp":
                computer = new HPComputer();
        }
        return computer;
    }
    
}
复制代码

测试

  public static void main(String[] args) {
      ComputerFactory.createComputer("hp").start();
      System.out.println("-----------");
      ComputerFactory.createComputer("lenovo").start();
  }
复制代码

4.2.3 Mybatis中体现

SqlSessionFactory中的openSession有很多重载,支持各类参数类别的输入,从而构建核心SqlSeesion对象

这个工厂的核心底层方法是openSessionFromDataSource

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 根据参数创建制定类型的Executor
      final Executor executor = configuration.newExecutor(tx, execType);
      // 执行器注入构建方法,返回SqlSession对象
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
复制代码

4.3 代理模式

(目前掌握动态代理即可)

4.3.1 通俗定义

给某一个对象提供一个代理,并由代理对象控制对原对象的引用

4.3.2 简单实现

Demo【对Person实体进行代理】


【Step-A】‘人’接口和默认实现类

public interface Person {
    void doSomething();   
}

public class Justin implements Person{
    @Override
    public void doSomething() {
        System.out.println("Justin is doing something...");
    }
}
复制代码

【Step-B】具体的代理实现类

public class JdkDynamicProxyImpl implements InvocationHandler {
    
    // 声明被代理的对象
    private Person person;
    
    // 构造函数
    public JdkDynamicProxyImpl(Person person) {
        this.person = person;
    }
    
    /**
     * 获取代理对象
     * @return
     */
    public Object getTarget() {
        Object proxyInstance = Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), this);
        return proxyInstance;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("...对原方法进行前置增强");
        Object invoke = method.invoke(person, args); // 原方法的执行
        System.out.println("...对原方法进行后置增强");
        return invoke;
    }
}
复制代码

测试

public class ProxyTest {
    
    public static void main(String[] args) {
        System.out.println("【直接调用某人类的方法】");
        Justin justin = new Justin();
        justin.doSomething();
        System.out.println("-----------------");
        System.out.println("【使用代理类调用同一个人类的行为】");
        Person proxyJustn = (Person) new JdkDynamicProxyImpl(new Justin()).getTarget();
        proxyJustn.doSomething();
    }
    
}
复制代码

4.3.3 Mybatis中体现

代理模式是MyBatis的核心模式 由于这个模式,我们只需要编写XXXMapper.java接口,而不需要实现;由MyBatis完成具体SQL的执行。

高清传送门

文章分类
后端
文章标签