MyBatis 框架基本使用及深入理解

6,354 阅读9分钟

题记: 本文对 Mybatis 框架相关内容进行整理,从最开始使用JDBC 操作数据库,理解 DAO 层底层需要执行的步骤,到仿照 MyBatis 自定义框架,对 MyBatis 框架结构进行梳理。之后再介绍 MyBatis 框架的基本使用以及常用特性,了解 MyBatis 的日常应用,最后深入框架源码去感受 MyBatis 框架的精妙设计。

  • 注:文章内容输出来源:拉勾教育Java高薪训练营;

复习JDBC操作流程

  1. 加载数据库连接驱动
  2. 通过驱动管理类获取数据库连接
  3. 获取预编译语句
  4. 设置预编译语句参数
  5. 执行SQL, 处理结果集
  6. 释放资源
public static void main(String[] args) { 
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
	try { 
        // 1. 加载数据库驱动
        Class.forName("com.mysql.jdbc.Driver"); 
        // 2. 通过驱动管理类获取数据库连接
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root"); 
        
        String sql = "select * from user where username = ?";
        // 3. 获取预编译语句
        preparedStatement = connection.prepareStatement(sql); 
        // 4. 设置预编译语句参数 
        preparedStatement.setString(1, "tom");
        // 5. 执行SQL, 处理结果集
        resultSet = preparedStatement.executeQuery();
       
        while (resultSet.next()) {
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            
            user.setId(id);
            user.setUsername(username);
        }
        System.out.println(user); 
    } catch (Exception e) { 
        e.printStackTrace();
    } finally { 
        // 6. 释放资源
        if (resultSet != null) { 
            try {
                resultSet.close();
            } catch (SQLException e) { 
                e.printStackTrace(); 
            }
        }
        if (preparedStatement != null) { 
            try {
                preparedStatement.close();
            } catch (SQLException e) { 
                e.printStackTrace();
            } 
        }
        if (connection != null) { 
            try {
                connection.close();
            } catch (SQLException e) { 
                e.printStackTrace();
            }
        }
    }
}

直接使用JDBC存在的问题:

  1. 数据库连接的创建、销毁频繁造成系统资源浪费。
  2. SQL 语句在代码中硬编码,SQL 语句的变化需要改变Java代码
  3. 在向 preparedStatement 占位符传参存在硬编码
  4. 对结果集解析存在硬编码(查询列名),系统不易维护

问题解决思路

  1. 数据库频繁创建连接、释放资源 ⇒ 数据库连接池
  2. SQL语句及参数硬编码 ⇒ 配置文件
  3. 手动解析封装返回结果集 ⇒ 反射、内省

自定义框架设计

使用端:

提供核心配置文件

/**
*	sqlMapConfig.xml:存放数据源信息,引入mapper.xml
*	Mapper.xml:SQL语句的配置文件信息
*/

框架端:

  1. 读取配置文件

    /**
    *	读取完以后以流的形式存在,可创建JavaBean来存储
    */
    public class Configuration {
        // 数据源
        private DataSource dataSource;
        // map集合: key:statementId value:MappedStatement
        private Map<String,MappedStatement> mappedStatementMap = 
            new HashMap<String, MappedStatement>();
    }
    
    public class MappedStatement {
        //id
        private Integer id;
        //sql语句
        private String sql;
        //输入参数
        private Class<?> paramterType;
        //输出参数
        private Class<?> resultType;
    }
    
  2. 解析配置文件

    创建SqlSessionFactoryBuilder类

    使用 dom4j 解析配置文件,将解析出来的内容封装到 Configuration 和 MappedStatement 中

  3. 创建SqlSessionFactory

    创建SqlSessionFactory的实现类DefaultSqlSession,并实现openSession() 方法,获取sqlSession接口的实现类实例对象 ( 传递Configuration 对象)

  4. 创建SqlSession接口及实现类 主要封装CRUD方法

    方法:selectList(String StatementId,Object param)查询所有

    selectOne(String StatementId,Object param)查询单个

    close() 释放资源

    具体实现:封装JDBC完成对数据库表的查询操作

    Executor 类,从Configuration类中获取,DataSource、SQL、paramterType、resultType,通过反射设置预编译语句占位符的值,执行SQL,通过内省,通过列名与对象属性的对应关系解析结果为对象

