来自MyBatis不一样收获结果的探索之旅-v3.5.9
阅读原文时间:2022年01月06日阅读:6

概述

MyBatis官网 https://mybatis.org/mybatis-3/ 最新版本为3.5.9

MyBatis是一个的ORM框架,支持自定义SQL、存储过程和高级映射。MyBatis对JDBC做了稳定封装使我们不再需要直接操作繁琐JDBC的代码进行参数的手动设置和结果检索。在MyBatis可以使用XML、注解和Java pojo映射到数据库记录。

由于我们Java技术栈程序员对MyBatis都已非常熟悉,本篇我们则主要是从源码的角度去收获不一样MyBatis理解之旅。众所周知JDBC操作核心步骤包括创建连接、创建Statement和执行SQL语句、ResultSet接收结果;MyBatis的ORM核心思想是用于实现面向对象编程语言里不同类型系统的数据之间的转换,我们接下来探索三个问题。

  • 如何获取数据库源?
  • 如何执行Sql语句?
  • 结果集如何处理?

JDBC

我们先简单回顾下JDBC操作MySQL数据库的例子

package org.apache.ibatis.itxs;

import java.sql.*;

public class JDBCMain {
  public static void main(String[] args) {
    Connection connection = null;
    Statement st = null;
    ResultSet rs = null;

    try {
      //第一种注册驱动
      DriverManager.registerDriver(new com.mysql.jdbc.Driver());

      //第二种方式也可加载驱动程序
      //Class.forName("com.mysql.jdbc.Driver");

      //2.建立连接,参数一:协议+访问数据库,参数二:用户名,参数三:密码
      connection = DriverManager.getConnection("192.168.50.68:3306/testdb?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull", "test", "");

      //3.创建statement,跟数据库打交道一定需要这个对象
      st = connection.createStatement();

      //4.执行查询
      String sql = "select * from blog";
      rs = st.executeQuery(sql);

      //5.遍历查询每一条记录
      while(rs.next()) {
        int id = rs.getInt("id");
        String title = rs.getString("title");
        String content = rs.getString("content");
        System.out.println("id = " + id + "; title = " + title + "; content = " + content);
      }

      connection.close();
      st.close();
      rs.close();

    } catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

搭建源码调试环境

mybatis源码https://github.com/mybatis/mybatis-3

由于我们直接在mybatis源码演示demo,所以需要先将mybatis源码下的pom中mysql-connector-java依赖的test注释掉,

数据库准备Blog表并初始数据,新建resources并标记为资源目录,常见mybatis-config.xml和db.properties文件,创建实体类Blog、还有对应Mapper接口、Mapper xml文件。创建一个main类调试。

Blog表语句

CREATE TABLE `blog` (
  `id` BIGINT NOT NULL,
  `title` VARCHAR(100) DEFAULT NULL,
  `content` VARCHAR(300) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
insert into blog(id,title,content) values(1,'Java','面向对象');

mybatis-config.xml

<?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>
  <properties resource="config/db.properties"></properties>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <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="org/apache/ibatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

db.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://192.168.50.68:3306/testdb?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull
username=test
password=

Blog.java

package org.apache.ibatis.example;

public class Blog {
  private long id;
  private String title;
  private String content;

  public long getId() {
    return id;
  }

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

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getContent() {
    return content;
  }

  public void setContent(String content) {
    this.content = content;
  }

  @Override
  public String toString() {
    return "Blog{" +
      "id=" + id +
      ", title='" + title + '\'' +
      ", content='" + content + '\'' +
      '}';
  }
}

BlogMapper.java

package org.apache.ibatis.example;

public interface BlogMapper {
  Blog selectBlog111(Long id);
}

BlogMapper.xml

<?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="org.apache.ibatis.example.BlogMapper">
  <select id="selectBlog" resultType="org.apache.ibatis.example.Blog">
    select * from Blog where id = #{id}
  </select>
</mapper>

Main.java

package org.apache.ibatis.itxs;

import org.apache.ibatis.example.Blog;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

public class Main {
  public static void main(String[] args) {
    String resource = "config/mybatis-config.xml";
    InputStream inputStream = null;
    try {
      inputStream = Resources.getResourceAsStream(resource);
      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
      try (SqlSession session = sqlSessionFactory.openSession()) {
        Blog blog = session.selectOne("org.apache.ibatis.example.BlogMapper.selectBlog", 1);
        System.out.printf(blog.toString());
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

如何获取数据源

加载数据源配置信息

我们这里采用基于XML配置文件方式来演示,这样方式来学习通过mybatis源码笔者也认为最能得到,是Main类的main方法逐步调试进入mybatis的源码,从下面的堆栈调用链可以看出操作mybatis是从SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream)这一行开始,在这里先读取mybatis配置文件数据加载到全局配置Configuration里

首先parseConfiguration(XNode root)这个方法主要是初始化处理mybatis配置文件,我们发现配置文件中的在源码中是放在最前面,这个也解释如果我们把这行放在environments节点后面XML格式校验提示错误。org.apache.ibatis.builder.xml.XMLConfigBuilder中parseConfiguration方法代码如下:

  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(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);
    }
  }

在org.apache.ibatis.builder.xml.XMLConfigBuilder中environmentsElement(XNode context)这个方法处理environments节点,通过configuration.setEnvironment(environmentBuilder.build())将数据源的配置存储数据源工厂中,便于后续操作sql时候可以从配置类中获取信息进而创建连接。

Configuration可以说是mybatis最重要的一个全局类,mybatis大部分重要对象都存在Configuration里,Environment在Configuration里面,而JDBC的数据源是在Environment类中。mybatis采用工厂模式创建数据源和管理连接池(池化技术),不过在实际项目开发中,我们都会使用阿里的druid或者hikari等三方的连接池。

如何操作JDBC

首先在mybatis源码的pom有包含mysql-connector-java的依赖,上面我们也已把test注释掉,在程序加载时候org.apache.ibatis.datasource.unpooled.UnpooledDataSource类通过静态代码块方式将mysql驱动写入registeredDrivers的ConcurrentHashMap里。

在UnpooledDataSource中doGetConnection方法调用获取连接,在这里我们见到熟悉的JDBC操作代码(DriverManager.getConnection),并返回一个JDBC的连接供后续操作使用。该方法主要做下面三个工作:

  • initializeDriver,初始化驱动,如果在前面的静态代码块中没有扫描到需要的数据库驱动,则需要在这里加载需要的驱动。

  • 根据配置信息获取一个新的连接。

  • 配置创建的连接对象。

    private Connection doGetConnection(Properties properties) throws SQLException {
    initializeDriver();
    Connection connection = DriverManager.getConnection(url, properties);
    configureConnection(connection);
    return connection;
    }

    private synchronized void initializeDriver() throws SQLException {
    if (!registeredDrivers.containsKey(driver)) {
    Class driverType;
    try {
    if (driverClassLoader != null) {
    driverType = Class.forName(driver, true, driverClassLoader);
    } else {
    driverType = Resources.classForName(driver);
    }
    // DriverManager requires the driver to be loaded via the system ClassLoader.
    // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
    Driver driverInstance = (Driver) driverType.getDeclaredConstructor().newInstance();
    DriverManager.registerDriver(new DriverProxy(driverInstance));
    registeredDrivers.put(driver, driverInstance);
    } catch (Exception e) {
    throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
    }
    }
    }

    private void configureConnection(Connection conn) throws SQLException {
    if (defaultNetworkTimeout != null) {
    conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
    }
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
    conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
    conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
    }

到此我们知道MyBatis是如何一步步走到获取数据源和JDBC创建连接的过程,到这里第一个问题也已梳理清楚。

如何执行Sql语句

mapper支持类型?

我们接着从上面org.apache.ibatis.builder.xml.XMLConfigBuilder中parseConfiguration(XNode root)方法中的mapperElement(XNode parent)这里可以看到源码对于mapper的四种类型配置支持的优先级和逻辑,可以很清晰看出package优先级最高,且resource、url 、class只能存在一个,否则直接抛出异常。我们这里的例子使用的是resource方式

Mybatis配置文件mapper类型配置使用官网示例如下

mapper.xml内容在哪里?

mapper.xml存储很多操作的信息比如最核心的sql语句,看下堆栈调用链从XMLConfigBuilder->XMLMapperBuilder->XMLStatementBuilder,mybatis有很多这种Builder,每个Builder处理各自的职责。

addMappedStatement:298, MapperBuilderAssistant (org.apache.ibatis.builder)
parseStatementNode:113, XMLStatementBuilder (org.apache.ibatis.builder.xml)
buildStatementFromContext:138, XMLMapperBuilder (org.apache.ibatis.builder.xml)
buildStatementFromContext:131, XMLMapperBuilder (org.apache.ibatis.builder.xml)
configurationElement:121, XMLMapperBuilder (org.apache.ibatis.builder.xml)
parse:95, XMLMapperBuilder (org.apache.ibatis.builder.xml)
mapperElement:379, XMLConfigBuilder (org.apache.ibatis.builder.xml)
parseConfiguration:120, XMLConfigBuilder (org.apache.ibatis.builder.xml)
parse:99, XMLConfigBuilder (org.apache.ibatis.builder.xml)
build:78, SqlSessionFactoryBuilder (org.apache.ibatis.session)
build:64, SqlSessionFactoryBuilder (org.apache.ibatis.session)
main:33, Main (org.apache.ibatis.itxs)

我们这里XMLMapperBuilder则是加载解析mapper节点文件并将数据写入Configuration里面集合成员变量。

XMLStatementBuilder的parseStatementNode则是解析我们mapper.xml文件的内容,将mapper.xml的所有元素拆分并组装成MappedStatement对象,然后通过configuration.addMappedStatement(statement)的put存储到Configuration全局配置中mappedStatements的StrictMap,StrictMap是一个内部类继承HashMap,所以本质也是一个HashMap。

  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());

  protected static class StrictMap<V> extends HashMap<String, V> {

  }

执行器类型有哪些?

通过堆栈调用链,mybatis在openSession中先创建执行器,

newExecutor:691, Configuration (org.apache.ibatis.session)
openSessionFromDataSource:96, DefaultSqlSessionFactory (org.apache.ibatis.session.defaults)
openSession:47, DefaultSqlSessionFactory (org.apache.ibatis.session.defaults)
main:34, Main (org.apache.ibatis.itxs) //sqlSessionFactory.openSession()

执行器有SimpleExecutor(简单执行器)、BatchExecutor(批量执行器)、ReuseExecutor(重用执行器),然后对上面三种执行器判断是否需要缓存增强功能,在调试中可以看到cacheEnabled为true,也即是mybatis默认是开启缓存功能的,本篇就不展开研究mybatis的一二级缓存功能。

public enum ExecutorType {
  SIMPLE, REUSE, BATCH
}

Mybatis中的插件允许你针对核心组件接口Executor 、StatementHandler、ParameterHandler、ResultSetHandler中任何一个方法进行拦截调用。而每个Interceptor(拦截的接口)其实是通过JDK的动态代理技术生成的代理类,每当执行这4种接口中的方法时,就会进入拦截方法(具体就是InvocationHandler的invoke()方法)。插件这里采用的是责任链设计模式,很多对象由每一个对象对其下家的引用而连接起来形成一条链,请求在这个链上传递,直到链上的某一个对象决定处理此请求。这里设计到InterceptorChain(拦截链类)、Interceptor(拦截器接口)、Plugin(插件类,实现了InvocationHandler),具体有兴趣可以自己再深入研究。

在哪执行SQL语句

从下面当我们执行session.selectOne查询数据时

query:64, PreparedStatementHandler (org.apache.ibatis.executor.statement)
query:79, RoutingStatementHandler (org.apache.ibatis.executor.statement)
doQuery:63, SimpleExecutor (org.apache.ibatis.executor)
queryFromDatabase:325, BaseExecutor (org.apache.ibatis.executor)
query:156, BaseExecutor (org.apache.ibatis.executor)
query:109, CachingExecutor (org.apache.ibatis.executor)
query:89, CachingExecutor (org.apache.ibatis.executor)
selectList:151, DefaultSqlSession (org.apache.ibatis.session.defaults)
selectList:145, DefaultSqlSession (org.apache.ibatis.session.defaults)
selectList:140, DefaultSqlSession (org.apache.ibatis.session.defaults)
selectOne:76, DefaultSqlSession (org.apache.ibatis.session.defaults)
main:35, Main (org.apache.ibatis.itxs)  //session.selectOne("org.apache.ibatis.example.BlogMapper.selectBlog", 1);

首先从DefaultSqlSession获取MappedStatement 这里存储了我们在上一步存储的sql语句当然还有其他很多很多的信息,执行器去执行sql语句

MappedStatement ms = configuration.getMappedStatement(statement);
BoundSql boundSql = ms.getBoundSql(parameterObject);
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

//在org.apache.ibatis.executor.SimpleExecutor#doQuery完成sql语句和参数组装,这里包括sql语句解析、参数替换等内容,我们本篇不展开

stmt = prepareStatement(handler, ms.getStatementLog());

最后在org.apache.ibatis.executor.statement.PreparedStatementHandler#query,这里就非常熟悉了,这个是JDBC的PreparedStatement,到这里我们也一步步找到MyBatis使用JDBC执行查询Sql语句最终地方。

如何处理结果集?

我们接着上小节最后地方org.apache.ibatis.executor.resultset.ResultSetHandler#handleResultSets,这个就是mybatis处理结果集核心函数,mybatis提供一个非常重要结果集包装类ResultSetWrapper,这个就是MyBatis实现ORM的重要地方

ResultSetWrapper使用了columnNames、classNames、jdbcTypes三个ArrayList分别存放数据库的列名、Java类型、Mybatis jdbc对应数据库类型。

至此我们前面对MyBatis源码的三个基础问题已经全部弄清楚了。

MyBatis-Plus

MyBatis-Plus官网 https://www.baomidou.com/

MyBatis-Plus(简称 MP)是MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。是MyBatis最好的搭档,就像魂斗罗中的1P、2P,基友搭配,效率翻倍。

我们在企业数据库应用开发为提升效率更多会使用MyBatis-Plus,MyBatis-Plus宗旨就是为了简化开发而生,只做增强,简单配置就可以使用单表的CRUD,还支持丰富功能如代码生成、自动分页、逻辑删除、自动填充。MyBatis-Plus提供非常详细功能使用示例,需全面学习伙伴可自行前往官网,我们这里以一个简单Mybatis-Plus整合Spring Boot的实例抛出一个引子。

创建用户信息表:

CREATE TABLE user_info
(
   id                   INT NOT NULL AUTO_INCREMENT COMMENT '主键唯一ID',
   user_name            VARCHAR(20) COMMENT '名称',
   age                  SMALLINT COMMENT '年龄',
   create_time          DATETIME COMMENT '创建时间',
   update_time          DATETIME COMMENT '更新时间',
   VERSION              INT COMMENT '版本号',
   deleted              INT COMMENT '删除标识(0:正常,1:删除)',
   PRIMARY KEY (id)
)ENGINE=INNODB DEFAULT CHARSET=utf8;

pom文件内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itxs</groupId>
    <artifactId>mybatis-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <repositories>
        <!--阿里云仓库-->
        <repository>
            <id>aliyun</id>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>
        <!--快照版本使用,正式版本无需添加此仓库-->
        <repository>
            <id>snapshots</id>
            <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
        </repository>
    </repositories>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.2</version>
    </parent>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <java.version>1.8</java.version>
        <druid.version>1.2.8</druid.version>
        <mysql.version>8.0.27</mysql.version>
        <lombok.version>1.18.22</lombok.version>
        <mybatis-plus.version>3.5.0</mybatis-plus.version>
        <mybatis-plus-generator.version>3.5.1</mybatis-plus-generator.version>
        <freemarker.version>2.3.31</freemarker.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus-generator.version}</version>
        </dependency>

        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>${freemarker.version}</version>
        </dependency>

    </dependencies>

</project>

application.yml内容

spring:
  application:
    name: mybatis-plus-demo
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.50.95:3308/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      max-active: 1000
      min-idle: 5
      initial-size: 10
mybatis-plus:
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0
  configuration:
    map-underscore-to-camel-case: on
    call-setters-on-nulls: on
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

MyBatisPlusConfig.java

package cn.itxs.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@MapperScan("cn.itxs.mapper")
@EnableTransactionManagement
@Configuration
public class MyBatisPlusConfig {

    /**
     * 配置新版乐观锁插件,新版分页插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //乐观锁插件
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        //分页插件
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return mybatisPlusInterceptor;
    }
}

MyMetaObjectHandler.java

package cn.itxs.handle;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * 用于对数据库表中实现记录的创建时间和修改时间自动填充实现类
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    //插入记录时填充策略
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill.....");
        this.setFieldValByName("createTime",new Date(),metaObject);
        this.setFieldValByName("updateTime",new Date(),metaObject);
        this.setFieldValByName("modifyTime",new Date(),metaObject);
    }

    //更新记录时填充策略
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill.....");
        this.setFieldValByName("updateTime",new Date(),metaObject);
        this.setFieldValByName("modifyTime",new Date(),metaObject);
    }
}

UserInfo.java

package cn.itxs.pojo;

import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserInfo {

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String userName;

    private Short age;

    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

    @Version
    private Integer version;

    @TableLogic
    private Integer deleted;
}

UserInfoMapper.java

package cn.itxs.mapper;

import cn.itxs.pojo.UserInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface UserInfoMapper extends BaseMapper<UserInfo> {

}

加下来的service和serviceImpl文件是为了使用提供批量插入功能,上面UserInfoMapper已经提供很多常用增删改查的功能了。

UserInfoService.java

package cn.itxs.service;

import cn.itxs.pojo.UserInfo;
import com.baomidou.mybatisplus.extension.service.IService;

public interface UserInfoService extends IService<UserInfo> {

}

UserInfoServiceImpl.java

package cn.itxs.service.impl;

import cn.itxs.mapper.UserInfoMapper;
import cn.itxs.pojo.UserInfo;
import cn.itxs.service.UserInfoService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {

}

MainAppllication.java

package cn.itxs;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MainAppllication {

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

最后我们创建一个测试类,常见的添加、默认值填充、乐观锁修改、逻辑删除、批量添加、条件查询、条件修改、条件删除等功能

MyBatisPlusTest.java

package cn.itxs;

import cn.itxs.mapper.UserInfoMapper;
import cn.itxs.pojo.UserInfo;
import cn.itxs.service.UserInfoService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@SpringBootTest
@Slf4j
@SuppressWarnings("all")
public class MyBatisPlusTest {

    @Autowired
    UserInfoMapper userInfoMapper;

    @Autowired
    UserInfoService userInfoService;

    @Test
    void addUserInfo(){
        int result = userInfoMapper.insert(UserInfo.builder().userName("张三").age((short)22).version(1).deleted(0).build());
        log.info("result={}",result);
    }

    @Test
    void updateUserInfo(){
        UserInfo userInfo = new UserInfo();
        userInfo.setId(3);
        userInfo.setUserName("张三三");
        userInfo.setAge((short)26);
        int result = userInfoMapper.updateById(userInfo);
        log.info("result={}",result);
    }

    @Test
    void updateUserInfoByVersion(){
        UserInfo userInfo = userInfoMapper.selectById(3);
        userInfo.setUserName("张三丰");
        userInfo.setAge((short)23);
        int result = userInfoMapper.updateById(userInfo);
        log.info("result={}",result);
    }

    @Test
    void insertUserInfoBatch() {
        Random random =new Random();
        List<UserInfo> userInfos = new ArrayList<>(1000);
        for (int i=1;i<1000;i++){
            UserInfo userInfo = new UserInfo();
            userInfo.setUserName("李四"+i);
            userInfo.setAge((short)(random.nextInt(100)+1));
            userInfo.setVersion(1);
            userInfo.setDeleted(0);
            userInfos.add(userInfo);
        }
        boolean result = userInfoService.saveBatch(userInfos, 100);
        log.info("result={}",result);
    }

    @Test
    public void SelectAll() {
        List<UserInfo> userInfos = userInfoMapper.selectList(null);
        userInfos.forEach(System.out::println);
    }

    @Test
    void selectUserInfoByQuery() {
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().gt(UserInfo::getId,300);
        queryWrapper.lambda().eq(UserInfo::getAge,50);
        List<UserInfo> userInfos = userInfoMapper.selectList(queryWrapper);
        userInfos.forEach(System.out::println);
    }

    @Test
    void updateUserInfoByQuery() {
        UpdateWrapper<UserInfo> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().set(UserInfo::getAge,30)
                .gt(UserInfo::getId ,800);
        int result = userInfoMapper.update(null,updateWrapper);
        log.info("result={}",result);
    }

    @Test
    void updateUserInfoByQuerySql() {
        UpdateWrapper<UserInfo> updateWrapper = new UpdateWrapper<>();
        updateWrapper.set("user_name","age30username");
        updateWrapper.apply("age = 30");
        int result = userInfoMapper.update(null,updateWrapper);
        log.info("result={}",result);
    }

    @Test
    void deleteUserInfoByQuery() {
        UpdateWrapper<UserInfo> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda().gt(UserInfo::getId ,900);
        int result = userInfoMapper.delete(updateWrapper);
        log.info("result={}",result);
    }

}

示例运行如下

上一小节我们已经引入依赖mybatis-plus-generator和freemarker模板引擎,详细参数可以查阅官网

快速生成

package cn.itxs.generator;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.Collections;

public class CodeGererator {
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://192.168.50.95:3308/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8", "root", "123456")
                .globalConfig(builder -> {
                    builder.author("itxs") // 设置作者
                            .enableSwagger() // 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir("D://code_generator"); // 指定输出目录,一般设置我们工程项目下
                })
                .packageConfig(builder -> {
                    builder.parent("cn.itxs.generator") // 设置父包名
                            .moduleName("system") // 设置父包模块名
                            .entity("po")
                            .service("service")
                            .serviceImpl("service.impl")
                            .mapper("mapper")
                            .controller("controller")
                            .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://code_generator")); // 设置mapperXml生成路径,指定输出目录,一般设置我们工程项目下
                })
                .strategyConfig(builder -> {
                    builder.addInclude("user_info") // 设置需要生成的表名
                            .addTablePrefix("t_", "c_"); // 设置过滤表前缀
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
}

交互式生成

package cn.itxs.generator;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.fill.Column;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class InteractiveCodeGererator {

    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://192.168.50.95:3308/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8", "root", "123456")
                // 全局配置
                .globalConfig((scanner, builder) -> builder.author(scanner.apply("请输入作者名称?")).fileOverride())
                // 包配置
                .packageConfig((scanner, builder) -> builder.parent(scanner.apply("请输入包名?")))
                // 策略配置
                .strategyConfig((scanner, builder) -> builder.addInclude(getTables(scanner.apply("请输入表名,多个英文逗号分隔?所有输入 all")))
                        .controllerBuilder().enableRestStyle().enableHyphenStyle()
                        .entityBuilder().enableLombok().addTableFills(
                                new Column("create_time", FieldFill.INSERT)
                        ).build())
                /*
                    模板引擎配置,默认 Velocity 可选模板引擎 Beetl 或 Freemarker
                   .templateEngine(new BeetlTemplateEngine())
                   .templateEngine(new FreemarkerTemplateEngine())
                 */
                .execute();
    }

    // 处理 all 情况
    protected static List<String> getTables(String tables) {
        return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
    }
}

概述

MyBatis-Plus由苞米豆开发的,而苞米豆生态圈还有很多组件。我们本篇再简单了解MybatisX和Mybatis-Mate。

