MyBatis

目录

Mybatis结构

MyBatis分页方式

逻辑分页

物理分页

分页插件(PageHelper)

Mybatis代码生成器

添加依赖

新建工具类

Mybatis通用方法

Mybatis初始化

BaseBuilder

XMLConfigBuilder

XMLMapperBuilder

XMLStatementBuilder

绑定 Mapper 接口


生如蝼蚁 当立鸿鹄之志 命薄如纸 应有不屈之心 大丈夫生于天地间 岂能郁郁久居人下 乾坤未定 你我皆是黑马

MyBatis_第1张图片

iBATIS

iBATIS一词来源于"internet"和"abatis"的组合,是apache的一个开源项目(由Clinton Begin在2001年发起的开放源代码项目)。 于2010年6月16号这个项目由apache software foundation 迁移到了google code(被谷歌托管)并且改名为MyBatis ,2013年11月迁移到Github。是一个基于SQL映射支持Java和.NET的持久层框架,使用XML或注解来配置和映射原生信息,将接口和Java的POJO(Plain Ordinary Java Object,普通的Java对象)映射成数据库中的记录。查看ibatis官网:

iBATIS HomeiBATIS Data Mapping Framework for Java and .NEThttps://ibatis.apache.org/会发现有如下提示:

MyBatis_第2张图片

Mybatis

mybatis是一个优秀的基于java语言开发的持久层开源框架,内部封装jdbc,开发者只需要关注sql 语句本身,不需要加载驱动、创建连接、创建statement等操作。通过xml文件或注解的方式将要执行的各种statement配置起来,然后java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,由mybatis框架执行sql并将结果映射为java对象并返回。采用ORM(Object relational mapping)思想解决了实体和数据库映射的问题,对jdbc的封装,屏蔽了jdbc api底层访问细节,不用与jdbc api打交道就可以完成对数据库的持久化操作,简化开发。以前用JDBC是这样的:

