MyBatis 原理探析

本文主要简单盘点 MyBatis 用法, MyBatis 源码解读, 以及 Mybatis / Hibernate 比较

基本概念

MyBatis 是一种 Java ORM (Object Related Map) 框架, 所谓 ORM 是指对象关系映射, 即将数据库的一张表和 POJO 对应起来, ORM 模型即描述了二者的映射关系. 传统操作数据库的方法是通过 JDBC 来实现的, 但是 JDBC 只是定义了接口规范, 具体实现是数据库厂商实现的, 例如 MySQL 的 JDBC 连接器可能是 Oracle 自己实现, SQLServer 的 JDBC 连接器可能就是靠微软来实现. 传统的 JDBC 存在一个问题, 即重复代码量大, 严格按照一定的顺序连接->打开->执行->读取->转换->关闭. MyBatis 这种 ORM 框架就出现了.

MyBatis 前身是开源项目 iBatis, MyBatis 的工作需要提供

  • SQL 语句
  • Mapping 规则
  • POJO
MyBatis 工作流程

MyBatis 的映射模型如下:

以 *.XML 的映射文件为例, 我们可以按照如下方式构建数据库

<?xml version="1.0" encoding="utf-8" ?>
<!--- file name: mybatis-config.xml -->
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="mybatis.properties"/>
    <typeAliases>
        <typeAlias alias="role" type="wang.rancho.mybatis.RoleDemo"/>
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC">
                <property name="autoCommit" value="false"/>
            </transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="RoleMapper.xml"/>
    </mappers>
</configuration>

在这里我们可以看到对许多属性进行了一下设置, 例如

  • 数据库属性的具体配置在 myBatis.properties 文件中
  • 给定 POJO 的别名
  • 数据库的配置环境, 数据库连接种类是 JDBC, 通过连接池连接, 驱动器, 数据库 url , 用户名, 密码(这些都在 myBatis.properties 中)
  • Mapper 映射器定义的位置.

注意这里遵循的 dtd 标签标准是 “http://mybatis.org/dtd/mybatis-3-config.dtd” 所定义的.

在 Mapper 设置中, 我们可以这样设置:

<?xml version="1.0" encoding="utf-8" ?>
<!--- file name RoleMapper.xml.xml -->
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="wang.rancho.mapper.RoleMapper">
    <select id="getRole" parameterType="long" resultType="role">
        select id, role_name as roleName, note from t_role where id = #{id}
    </select>
    <insert id="insertRole" parameterType="role">
        insert into t_role(role_name, note) values(#{roleName}, #{note})
    </insert>
    <delete id="deleteRole" parameterType="Long">
        delete from t_role where id = #{id}
    </delete>
</mapper>

这里我们定义了几个 SQL 语句, select, insert, delete (都是mybatis-3-mapper.dtd 定义的标签), 这些都是在接口中定义的函数, 通过 id 匹配函数名, 通过 parameterType 匹配参数. 例如在接口函数中, 我们定义:

package wang.rancho.mapper;

import wang.rancho.mybatis.Role;

/**
 * RoleMapper.java
 * Created by Rancho on 8/29/2018.
 */
public interface RoleMapper {
    public Role getRole(Long id);
    public int deleteRole(Long id);
    public int insertRole(Role role);
}

以及

package wang.rancho.mybatis;

/**
 * Created by Rancho on 8/29/2018.
 */
public class Role {
    private Long id;
    private String roleName;
    private String note;
    //...Getter and Setter
}

那么在 RoleMapper 实例中调用这些函数, 实际上就是调用这些 SQL 操作了. 注意这里类似于 Spring 的 Autowired 自动将成员字段与其 Getter 相互匹配了.

MyBatis 实现原理

如何用 Java 语言去执行 XML 中定义的 SQL 语句? 这涉及到 Java 最普遍广泛应用的代理的概念. 有关代理的知识不必多讲, 例如 反射/动态代理/CGLIB 的原理不必本文多赘述. 总之利用编译后的 Java 代码元数据本身丰富的信息, 可以实现很方便的从其他加载代码. 在这里只介绍如何实现的. 具体示例代码如下:

RoleDemo.java
package wang.rancho.mybatis;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import wang.rancho.mapper.RoleMapper;
import wang.rancho.util.SqlSessionFactoryUtil;

/**
 * Created by Rancho on 8/29/2018.
 */
public class RoleDemo {
    public static void main(String[] args){
        SqlSession sqlSession = null;
        SqlSessionFactory sqlSessionFactory = null;
        try {
            sqlSession = SqlSessionFactoryUtil.openSqlSession();
            RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
            Role role = new Role();
            role.setId(1L);
            role.setRoleName("Tracer");
            role.setNote("Lena Oxton");
            roleMapper.insertRole(role);
            sqlSession.commit();
        }catch (Exception ex){
            System.err.println(ex.getMessage());
            sqlSession.rollback();
        }finally {
            if(sqlSession != null){
                sqlSession.close();
            }
        }
    }
}
SqlSessionFactoryUtil .java
package wang.rancho.util;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.*;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;

/**
 * Created by Rancho on 8/30/2018.
 */
public class SqlSessionFactoryUtil {
    private static SqlSessionFactory sqlSessionFactory = null;
    private static final Class CLASS_LOCK = SqlSessionFactoryUtil.class;
    private static final Logger logger = Logger.getLogger(SqlSessionFactoryUtil.class);
    private SqlSessionFactoryUtil(){}
    public static SqlSessionFactory initSqlSessionFactory(){
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        }catch (IOException e){
            logger.log(Level.FATAL, null, e);
        }
        synchronized (CLASS_LOCK){
            if(sqlSessionFactory == null){
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            }
        }
        return sqlSessionFactory;
    }
    public static SqlSession openSqlSession(){
        if(sqlSessionFactory == null){
            initSqlSessionFactory();
        }
        return sqlSessionFactory.openSession();
    }
}

下图展示了使用的过程:

i. SqlSessionFactory 是一个工厂类, 在上面的代码中, 我们通过单例模式创造工厂类, 这是为了构造器避免在多线程环境下的不唯一, 并且减少对象建立的开销. 具体做法是读 “mybatis-config.xml” 以文件流的形式读入配置, 然后通过 new SqlSessionFactoryBuilder().build(inputStream) 建立工厂

在 build 方法中, inputStream 被转化成了一个 Configuration 对象, Configuration 如同名字一样, 包含了很多配置项的字段, 包括了所有在配置文件 mybatis-config.xml 的所有项目. 包括以下的初始化:

  • properties
  • settings
  • typeAliases
  • typeHandler
  • ObjectFactory
  • environment
  • DatabaseIdProvider
  • Mapper

其中最重要的便是 Mapper.

ii. SqlSession, 构建 SqlSession 只需要调用工厂的 build() 方法, 构建符合 Configuration 的 SqlSession. SqlSession 是一个接口, 定义了 Insert/Update/Delete/Select 等操作. 例如在 update操作中, 我们最后执行的是 excutor 的操作, 传入的 MappedStatement

iii. MappedStatement 存放的是映射器的一个节点, 包括配置好的 SQL, 缓存, resultMap, parameterType, resultType 等内容, 其中 Sql 存放在 SqlSource 中的 BoundSql 中, 也包括了 parameterObject 与 parameterMappings等.

iv. Excutor 是执行器接口, 分为了 Batch/Base/Caching/Reuse/Simple 这几种, 一般使用的默认是 SimpleExcutor. Base是模版 abstractExcutor 执行器, Reuse 可以将执行器重用, batch 批量专用, Cacheing 支持二级缓存(待跟进). 以 SimpleExcutor 为例, 会调用 SimpleStatementHandler 帮助实现 update(), 在 handler 的 update() 方法中, 我们最后看到通过 BoundSql 执行了最后的 sql.

