Mybatis那些事儿

一、前言

  我还记得去年有一次接到一个电话面试,我在给面试官介绍完项目的技术栈后,他突然问了我一句:Springboot是怎么整合mybatis的?我当时就懵逼了,怎么整合?就引个starter配置一下?感觉他听了我的回答很无语。最近比较闲,忽然想起这个事情,那么我就研究一下这个问题吧。


传送门:

Mybatis源码分析(一)MapperProxy 的初始化

Mybatis源码分析(二)MappedStatement

Mybatis源码分析(三)Annotation的支持

Mybatis源码分析(四)Mybatis执行sql的四大组件

Mybatis源码分析(五)一张图告诉你SQL语句怎么执行

Mybatis源码分析(六)Mybatis缓存机制

Mybatis源码分析(七)Mybatis插件机制


二、Mybatis

2.1 架构设计

  Mybatis真的是一款优秀的orm框架。其源码短小精悍,运用了大量的设计模式,很适合开发人员作为阅读源码的入门选择。同时Mybatis的分层也非常干净,这里我丢张网上的热图就不多深入讨论了。


三层架构
  • API接口层,暴露给开发者使用,我们一般常用的是Mapper。
  • 数据处理层,实现mybatis内部流程,包括SQL查找、解析、执行和实行结果映射处理等。
  • 基础模块层,提供最基础的功能支撑,例如事务、缓存、日志等。

2.2 执行流程

执行流程

2.2.1 初始化

  Mybatis在初始化的过程中,SqlSessionFactoryBuilder(bulider模式)会调用 XMLConfigBuilder (bulider模式)读取Mybatis-config.xml和所有的Mapper.xml文件,构建Mybatis运行的核心对象Configuration对象。然后将Configuration对象作为参数构建一个 SqlSessionFactory (工厂模式)用来创建 SqlSession 对象。

2.2.2 SqlSession

  SqlSession对象的主要功能是完成一次数据库的访问和结果的映射,默认实现类是DefaultSqlSession,它有两个必须配置的属性:ConfigurationExecutor。Configuration就是前面说到的Mybatis运行的核心对象。SqlSession对数据库的操作都是通过Executor来完成的。

  获取到 SqlSession 之后,我们还需要拿到映射器 Mapper 去执行sql方法。SqlSessiongetMapper 方法是联系应用程序和Mybatis纽带,当然MyBatis 获取 Mapper 实例采用的方式是:动态代理。动态代理类就是MapperProxy 类,后面的章节我们会对这里进行详细分析。

  此外,SqlSession是线程不安全的,所以SqlSession对象的作用域需限制方法内。但是Spring的 SqlSessionTemplate 帮我们解决了这个问题。 SqlSessionTemplate 是线程安全的,可以被多个 DAO 或映射器所共享使用。当调用 SQL 方法时(包括由 getMapper() 方法返回的映射器中的方法),SqlSessionTemplate 将会保证使用的 SqlSession 与当前 Spring 的事务相关。此外,它管理 session 的生命周期,包含必要的关闭、提交或回滚操作。另外,它也负责将 MyBatis 的异常翻译成 Spring 中的 DataAccessExceptions。具体可以去看下mybatis-spring的官方文档。

In MyBatis you use the SqlSessionFactory to create an SqlSession. Once you have a session, you use it to execute your mapped statements, commit or rollback connections and finally, when it is no longer needed, you close the session. With MyBatis-Spring you don't need to use SqlSessionFactory directly because your beans can be injected with a thread safe SqlSession that automatically commits, rollbacks and closes the session based on Spring's transaction configuration.

2.2.3 Sql语句的执行

  SQL语句的执行涉及各个组件,其中比较重要的是ExecutorStatementHandlerParameterHandlerResultSetHandler
Executor对象在创建Configuration对象的时候创建,并且缓存在Configuration对象里,负责一级缓存和二级缓存,并提供是事务管理的相关操作。Executor对象的主要功能是调用StatementHandler访问数据库,并将查询结果存入缓存中(如果配置了缓存的话。StatementHandler首先通过ParammeterHandler完成SQL的实参绑定,然后通过java.sql.Statement对象执行sql语句并得到结果集ResultSet,最后通过ResultSetHandler完成结果集的映射,得到对象并返回。

image

三、快速入门

3.1 Springboot整合Mybatis

Springboot整合Mybatis的准备工作非常简单,如果是maven工程只需要在pom.xml中完成引入即可快速上手。

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>
<!-- 添加mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.12</version>
 </dependency>

那么mybatis-spring-boot-starter做了哪些事情呢,我们看下官方文档里是这么说的

As you may already know, to use MyBatis with Spring you need at least an SqlSessionFactory and at least one mapper interface.MyBatis-Spring-Boot-Starter will:

  • Autodetect an existing DataSource
  • Will create and register an instance of a SqlSessionFactory passing that DataSource as an input using the SqlSessionFactoryBean
  • Will create and register an instance of a SqlSessionTemplate got out of the SqlSessionFactory
  • Auto-scan your mappers, link them to the SqlSessionTemplate and register them to Spring context so they can be injected into your beans
  • Springboot会自动检测我们配置的 数据源
  • 创建并注册一个 SqlSessionFactory 实例作用于 SqlSessionFactoryBean(代理对象。BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象)
  • 还会注册创建从 SqlSessionFactory 中获取的 SqlSessionTemplate 的实例。
  • 自动扫描 Mapper(这里Springboot会默认扫描带有@Mapper 注解的映射器,必要时可通过 @MapperScan 定义扫描路径使用) 将它们链接到 SqlSessionTemplate 并将它们注册到Spring context中注入容器。
/**
   * If mapper registering configuration or mapper scanning configuration not present, this configuration allow to scan
   * mappers based on the same component-scanning path as Spring Boot itself.
   */
  @org.springframework.context.annotation.Configuration
  @Import(AutoConfiguredMapperScannerRegistrar.class)
  @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
  public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
      logger.debug(
          "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
    }

  }