public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            //加载数据库驱动
            Class.forName("com.mysql.jdbc.Driver");
            //通过驱动管理类获取数据库链接
            connection = DriverManager
            .getConnection("jdbc:mysql://localhost:3306/mybatis?
            characterEncoding=utf-8", "root", " root");
            //定义 sql 语句 ?表示占位符
            String sql = "select * from user where username = ?";
            //获取预处理 statement
            preparedStatement = connection.prepareStatement(sql);
            //设置参数,第一个参数为 sql 语句中参数的序号(从 1 开始)
            //第二个参数为设置的参数值
            preparedStatement.setString(1, "王五");
            //向数据库发出 sql 执行查询,查询出结果集
            resultSet = preparedStatement.executeQuery();
            //遍历查询结果集
            while (resultSet.next()) {      
            System.out.println(resultSet.getString("id")+""
            +resultSet.getString("username"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放资源
            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) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
}

数据库连接创建、连接释放操作频繁会造成系统资源的浪费影响系统性能,使用数据库连接池解决了此问题。Sql语句在代码中硬编码,代码不易维护。例如结果集解析(查询列名)sql变化导致解析代码变更。Mybatis内置数据库连接池、提供DAO层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试、提供XML标签,支持编写动态sql,sql语句写在xml文件里面,便于统一进行管理。

mybatis – MyBatis 3 | Introductionhttps://mybatis.org/mybatis-3/

Mybatis-Plus

MyBatis-Plus (opens new window)(简称 MP)在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生。MyBatis-PlusMyBatis-Plus 官方文档https://baomidou.com/

1、无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作BaseMapper。
2、强大的CRUD操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD操作,更有强大的条件构造器,满足各类使用需求,简单的CRUD操作不用自己编写。
3、支持Lambda形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错。
4、支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题。
5、支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作。
6、支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
7、内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用(自动生成代码)。
8、内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询。
9、分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库。
10、内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询。
11、内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作。


Mybatis结构

Mybatis持久层框架具有轻量级、解耦(业务逻辑和数据访问分离,使系统结构更清晰,易维护,更易单元测试)的特点,分为以下几层:

API接口层:提供一些API接口,通过这些API接口来操纵数据库,接口层收到调用请求会调用数据处理层来完成具体的数据处理。

数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等,主要目的是根据调用的请求完成一次数据库操作。

基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的功能,将他们抽取出来作为最基础的组件,为上层的数据处理层提供最基础的支撑。

IBatis与MyBatis

在sqlMap里面,iBatis的传入参数是parameterClass,而MyBatis是可以不写的,也可以用parameterType,iBatis的传出参数是resultClass

iBatis:


MyBatis:
 

条件判断语句对于MyBatis很简单,标签里面写判断条件即可。但是IBatis就麻烦了许多,它将每个方法都进行了封装

例如:
isNull:判断property字段是否是null        


isEqual相当于equals,判断状态值。
  

isEmpty判断参数是否为Null或者空,满足其中一个条件则其true。
isNotEmpty相反,当参数既不为Null也不为空时其为true。 
        
Ibatis:
  
         username=#userNameList[]#  

  
Mybaits:

         #{item.id}

#{}和${}

select * from user where name = #{name};
select * from user where name = ${name};

//解析后结果为:
select * from user where name = 'zhangsan';

//#{}和${}在预编译中的处理是不一样的,#{}在预处理时会把参数部分用一个占位符?代替,变成:
select * from user where name = ?;
//${}则只是简单的字符串替换,在动态解析阶段sql语句会被解析成:
select * from user where name = 'zhangsan';

//#{}的参数替换是发生在DBMS中,而${}发生在动态解析过程中,${}会导致sql注入的问题。

MyBatis和Hibernate

灵活性

MyBatis操作灵活,使用起来比较方便。hibernate是全自动,而mybatis是半自动。hibernate可以通过对象关系模型实现对数据库的操作,通过实体对象与数据库的表进行映射来自动生成sql。而mybatis仅有基本的字段映射,对象数据以及对象实际关系仍然需要通过手写sql来实现和管理

可移植性

Hibernate数据库移植性远大于Mybatis。hibernate通过它强大的映射结构和hql语言,大大降低了对象与不同数据库(oracle、MySQL等)的耦合性,而mybatis由于需要手写sql,因此sql中很容易包含一些不同的数据库不兼容的函数或者语法,移植性也会随之降低很多,成本很高.

优化上

在sql优化上,mybatis要比hibernate方便一些,由于mybatis的sql都是写在xml里,因此优化sql比hibernate方便很多。而hibernate的sql很多都是自动生成的,无法直接维护sql;虽有hql,但功能还是不及sql强大,像报表等需求时,hql满足不了需求;hibernate虽然也支持原生sql,但开发模式上却与orm不同,需要转换思维,因此使用上不是非常方便。总之写sql的灵活度上hibernate不及mybatis。

二级缓存

hibernate拥有更好的二级缓存,它的二级缓存可以自行更换为第三方的二级缓存。

MyBatis分页方式

逻辑分页

使用MyBatis自带的RowBounds进行分页,一次性查询很多数据,然后在结果中检索分页的数据。这样弊端是需要消耗大量的内存、有内存溢出的风险、对数据库压力较大。 RowBounds不是一次性查询全部结果,因为MyBatis是对jdbc的封装,在jdbc驱动中有一个Fetch Size的配置,它规定了每次最多从数据库查询多少条数据,假如你要查询更多数据,它会在你执行next()的时候,去查询更多的数据。

物理分页

自己手写SQL分页或使用分页插件PageHelper,从数据库查询指定条数的数据。

原理

分页插件的基本原理是使用MyBatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的SQL,然后重写SQL,根据dialect方言,添加对应的物理分页语句和物理分页参数。

分页插件(PageHelper)

        //maven依赖
          
            com.github.pagehelper  
            pagehelper  
            5.1.8  
        
            com.github.pagehelper
            pagehelper-spring-boot-starter
            1.2.10
        

分页

       SqlSession sqlSession = sessionFactory.openSession();
       UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
       PageHelper.startPage(1,10,true);  //第一页 每页显示10条
       Page page=userMapper.findUserAll();

不分页

       PageHelper.startPage(1,-1,true);

查询总条数

       PageInfo  info=new PageInfo<>(userMapper.findUserAll());

Mybatis代码生成器

添加依赖

            
                com.baomidou
                mybatis-plus-generator
                3.3.0
            
            
                mysql
                mysql-connector-java
                5.1.25
            
            
                org.apache.velocity
                velocity-engine-core
                2.0
            

新建工具类

        public class Generator {
              public static void main(String[] args) throws IOException {
                  AutoGenerator mpg = new AutoGenerator();// 代码生成器
                  GlobalConfig gc = new GlobalConfig(); // 全局配置
                  String projectPath = "D:\\parkson-platform-dev\\account"; //当前项目路径
                  gc.setOutputDir(projectPath + "/src/main/java");//输出路径
                  gc.setAuthor("parkson");//作者
                  gc.setOpen(false);
                  gc.setSwagger2(true);//实体属性 Swagger2 注解
                  mpg.setGlobalConfig(gc);
                  DataSourceConfig dsc = new DataSourceConfig();// 数据源配置
                  //设置数据库地址
                  dsc.setUrl("jdbc:mysql://200.86.3.85:8003/parkson_account_dev?serverTimezone=UTC&characterEncoding=UTF-8");
                  //dsc.setSchemaName("public");
                  dsc.setDriverName("com.mysql.jdbc.Driver");
                  dsc.setUsername("root");
                  dsc.setPassword("parkson1016");
                  mpg.setDataSource(dsc);
                  PackageConfig pc = new PackageConfig();// 包配置
                  // pc.setModuleName(scanner("模块名"));
                  pc.setModuleName("");
                  pc.setParent("com.parkson.platform.account");//生成的代码路径
                  mpg.setPackageInfo(pc);

                  // 自定义配置
                  InjectionConfig cfg = new InjectionConfig() {
                      @Override
                      public void initMap() {
                          // to do nothing
                      }
                  };

                  // 配置自定义输出模板
                  String templatePath = "/templates/mapper.xml.vm";
                  List focList = new ArrayList<>();
                  focList.add(new FileOutConfig(templatePath) {
                      @Override
                      public String outputFile(TableInfo tableInfo) {
                          // 自定义输出文件名
                          return projectPath + "/src/main/resources/mapper/"
                                  + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
                      }
                  });
                  cfg.setFileOutConfigList(focList);
                  mpg.setCfg(cfg);

                  // 配置模板
                  TemplateConfig templateConfig = new TemplateConfig();
                  templateConfig.setXml(null);
                  mpg.setTemplate(templateConfig);

                  // 策略配置
                  StrategyConfig strategy = new StrategyConfig();
                  strategy.setNaming(NamingStrategy.underline_to_camel);
                  strategy.setColumnNaming(NamingStrategy.underline_to_camel);;
                  strategy.setEntityLombokModel(true);
                  strategy.setRestControllerStyle(true);
                  strategy.setControllerMappingHyphenStyle(true);
                  strategy.setTablePrefix(pc.getModuleName() + "_");
                  strategy.setInclude("t_vcard");
                  mpg.setStrategy(strategy);
                  //mpg.setTemplateEngine(new VelocityTemplateEngine());
                  mpg.execute();
              }
      }

示例2

    
        com.oracle
        ojdbc7
        12.1.0.2
    
  /**
   * Oracle数据库
   * mybatis-plus代码生成器
   */
  public class Generator {

      public static void main(String[] args) {
          AutoGenerator mpg = new AutoGenerator();

          // 全局配置
          GlobalConfig gc = new GlobalConfig();
          gc.setOutputDir("E:\\Mybatis_Plus");// 生产文件所在的目录
          gc.setFileOverride(true); // 默认 false ,是否覆盖已生成文件
          gc.setOpen(false); //默认true ,是否打开文件生产所在的目录
          gc.setIdType(IdType.ASSIGN_UUID); // 指定生成的主键类型
          gc.setActiveRecord(true);
          gc.setEnableCache(false);// XML 二级缓存
          gc.setBaseResultMap(true);// XML ResultMap
          gc.setBaseColumnList(false);// XML columList
          gc.setAuthor("[email protected]");
          // 生成文件命名('%s'会自动填充表名)
          gc.setMapperName("%sMapper");
          gc.setXmlName("%sMapper");
          gc.setServiceName("%sService");
          gc.setServiceImplName("%sServiceImpl");
          gc.setControllerName("%sController");
          gc.setSwagger2(true);
          mpg.setGlobalConfig(gc);

          // 数据源配置
          DataSourceConfig dsc = new DataSourceConfig();
          dsc.setDbType(DbType.ORACLE);
          dsc.setDriverName("oracle.jdbc.driver.OracleDriver");
          dsc.setUsername("qms");
          dsc.setPassword("csotqms1129");
          dsc.setUrl("jdbc:oracle:thin:@10.108.234.130:1521:d2pidb1");
          mpg.setDataSource(dsc);

          // 策略配置
          StrategyConfig strategy = new StrategyConfig();
          //strategy.setTablePrefix(new String[]{"D_"});// 设置表前缀
          strategy.setNaming(NamingStrategy.underline_to_camel);// 表名生成策略
          strategy.setInclude(new String[]{"QMS_LAB_APP_RESULTS"}); // 需要生成的表
          mpg.setStrategy(strategy);

          // 文件生产所在目录配置
          PackageConfig pc = new PackageConfig();
          pc.setParent("com.csot.qms");
          pc.setEntity("entity");
          pc.setMapper("mapper");
          pc.setXml("mapper");
          pc.setService("service");
          pc.setServiceImpl("service.impl");
          pc.setController("controller");
          mpg.setPackageInfo(pc);

          // 注入自定义配置,可以在VM中使用cfg.abc设置的值
          InjectionConfig cfg = new InjectionConfig() {
              @Override
              public void initMap() {
              }
          };
          // 配置模板
          TemplateConfig tc = new TemplateConfig();
          // templates/entity.java 模板路径配置,默认在templates目录下,.vm 后缀不用加
          tc.setEntity("templates/entity.java");//使用自定义模板生成实体类
          tc.setXml("");
          //mpg.setTemplate(tc);
          mpg.setCfg(cfg);
          // 执行生成
          mpg.execute();
          // 打印注入设置
          //System.err.println(mpg.getCfg().getMap().get("abc"));
      }
  }

Mybatis通用方法

tk.mybatis包提供通用的增删改查的方法

        
        
            tk.mybatis
            mapper
            4.0.4
        

更新

根据主键更新数据,更新NULL的字段,

updateByPrimaryKey(T var1) 

根据主键更新有值的数据,

updateByPrimaryKeySelective(T var1)

根据Condition条件更新实体record包含的全部属性,null值会被更新,

updateByCondition(@Param("record") T var1, @Param("condition") Object var2) 

根据Condition条件更新实体record包含的不是null的属性值,

updateByConditionSelective(@Param("record") T var1, @Param("condition") Object var2) 

根据Example条件更新实体record包含的不是null的属性值,

updateByExampleSelective(@Param("record") T var1, @Param("example") Object var2)

根据Example条件更新实体record包含的全部属性,null值会被更新,

updateByExample(@Param("record") T var1, @Param("example") Object var2)

根据主键批量修改。

updateListByPrimaryKey(List recordList);


根据指定条件更新指定字段的情况上面几种方法可能都不满足,就只能自己写sql了。

       updateStatusByPrimaryKey(InsureOrgSchemesInfo insureOrgSchemesInfo);
       
               update INSURE_ORG_SCHEMES_INFO a
               
                   
                       a.state = #{state}
                   
               
               where a.id = #{id}
       

插入

批量新增

insertList(List var1):

所有的字段都会添加一遍,即使有的字段没有值

insert(T var1) 


只给有值的字段赋值(会对传进来的值做非空判断)

insertSelective(T var1) 

insert和insertSelective在数据库中的效果是一样的,只是sql语句不同。
insertUseGeneratedKeys():
官网文档:MySQL :: MySQL 5.6 Reference Manual :: 13.2.5.2 INSERT ... ON DUPLICATE KEY UPDATE Statement

删除

delete(T var1);

     DELETE FROM db_test.t_test_table 
     
       
       AND id = #{id}
      
       
      AND col = #{col}
      
     

deleteByPrimaryKey(Object var1):

    DELETE FROM db_test.t_test_table 
     
     AND id = #{id}
    

根据条件删除

deleteByExample(Object var1) 

Example example = new Example(User.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("name","王小二");//相当于 where name = "王小二"
deleteByExample(example );

查询

根据条件查询,作非空判断。

select(T var1) 

根据条件查询 查询行数

selectCount(T var1)

查询全部

selectAll() 

根据Example条件查询

selectByExample(Object var1)

根据Example条件查询行数

selectCountByExample(Object var1)

根据条件查询一个

selectOne(T var1)

selectOneByExample(Object var1)

根据example条件和RowBounds进行分页查询

selectByExampleAndRowBounds(Object var1, RowBounds var2)

根据实体条件和RowBounds进行分页查询

selectByRowBounds(T var1, RowBounds var2)

根据主键查询

selectByPrimaryKey(Object var1) 

Mybatis初始化

和Spring框架的IoC容器初始化 一样,Mybatis也会通过解析相应的配置文件完成自己的初始化。Mybatis 的配置文件主要有 mybatis-config.xml 核心配置文件 及一系列映射配置文件,另外,Mybatis 也会根据注解进行配置。

BaseBuilder

Mybatis 初始化 的主要内容是加载并解析 mybatis-config.xml 配置文件、映射配置文件以及相关的注解信息。Mybatis 的初始化入口是 SqlSessionFactoryBuilder 的 build()方法。

public class SqlSessionFactoryBuilder {

  public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
  }

  public SqlSessionFactory build(Reader reader, String environment) {
    return build(reader, environment, null);
  }

  public SqlSessionFactory build(Reader reader, Properties properties) {
    return build(reader, null, properties);
  }

  /**
   * build()方法 的主要实现
   */
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      // SqlSessionFactory 会创建 XMLConfigBuilder对象 来解析 mybatis-config.xml配置文件
      // XMLConfigBuilder 继承自 BaseBuilder抽象类,顾名思义这一系的类使用了 建造者设计模式
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      // 解析配置文件的内容 到 Configuration对象,根据 Configuration对象
      // 创建 DefaultSqlSessionFactory对象,然后返回
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        // 关闭配置文件输入流
        reader.close();
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

BaseBuilder

public abstract class BaseBuilder {

  // 保存了 Mybatis 的几乎所以核心配置信息,全局唯一
  protected final Configuration configuration;
  // 在 mybatis-config.xml 中可以通过 标签 定义别名
  protected final TypeAliasRegistry typeAliasRegistry;
  // 在 mybatis-config.xml 中可以通过 标签 添加 自定义TypeHandler
  // TypeHandler 用于完成 JDBC数据类型 与 Java类型 的相互转换,所有的 TypeHandler
  // 都保存在 typeHandlerRegistry 中
  protected final TypeHandlerRegistry typeHandlerRegistry;

  public BaseBuilder(Configuration configuration) {
    this.configuration = configuration;
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }
}

BaseBuilder 中的 typeAliasRegistry 和 typeHandlerRegistry 字段 均来自于 configuration,通过 BaseBuilder 的构造方法可以看到详细内容

XMLConfigBuilder

XMLConfigBuilder 是 BaseBuilder 的众多子类之一,主要负责解析 mybatis-config.xml 配置文件。它通过调用 parseConfiguration()方法 实现整个解析过程,其中,mybatis-config.xml 配置文件 中的每个节点都被封装成了一个个相应的解析方法,parseConfiguration()方法 只是依次调用了这些解析方法而已。

public class XMLConfigBuilder extends BaseBuilder {

  // 标记是否解析过 mybatis-config.xml文件
  private boolean parsed;
  // 用于解析 mybatis-config.xml 的解析器
  private final XPathParser parser;
  // 标识 配置 的名称,默认读取 标签 的 default属性
  private String environment;
  // 创建并缓存 Reflector对象
  private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

  /**
   * 解析的入口,调用了 parseConfiguration() 进行后续的解析
   */
  public Configuration parse() {
    // parsed标志位 的处理
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 在 mybatis-config.xml配置文件 中查找 节点,并开始解析
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  private void parseConfiguration(XNode root) {
    try {
      // 根据 root.evalNode("properties") 中的值就可以知道具体是解析哪个标签的方法咯
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

Mybatis 中的标签很多,所以相对应的解析方法也很多,这里挑几个比较重要的标签进行分析。

解析标签

 private void typeHandlerElement(XNode parent) throws Exception {
    if (parent != null) {
      // 处理  下的所有子标签
      for (XNode child : parent.getChildren()) {
        // 处理  标签
        if ("package".equals(child.getName())) {
          // 获取指定的包名
          String typeHandlerPackage = child.getStringAttribute("name");
          // 通过 typeHandlerRegistry 的 register(packageName)方法
          // 扫描指定包中的所有 TypeHandler类,并进行注册
          typeHandlerRegistry.register(typeHandlerPackage);
        } else {
          // Java数据类型
          String javaTypeName = child.getStringAttribute("javaType");
          // JDBC数据类型
          String jdbcTypeName = child.getStringAttribute("jdbcType");
          String handlerTypeName = child.getStringAttribute("handler");
          Class javaTypeClass = resolveClass(javaTypeName);
          JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
          Class typeHandlerClass = resolveClass(handlerTypeName);
          // 注册
          if (javaTypeClass != null) {
            if (jdbcType == null) {
              typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
            } else {
              typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
            }
          } else {
            typeHandlerRegistry.register(typeHandlerClass);
          }
        }
      }
    }
  }

解析标签

/**
   * Mybatis 可以配置多个 环境,分别用于开发、测试及生产等,
   * 但每个 SqlSessionFactory实例 只能选择其一
   */
  private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      // 如果未指定 XMLConfigBuilder 的 environment字段,则使用 default属性 指定的 环境
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      // 遍历 节点
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          // 实例化 TransactionFactory
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          // 创建 DataSourceFactory 和 DataSource
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          // 创建的 Environment对象 中封装了上面的 TransactionFactory对象 和 DataSource对象
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          // 为 configuration 注入 environment属性值
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

解析标签

Mybatis 不像 Hibernate 那样,通过 HQL 的方式直接帮助开发人员屏蔽不同数据库产品在 sql 语法 上的差异,针对不同的数据库产品, Mybatis 往往要编写不同的 sql 语句。但在 mybatis-config.xml 配置文件 中,可以通过 定义所有支持的数据库产品的 databaseId,然后在映射配置文件中定义 sql 语句节点 时,通过 databaseId 指定该 sql 语句 应用的数据库产品,也可以达到类似的屏蔽数据库产品的功能。Mybatis 初始化时,会根据前面解析到的 DataSource 来确认当前使用的数据库产品,然后在解析映射文件时,加载不带 databaseId 属性 的 sql 语句 及带有 databaseId 属性 的 sql 语句,其中,带有 databaseId 属性 的 sql 语句 优先级更高,会被优先选中。

 /**
   * 解析 节点,并创建指定的 DatabaseIdProvider对象,
   * 该对象会返回 databaseId的值,Mybatis 会根据 databaseId 选择对应的 sql语句 去执行
   */
  private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    if (context != null) {
      String type = context.getStringAttribute("type");
      // 为了保证兼容性,修改 type取值
      if ("VENDOR".equals(type)) {
          type = "DB_VENDOR";
      }
      // 解析相关配置信息
      Properties properties = context.getChildrenAsProperties();
      // 创建 DatabaseIdProvider对象
      databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
      // 配置 DatabaseIdProvider,完成初始化
      databaseIdProvider.setProperties(properties);
    }
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
      // 根据前面解析到的 DataSource 获取 databaseId,并记录到 configuration 的 configuration属性 上
      String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
      configuration.setDatabaseId(databaseId);
    }
  }

Mybatis 提供了 DatabaseIdProvider 接口,该接口的核心方法为 getDatabaseId(DataSource dataSource),主要根据 dataSource 查找对应的 databaseId 并返回。该接口的主要实现类为 VendorDatabaseIdProvider。

public class VendorDatabaseIdProvider implements DatabaseIdProvider {

  private static final Log log = LogFactory.getLog(VendorDatabaseIdProvider.class);

  private Properties properties;

  @Override
  public void setProperties(Properties p) {
    this.properties = p;
  }

  @Override
  public String getDatabaseId(DataSource dataSource) {
    if (dataSource == null) {
      throw new NullPointerException("dataSource cannot be null");
    }
    try {
      return getDatabaseName(dataSource);
    } catch (Exception e) {
      log.error("Could not get a databaseId from dataSource", e);
    }
    return null;
  }

  private String getDatabaseName(DataSource dataSource) throws SQLException {
    // 解析到数据库产品名
    String productName = getDatabaseProductName(dataSource);
    if (this.properties != null) {
      // 根据 子节点 配置的数据库产品和 databaseId 之间的对应关系,
      // 确定最终使用的 databaseId
      for (Map.Entry property : properties.entrySet()) {
        if (productName.contains((String) property.getKey())) {
          return (String) property.getValue();
        }
      }
      // 没有合适的 databaseId,则返回 null
      return null;
    }
    return productName;
  }

  // 根据 dataSource 获取 数据库产品名的具体实现
  private String getDatabaseProductName(DataSource dataSource) throws SQLException {
    Connection con = null;
    try {
      con = dataSource.getConnection();
      DatabaseMetaData metaData = con.getMetaData();
      return metaData.getDatabaseProductName();
    } finally {
      if (con != null) {
        try {
          con.close();
        } catch (SQLException e) {
          // ignored
        }
      }
    }
  }
}

解析标签

Mybatis 初始化时,除了加载 mybatis-config.xml 文件,还会加载全部的映射配置文件,mybatis-config.xml 文件的 节点 会告诉 Mybatis 去哪里查找映射配置文件,及使用了配置注解标识的接口。

/**
   * 解析 节点,本方法会创建 XMLMapperBuilder对象 加载映射文件,如果映射配置文件存在
   * 相应的 Mapper接口,也会加载相应的 Mapper接口,解析其中的注解 并完成向 MapperRegistry 的注册
   */
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      // 处理  的子节点
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          // 获取 子节点 中的包名
          String mapperPackage = child.getStringAttribute("name");
          // 扫描指定的包目录,然后向 MapperRegistry 注册 Mapper接口
          configuration.addMappers(mapperPackage);
        } else {
          // 获取 节点 的 resource、url、mapperClass属性,这三个属性互斥,只能有一个不为空
          // Mybatis 提供了通过包名、映射文件路径、类全名、URL 四种方式引入映射器。
          // 映射器由一个接口和一个 XML配置文件 组成,XML文件 中定义了一个 命名空间namespace,
          // 它的值就是接口对应的全路径。
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          // 如果 节点 指定了 resource 或是 url属性,则创建 XMLMapperBuilder对象 解析
          // resource 或是 url属性 指定的 Mapper配置文件
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            // 如果 节点 指定了 class属性,则向 MapperRegistry 注册 该Mapper接口
            Class mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

XMLMapperBuilder

和 XMLConfigBuilder 一样,XMLMapperBuilder 也继承了 BaseBuilder,其主要负责解析映射配置文件,其解析配置文件的入口方法也是 parse(),另外,XMLMapperBuilder 也将各个节点的解析过程拆分成了一个个小方法,然后由 configurationElement()方法 统一调用

public class XMLMapperBuilder extends BaseBuilder {
  public void parse() {
    // 是否已经加载过该配置文件
    if (!configuration.isResourceLoaded(resource)) {
      // 解析 节点
      configurationElement(parser.evalNode("/mapper"));
      // 将 resource 添加到 configuration 的 loadedResources属性 中,
      // 该属性是一个 HashSet类型的集合,其中记录了已经加载过的映射文件
      configuration.addLoadedResource(resource);
      // 注册 Mapper接口
      bindMapperForNamespace();
    }
    // 处理 configurationElement()方法 中解析失败的 节点
    parsePendingResultMaps();
    // 处理 configurationElement()方法 中解析失败的 节点
    parsePendingCacheRefs();
    // 处理 configurationElement()方法 中解析失败的 节点
    parsePendingStatements();
  }

  private void configurationElement(XNode context) {
    try {
      // 获取 节点 的 namespace属性
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // 使用 MapperBuilderAssistant对象 的 currentNamespace属性 记录 namespace命名空间
      builderAssistant.setCurrentNamespace(namespace);
      // 解析 节点,后面的解析方法 也都见名知意
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
}

XMLMapperBuilder 也根据配置文件进行了一系列节点解析,我们着重分析一下比较重要且常见的 节点 和 节点

解析节点

select 语句 查询得到的结果是一张二维表,水平方向上是一个个字段,垂直方向上是一条条记录。而 Java 是面向对象的程序设计语言,对象是根据类的定义创建的,类之间的引用关系可以认为是嵌套结构。JDBC 编程 中,为了将结果集中的数据映射成 VO 对象,我们需要自己写代码从结果集中获取数据,然后将数据封装成对应的 VO 对象,并设置好对象之间的关系,这种 ORM 的过程中存在大量重复的代码。

Mybatis 通过 节点 定义了 ORM 规则,可以满足大部分的映射需求,减少重复代码,提高开发效率。

在分析 节点 的解析过程之前,先看一下该过程使用的数据结构。每个 ResultMapping 对象 记录了结果集中的一列与 JavaBean 中一个属性之间的映射关系。节点 下除了 子节点 的其它子节点,都会被解析成对应的 ResultMapping 对象。

public class ResultMapping {

  private Configuration configuration;
  // 对应节点的 property属性,表示 该列进行映射的属性
  private String property;
  // 对应节点的 column属性,表示 从数据库中得到的列名或列名的别名
  private String column;
  // 表示 一个 JavaBean 的完全限定名,或一个类型别名
  private Class javaType;
  // 进行映射列的 JDBC类型
  private JdbcType jdbcType;
  // 类型处理器
  private TypeHandler typeHandler;
  // 该属性通过 id 引用了另一个 节点,它负责将结果集中的一部分列映射成
  // 它所关联的结果对象。这样我们就可以通过 join方式 进行关联查询,然后直接映射成
  // 多个对象,并同时设置这些对象之间的组合关系(nested嵌套的)
  private String nestedResultMapId;
  // 该属性通过 id 引用了另一个