自定义框架优化

上述自定义框架存在的问题

  1. dao 的实现类中存在重复的代码,整个操作的过程重复(创建SqlSession, 调用SqlSession方法、关闭SqlSession)
  2. dao 的实现类中存在硬编码,调用SqlSession 的方法时,参数 Statement 的 Id硬编码

解决方法

使用代理模式来创建接口的代理对象

Mybatis快速使用

1. 依赖

<!--mybatis-->
<dependency> 
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.5</version>
</dependency>
<!--mysql驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.6</version>
    <scope>runtime</scope>
</dependency>

2. 分别创建数据表及实体类

3. 编写 UserMapper 映射文件

<?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">
    <select id="findAll" resultType="com.lagou.domain.User">
        select * from User
    </select>
</mapper>

4. 编写 MyBatis 核心文件

<?xml version="1.0" encoding="UTF-8" ?>
<!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.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///test"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/lagou/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

5. CRUD

注:增删改需要提交事务或设置自动提交

示例:

InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// SqlSession sqlSession = sqlSessionFactory.openSession(true);
// 带参数true 为自动提交事务
SqlSession sqlSession = sqlSessionFactory.openSession();
// namespace.id
int insert = sqlSession.insert("userMapper.add", user);
System.out.println(insert);
//提交事务
sqlSession.commit();
sqlSession.close();

MyBatis 核心配置文件层级关系

  • configuration 配置
    • properties 属性
    • settings 设置
    • typeAliases 类型别名
    • typeHandlers 类型处理器
    • objectFactory 对象工厂
    • plugins 插件
    • environments 环境
      • environment 环境变量
        • transactionManager 事务管理器type: [JDBC, MANAGED]
        • dataSource 数据源type: [UNPOOLED, POOLED, JNDI]
    • databaseIdProvider 数据库厂商标识
    • mappers 映射器

mapper.xml

动态 SQL 语句

SQL语句的主体结构,在编译时尚无法确定,只有等到程序运行起来,在执行的过程中才能确定,这种SQL叫做动态SQL。

常用标签:

  • <where>

  • <if>

<select id="findByCondition" parameterType="user" resultType="user"> 
    select * from User
    <where>
        <if test="id!=0">
            and id=#{id}
        </if>
        <if test="username!=null">
            and username=#{username}
        </if>
    </where>
</select>
  • <foreach>

    属性:

    • collection: 代表要遍历的集合元素
      1. 集合 ⇒  list
      2. 数组 ⇒ array
      3. Map ⇒ 集合对应的key
    • open: 代表语句的开始部分
    • close: 语句结束部分
    • item: 代表遍历集合的每个元素,生成的变量名
    • separator: 分隔符
<select id="findByIds" parameterType="list" resultType="user">
    <include refid="selectUser"></include>
    <where>
        <foreach collection="array" open="id in(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>
<!--
	List ids = new ArrayList();
	ids.add(1);
	ids.add(2);
	Map params = new HashMap();
	params.put("ids", ids);
	params.put("title", "中国");
-->
<select id="dynamicForeach3Test" resultType="Blog">
    select * from t_blog where title like "%"#{title}"%" and id in
    <foreach collection="ids" index="index" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</select>
  • <sql>

  • <include>

Mybatis 注解开发

常用注解

  • @Insert:新增

  • @Update:更新

  • @Delete:删除

  • @Select:查询

  • @Result:实现结果集封装 [<id>, <result>, <association>, <collection>]

  • @Results:可以与@Result 一起使用,封装多个结果集<resultMap>

  • @One:实现一对一结果封装

  • @Many:实现一对多结果集封装

    public interface UserMapper { 
        @Select("select * from user") 
        @Results({ 
            @Result(id = true,property = "id",column = "id"),
            @Result(property = "username",column = "username"),
            @Result(property = "password",column = "password"),
            @Result(property = "birthday",column = "birthday"),
            @Result(property = "roleList",column = "id",javaType = List.class,
                    many = @Many(select ="com.lagou.mapper.RoleMapper.findByUid")) })
        List<User> findAllUserAndRole();
    }
    
    public interface RoleMapper {
        @Select("select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=#{uid}")
        List<Role> findByUid(int uid);
    }
    