这里说明一下,当Spring容器找不到MapperFactoryBean.classMapperScannerConfigurer.class时,会import
AutoConfiguredMapperScannerRegistrar.class 扫描@Mapper注解的映射器继续完成注入。

 @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

      if (!AutoConfigurationPackages.has(this.beanFactory)) {
        logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
        return;
      }

      logger.debug("Searching for mappers annotated with @Mapper");

      List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
      if (logger.isDebugEnabled()) {
        packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
      }

      BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
      builder.addPropertyValue("processPropertyPlaceHolders", true);
      builder.addPropertyValue("annotationClass", Mapper.class);
      builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
      BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
      Stream.of(beanWrapper.getPropertyDescriptors())
          // Need to mybatis-spring 2.0.2+
          .filter(x -> x.getName().equals("lazyInitialization")).findAny()
          .ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
      registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
    }

不过本着能省则省的原则,还是建议使用@MapperScan来完成映射器的注入。在Application主启动类上加上@MapperScan("mapper所在路径")即可完成mybatis的初始化。

@SpringBootApplication
@MapperScan("com.example.*")
public class MybatisApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisApplication.class, args);
    }

}

3.2 编写mapper及xml

关于sql语句你也可以选择@select等注解的形式,mybatis同样是支持的。笔者个人比较钟情xml哈,可读性强便于维护。这里我实体和mapper我用generator自动生成的,挺方便的。感兴趣的小伙伴可以了解下哈。
实体:

package com.example.mybatis.model;

public class Role {
    private Long id;

    private String roleId;

    private String roleName;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleId() {
        return roleId;
    }

    public void setRoleId(String roleId) {
        this.roleId = roleId == null ? null : roleId.trim();
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName == null ? null : roleName.trim();
    }
}

mapper :映射器

package com.example.mybatis.mapper;

import com.example.mybatis.model.Role;
import org.apache.ibatis.annotations.Mapper;

//@Mapper
public interface RoleMapper {
    int deleteByPrimaryKey(Long id);

    int insert(Role record);

    int insertSelective(Role record);

    Role selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(Role record);

    int updateByPrimaryKey(Role record);
}

xml:sql语句

<?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="com.example.mybatis.mapper.RoleMapper" >
  <resultMap id="BaseResultMap" type="com.example.mybatis.model.Role" >
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="role_id" property="roleId" jdbcType="VARCHAR" />
    <result column="role_name" property="roleName" jdbcType="VARCHAR" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, role_id, role_name
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long" >
    select 
    <include refid="Base_Column_List" />
    from role
    where id = #{id,jdbcType=BIGINT}
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long" >
    delete from role
    where id = #{id,jdbcType=BIGINT}
  </delete>
  <insert id="insert" parameterType="com.example.mybatis.model.Role" >
    insert into role (id, role_id, role_name
      )
    values (#{id,jdbcType=BIGINT}, #{roleId,jdbcType=VARCHAR}, #{roleName,jdbcType=VARCHAR}
      )
  </insert>
  <insert id="insertSelective" parameterType="com.example.mybatis.model.Role" >
    insert into role
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        id,
      </if>
      <if test="roleId != null" >
        role_id,
      </if>
      <if test="roleName != null" >
        role_name,
      </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="id != null" >
        #{id,jdbcType=BIGINT},
      </if>
      <if test="roleId != null" >
        #{roleId,jdbcType=VARCHAR},
      </if>
      <if test="roleName != null" >
        #{roleName,jdbcType=VARCHAR},
      </if>
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="com.example.mybatis.model.Role" >
    update role
    <set >
      <if test="roleId != null" >
        role_id = #{roleId,jdbcType=VARCHAR},
      </if>
      <if test="roleName != null" >
        role_name = #{roleName,jdbcType=VARCHAR},
      </if>
    </set>
    where id = #{id,jdbcType=BIGINT}
  </update>
  <update id="updateByPrimaryKey" parameterType="com.example.mybatis.model.Role" >
    update role
    set role_id = #{roleId,jdbcType=VARCHAR},
      role_name = #{roleName,jdbcType=VARCHAR}
    where id = #{id,jdbcType=BIGINT}
  </update>
</mapper>

后面笔者会针对mybatis的执行流程陆续展开一些源码的分析,届时会不断把链接补充在这篇文章里。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352