  • MybatisX (opens new window) - 一款全免费且强大的 IDEA 插件,支持跳转,自动补全生成 SQL,代码生成。
  • Mybatis-Mate (opens new window) - 为 MyBatis-Plus 企业级模块,支持分库分表、数据审计、字段加密、数据绑定、数据权限、表结构自动生成 SQL 维护等高级特性。
  • Dynamic-Datasource (opens new window) - 基于 SpringBoot 的多数据源组件,功能强悍,支持 Seata 分布式事务。
  • Shuan (opens new window) - 基于 Pac4J-JWT 的 WEB 安全组件, 快速集成。
  • Kisso (opens new window) - 基于 Cookie 的单点登录组件。
  • Lock4j (opens new window) - 基于 SpringBoot 同时支持 RedisTemplate、Redission、Zookeeper 的分布式锁组件。
  • Kaptcha (opens new window) - 基于 SpringBoot 和 Google Kaptcha 的简单验证码组件,简单验证码就选它。
  • Aizuda 爱组搭 (opens new window) - 低代码开发平台组件库。

MybatisX

MybatisX是一款全免费且强大的 IDEA 插件,支持跳转,自动补全生成 SQL,代码生成。在插件市场下载MybatisX,安装即可使用

快速体现跳转功能,可以从mapper接口中的接口方法直接跳转到对应xml文件的代码部分

代码快速补全功能

Mybatis-Mate

Mybatis-Mate为 MyBatis-Plus 企业级模块,支持分库分表、数据审计、字段加密、数据绑定、数据权限、表结构自动生成 SQL 维护等高级特性等。

https://gitee.com/baomidou/mybatis-mate-examples 这里提供企业级很多使用场景示例,我们下载源码工程后测试数据脱敏的例子,我将数据库修改为mysql,端口为8088

访问测试页面UserController的几个测试页面,针对用户表用户名、手机、邮箱的脱敏的结果如下,有兴趣可以多研究其他高级功能

**本人博客网站 **IT小神 www.itxiaoshen.com