那么我们实际使用的时候的是传入一个 Mapper, 在一个Mapper中我们定义了诸多数据库操作, 如何 SqlSession 如何执行 Mapper 定义的这些数据库操作呢?

答案是就是代理, Mapper 主要由 mapperRegistry 添加(addMappers()) 与获取(getMapper()). mapperRegistry 维护了一个哈希表 knownMappers, 用于存储从 Class 类型到代理工厂 MapperProxyFactory 的映射. 使用时通过类获取其代理工厂, 代理工厂生产出代理 MapperProxy, 通过代理执行 invoke() (即InvvocationHandler()的invoke() 函数).

在 invoke() 方法中, 如果传进来是一个类, 直接执行 method 的 invoke 方法(), 但传入的 Mapper 是一个接口. 如果是默认方法(这里也不是), 最后 cachedMapperMethod(method).execute(SqlSession, args). 最后在这里, 我们看到了分成了 Insert/Update/Delete/Select 等操作. 分别对应 SqlSession 的对应操作.

其余特性

动态 SQL

动态 SQL 主要包含以下几个语句

  • if
  • choose (相当于 switch case)
  • trim (辅助)
  • foreach

例如使用 if

<select id="findGenji" parameterType="string" resultMap="roleResultMap">
    select role_no, role_name, note from t_role
    <where>
    <if test="roleName != null and roleName != ''">
        and role_name like concat('%', #{roleName}, '%')
    </if>
    </where>
    <if testcase>
        <!-- SQL here --->
    </if>
    <where>
</select>

where 标签是当 if 标签内的条件成立时, 才会将 where 加入sql, 或者也可以使用 trim 进行消除. test 用于判断真假.

Spring 支持 配置 MyBatis Spring 需要分别执行

  • 配置数据源
  • 配置 SqlSessionFactory
  • 配置 SqlSessionTemplate
  • 配置 Mapper
  • 事务处理

例如, 我们以 Bean 的方式初始化数据源:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName">
        <value>com.mysql.jdbc.Driver</driver>
    </property>
    <property name="url">
        <value>jdbc://msql://localhost:3306/mybatis</value>
    </property>
    <property name="username">
        <value>root</value>
    </property>
    <property name="password">
        <value></value>
    </property>
</bean>

定义 DAO

public interface UserDao{
    public Role getRole(Long id);
    public List<role> findRole(String username);
    public int insertRole(Role role);
    publci int deleteRole(Role role);
}
</role>

然后定义实现(略去), 最后在 bean 中使用(更推荐使用注解方式):

<bean id="userDao" class="wang.rancho.mybatis.dao.USERDaoImpl">
    <property name="sqlSessionTemplate" ref="sqlSessionTemplate"/>
</bean>

其余 Mapper 与事务类似.

与 Hibernate 比较

Hibernate 也是另外一种应用比较广泛的 ROM 框架, Hibernate 是建立在 POJO通过 xml 或者注解提供的规则映射到数据库表上的. 它提供的是一种全表映射的模型, 不需要开发者去编写 SQL, 只要使用 HQL 就可以了.

Hibernate 好处在于

  • 有着相当优秀的二级缓存机制, 并且可以使用第三方的缓存。
  • 消除了数据库连接
  • 消除了代码映射规则
  • 拥有良好的数据库移植性
  • Hibernate是一个全自动的框架, 开发效率高 等等

但是缺点也比较明显

  • 无法优化 SQL, 例如更新某条数据需要发送所有字段, HQL 性能较差
  • 不支持动态 SQL
  • 对复杂 SQL 支持比较差(支持原生 SQL, 但并不是传统 ORM)
  • 门槛高, 难用好

MyBatis 的优点在于 – Hibernate 缺点反过来

MyBatis 的缺点在于 – Hibernate 优点反过来 – 动态SQL调试比较难 – 不支持级联 – 组装难度比较大

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.