Mybatis 缓存

  • 一级缓存是 SqlSession 级别的缓存,由sqlSession 对象中的 HashMap 数据结构来储存, 不同sqlSession 之间的缓存互不影响,一级缓存默认开启

  • 二级缓存是 mapper 级别的缓存,二级缓存需手动开启

    一级缓存

    • 执行commit() 操作, 会清空对应 sqlSession 下的一级缓存

Map 中 CacheKey

CacheKey cacheKey = new CacheKey(); 
// MappedStatement的id 
// id: namespace + SQLid
cacheKey.update(ms.getId()); 
// offset  0
cacheKey.update(rowBounds.getOffset()); 
// limit就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit()); 
// 具体的SQL语句
cacheKey.update(boundSql.getSql()); 
// SQL中带的参数
cacheKey.update(value); ...
if (configuration.getEnvironment() != null) {
    // environmentId
    cacheKey.update(configuration.getEnvironment().getId());
}

二级缓存

  • 二级缓存与一级缓存流程类似,但二级缓存基于 mapper 文件的 namespace

  • 二级缓存底层还是 HashMap 结构

  • 执行 commit() 操作会清空二级缓存数据

1. 开启二级缓存

  1. 在全局配置文件sqlMapConfig.xml 中加入

    <!-- 开启二级缓存 --> 
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    
  2. 在 Mapper.xml 中开启缓存

    <!-- 开启二级缓存 -->
    <!-- 
    	空标签,默认 type=PerpetualCache的相对路径
    	也可以通过实现 Cache 接口来自定义缓存
    -->
    <cache></cache>
    
  3. pojo类实现序列化接口

    因为二级缓存存储介质并非内存一种,可能会序列化到硬盘中

userCache 与 flushCache 配置项

  • userCache: 设置是否禁用二级缓存,控制粒度为 SQL,在statement中默认为true
  • flushCache: 刷新缓存,防止出现脏读,默认为true
  • 使用缓存是如果手动修改数据库表中的查询数据会出现脏读

二级缓存整合Redis

  • 目的:实现分布式缓存

1. 依赖

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>

2. 配置文件

Mapper.xml

<cache type="org.mybatis.caches.redis.RedisCache" />

3. Redis连接配置文件

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

注:

mybatis-redis 在存储数据的时候,使用的 hash 结构

key: namespace

field: Cachekey

value: result

因为需要序列化与反序列化,所以第二次从缓存中获取的对象和之前的对象并不是同一个

Mybatis 插件介绍

Mybatis作为一个应用广泛的优秀的ORM框架,这个框架具有强大的灵活性,在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象

MyBatis 所允许拦截的方法如下:

  • 执行器Executor [update、query、commit、rollback]
  • SQL语法构建器StatementHandler[prepare、parameterize、batch、update、query]
  • 参数处理器ParameterHandler[gerParameterObject、setParameters方法]
  • 结果集处理器ResultSetHandler[handleResultSets、handleOutputParameters等方法]

自定义插件

  1. 创建自定义插件类实现Interceptor接口
  2. 重写Intercept()方法
  3. 然后给插件编写注解,指定需要拦截 的接口和方法
  4. 在mybatis配置文件中配置所写的插件类

MyBatis 执行流程

设计模式

MyBatis 用到的设计模式

设计模式 MyBatis 体现
Builder模式 SqlSessionFactoryBuilder、Environment
工厂方法 SqlSessionFactory、TransactionFactory、LogFactory
单例模式 ErrorContext、LogFactory
代理模式 Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk动态代理,还有executor.loader包使用了cglib或者javassist达到延迟加载的效果
组合模式 SqlNode 和各个子类ChooseSqlNode等
模板方法模式 BaseExecutor和SimpleExecutor,还有BaseTypeHandler和他的子类例如IntegerTypeHandler;
适配器模式 例如Log的Mybatis接口和它对jdbc、log4j等日志框架的适配实现
装饰者模式 例如Cache包中的cache.decorators子包中等各个装饰者的实现
迭代器模式 例如迭代器模式PropertyTokenizer