跳至主要內容

MyBatis应用分析与最佳实践

soulballad分布式MybatisMybatis约 11675 字大约 39 分钟

一、mybatis概述

什么是 mybatis

MyBatis 是一款优秀的持久层框架,它支持 定制化 SQL存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。mybatis官方文档open in new window

为什么用mybatis

JDBC API

代码:

// 注册 JDBC 驱动
Class.forName ( "com.mysql.jdbc.Driver");
// 打开连接
conn = DriverManager. getConnection ( DB_URL , USER , PASSWORD );
// 执行查询
stmt = conn.createStatement();
String sql= "SELECT bid, name, author_id FROM blog";
ResultSet rs = stmt.executeQuery(sql);
// 获取结果集
while(rs.next()){
    int bid = rs.getInt( "bid");
    String name = rs.getString( "name");
    String authorId = rs.getString( "author_id");
}

步骤:

  • 首先,我们在 maven 中引入 MySQL 驱动的依赖(JDBC 的包在 java.sql 中);
  • 第一步,注册驱动;
  • 第二步,通过 DriverManager 获取一个 Connection,参数里面填数据库地址,用户名和密码;
  • 第三步,我们通过 Connection 创建一个 Statement 对象;
  • 第四步,通过 Statement 的 execute() 方法执行 SQL,获取结果集 ResultSet 对象;
  • 第五步,我们通过 ResultSet 获取数据。转换成一个 POJO 对象。
  • 最后,我们要关闭数据库相关的资源,包括 ResultSet、Statement、Connection,它们的关闭顺序和打开的顺序正好是相反的。

缺点:

  • 代码重复,每次操作数据库都得写上述代码;
  • 结果集处理麻烦,每个对象都得单独处理结果集;
  • sql硬编码,不利于扩展和维护;
  • 连接管理,需要手动开启关闭,如果没有关闭会导致数据库服务资源耗尽;
  • 耦合度高,业务逻辑和操作数据库的逻辑耦合在一起,需要修改的话很麻烦。

Apache DbUtils

地址:

[DbUtils](https://commons.apache.org/proper/commons-dbutils/)

功能:

  • 对 JDBC API 进行了封装;
  • 处理结果集的映射,可以把 ResultSet 封装成 JavaBean,这个是 DbUtils 解决的核心问题;
  • 实现了对数据库连接的管理;

使用:

  • 首先 DbUtils 提供了一个 QueryRunner 类,它对数据库的增删改查的方法进行了封装,那么我们操作数据库就可以直接使用它提供的方法。

    HikariConfig config = new HikariConfig(PROPERTY_PATH);
    dataSource = new HikariDataSource(config);
    queryRunner = new QueryRunner(dataSource);
    
  • 在 QueryRunner 的构造函数里面,我们又可以传入一个数据源,比如在这里我们 Hikari,这样我们就不需要再去写各种创建和释放连接的代码了。

    queryRunner = new QueryRunner(dataSource);
    
  • 那我们怎么把结果集转换成对象呢?比如实体类 Bean 或者 List 或者 Map?在 DbUtils 里面提供了一系列的支持泛型的 ResultSetHandler。

    image-20200410205102054

  • 只要在 DAO 层调用 QueryRunner 的查询方法,传入这个 Handler,它就可以自动把结果集转换成实体类 Bean 或者 List 或者 Map。

String sql = "select * from blog where bid = ? ";
Object[] params = new Object[]{bid};
BlogDto blogDto = queryRunner.query(sql, new BeanHandler<>(BlogDto.class), arams);

缺点:

数据库的字段个类中属性`完全一致`时,才能实现映射。

Spring JDBC

说明:

spring jdbc 是 spring 中一个模块,它对 jdbc 进行了封装,并且提供了一个模板方法 JdbcTemplate 来简化对数据库的操作。

功能:

  • 解决了资源管理问题
  • 提供 RowMapper 接口来实现 ResultSet 结果集的映射

使用:

  • 创建一个 RowMapper 对象,实现 RowMapper 接口,并且重写 mapRow()方法

    public class EmployeeRowMapper implements RowMapper<Employee> {
        @Override
        public Employee mapRow(ResultSet resultSet, int i) throws SQLException {
            Employee employee = new Employee();
            employee.setEmpId(resultSet.getInt( "emp_id"));
            employee.setEmpName(resultSet.getString( "emp_name"));
            employee.setEmail(resultSet.getString( "emial"));
            return employee;
        }
    }
    
  • 在 DAO 层调用的时候就可以传入自定义的 RowMapper 类,最终返回我们需要的类型。结果集和实体类类型的映射也是自动完成的。

    public List<Employee> query(String sql){
        new JdbcTemplate(new DruidDataSource());
        return  jdbcTemplate.query(sql, new EmployeeRowMapper());
    }
    
  • 通过这种方式,我们对于结果集的处理只需要写一次代码,然后在每一个需要映射的地方传入这个 RowMapper 就可以了,减少了很多的重复代码。

缺点:

每一个实体类对象,都需要定义一个 Mapper,然后要编写每个字段映射的 getString()、getInt() 这样的代码,还增加了类的数量。

优化:

创建一个 BaseRowMapper<T>,通过反射的方式自动获取所有属性,把表字段全部赋值到属性。这样在使用的时候只需要传入类型就可以完成自动映射,不用定义 RowMapper。

DbUtils 对比 Spring JDBC

优点:

  1. 无论是 QueryRunner 还是 JdbcTemplate,都可以传入一个数据源进行初始化,也就是资源管理这一部分的事情,可以交给专门的数据源组件去做,不用手动创建和关闭;
  2. 对操作数据的增删改查的方法进行了封装;
  3. 映射结果集,无论是映射成 List、Map 还是实体类。

缺点:

  1. SQL 语句都是写死在代码里面的,依旧存在硬编码的问题;
  2. 参数只能按固定位置的顺序传入(数组),它是通过占位符去替换的,不能自动映射;
  3. 在方法里面,可以把结果集映射成实体类,但是不能直接把实体类映射成数据库的记录(没有自动生成 SQL 的功能);
  4. 查询没有缓存的功能

方案:

要解决上述问题,使用这些工具类还是不够的,要用到 ORM 框架

  1. 说明:

    ORM 的全拼是 Object Relational Mapping,也就是对象与关系的映射,对象是程序里面的对象,关系是它与数据库里面的数据的关系。也就是说,ORM 框架帮助我们解决的问题是程序对象和关系型数据库的相互映射的问题。

  2. 图示:

    1557492917208

Hibernate

使用:

  1. 配置:

    <hibernate-mapping>
        <class name="cn.gupaoedu.vo.User" table="user">
            <id name="id">
            	<generator class="native"/>
            </id>
            <property name="password"/>
            <property name="cellphone"/>
            <property name="username"/>
        </class>
    </hibernate-mapping>
    
  2. 代码:通过 Hibernate 提供(session)的增删改查的方法来操作对象

    //创建对象
    User user = new User();
    user.setPassword("123456");
    user.setCellphone("18166669999");
    user.setUsername("qingshan");
    //获取加载配置管理类
    Configuration configuration = new Configuration();
    //不给参数就默认加载 hibernate.cfg.xml 文件,
    configuration.configure();
    //创建 Session 工厂对象
    SessionFactory factory = configuration.buildSessionFactory();
    //得到 Session 对象
    Session session = factory.openSession();
    //使用 Hibernate 操作数据库,都要开启事务,得到事务对象
    Transaction transaction = session.getTransaction();
    //开启事务
    transaction.begin();
    //把对象添加到数据库中
    session.save(user);
    //提交事务
    transaction.commit();
    //关闭 Session
    session.close();
    
  3. Hibernate的框架会自动生成SQL 语句(可以屏蔽数据库的差异),自动进行映射。使代码变得简洁,提高程序的可读性。

缺点:

  1. 使用 get()、save() 、update()对象的这种方式,实际操作的是所有字段,没有办法指定部分字段,不够灵活。
  2. 这种自动生成 SQL 的方式,如果要做一些优化话,非常困难,可能会出现性问题。
  3. 不支持动态 SQL(比如分表中的表名变化,以及条件、参数)。

Mybatis

说明:

1. MyBatis 解决了上述问题。是一种“半自动化”框架,它的封装程度没有 Hibernate 高,不会自动生成全部的 SQL 语句,主要解决的是 SQL 和对象的映射问题。

2. 在 MyBatis 里面,SQL 和代码是分离的,所以会写 SQL 基本上就会用 MyBatis,学习成本低。
Mybatis核心特性:
  1. 使用连接池对连接进行管理
  2. SQL 和代码分离,集中管理
  3. 结果集映射
  4. 参数映射和动态 SQL
  5. 重复 SQL 的提取
  6. 缓存管理
  7. 插件机制
如何选择工具或框架?
  • 在一些业务比较简单的项目中,我们可以使用 Hibernate;
  • 如果需要更加灵活的 SQL,可以使用 MyBatis;
  • 对于底层的编码,或者性能要求非常高的场合,可以用 JDBC。
  • 在实际项目中,MyBatis 和 Spring JDBC 是可以混合使用的;
  • 当然,也可以根据项目的需求自己写 ORM 框架。

二、mybatis使用

编程式使用

大部分时候,MyBatis 都是集成在 Spring 里面 。因为 Spring 对 MyBatis 的一些操作进行的封装,我们不能直接看到它的本质,所以先看下不使用容器的时候,也就是编程的方式,MyBatis 怎么使用。

  • 先引入 mybatis jar 包。
  • 首先我们要创建一个全局配置文件,这里面是对 MyBatis 的核心行为的控制,比如 mybatis-config.xml。
  • 第二个就是我们的映射器文件,Mapper.xml,通常来说一张表对应一个,我们会在这个里面配置我们增删改查的 SQL 语句,以及参数和返回的结果集的映射关系。

跟 JDBC 的代码一样,我们要执行对数据库的操作,必须创建一个会话,这个在MyBatis 里面就是 SqlSession。SqlSession 又是工厂类根据全局配置文件创建的。所以整个的流程就是这样的(如下代码)。最后我们通过 SqlSession 接口上的方法,传入我们的 Statement ID 来执行 SQL。这是第一种方式。这种方式有一个明显的缺点,就是会对 Statement ID 硬编码,而且不能在编译时进行类型检查,所以通常我们会使用第二种方式,就是定义一个 Mapper 接口的方式。这个接口全路径必须跟 Mapper.xml 里面的 namespace 对应起来,方法也要跟 StatementId 一一对应。

public void testMapper() throws IOException {
    String resource =  "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory =  new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession session = sqlSessionFactory.openSession();
    try {
        BlogMapper mapper = session.getMapper(BlogMapper.class);
        Blog blog = mapper.selectBlogById(1);
        System.out.println(blog);
    } finally {
        session.close();
    }
}

核心对象的生命周期

在编程式使用的这个 demo 里面,我们看到了 MyBatis 里面的几个核心对象:
SqlSessionFactoryBuilerSqlSessionFactorySqlSessionMapper 对象。这几个核心对象在 MyBatis 的整个工作流程里面的不同环节发挥作用。如果说我们不用容器,

自己去管理这些对象的话,我们必须思考一个问题:什么时候创建和销毁这些对象?
在一些分布式的应用里面,多线程高并发的场景中,如果要写出高效的代码,必须了解这四个对象的生命周期。这四个对象的声明周期的描述在官网open in new window上面也可以找到。

我们从每个对象的作用的角度来理解一下,只有理解了它们是干什么的,才知道什么时候应该创建,什么时候应该销毁。

SqlSessionFactoryBuiler

首 先 是 SqlSessionFactoryBuiler 。 它是用来构建 SqlSessionFactory 的 , 而 SqlSessionFactory 只需要一个,所以只要构建了这一个 SqlSessionFactory,它的使命就完成了,也就没有存在的意义了。所以它的生命周期只存在于<font color="red">方法的局部</font>

SqlSessionFactory

SqlSessionFactory 是用来创建 SqlSession 的,每次应用程序访问数据库,都需要创建一个会话。因为我们一直有创建会话的需要,所以 SqlSessionFactory 应该存在于应用的整个生命周期中(<font color="red">作用域是应用作用域</font>)。创建 SqlSession 只需要一个实例来做这件事就行了,否则会产生很多的混乱,和浪费资源。所以我们要采用单例模式。

SqlSession

SqlSession 是一个会话,因为它不是线程安全的,不能在线程间共享。所以我们在请求开始的时候创建一个 SqlSession 对象,在请求结束或者说方法执行完毕的时候要及时关闭它(<font color="red">一次请求或者操作中</font>)。

Mapper

Mapper(实际上是一个代理对象)是从 SqlSession 中获取的。

BlogMapper mapper = session.getMapper(BlogMapper.class);

它的作用是发送 SQL 来操作数据库的数据。它应该在一个 SqlSession 事务方法之内。

总结

对象生命周期
SqlSessionFactoryBuiler方法局部(method)
SqlSessionFactory(单例)应用级别(application)
SqlSession请求和操作(request/method)
Mapper方法(method)

核心配置

第一个是 config 文件。大部分时候我们只需要很少的配置就可以让 MyBatis 运行起来。其实 MyBatis 里面提供的配置项非常多,我们没有配置的时候使用的是系统的默认值。
mybatis-3 的源码托管在 githubopen in new window 上 。

1557495864806

第一个是 jar 包和文档。第二个第三个是源码。
在这个压缩包里面,解压出来有一个 mybatis-3.5.1.pdf,是英文版本的。如果阅读英文困难,可以基于 3.5.1 的中文版本open in new window学习。

一级 标签

configuration

configuration 是整个配置文件的根标签,实际上也对应着 MyBatis 里面最重要的配置类 Configuration。它贯穿 MyBatis 执行流程的每一个环节。我们打开这个类看一下,这里面有很多的属性,跟其他的子标签也能对应上。
注意:MyBatis 全局配置文件顺序是固定的,否则启动的时候会报错。(一级标签要求全部掌握)

properties

第一个是 properties 标签,用来配置参数信息,比如最常见的数据库连接信息。为了避免直接把参数写死在 xml 配置文件中,我们可以把这些参数单独放在 properties 文件中,用 properties 标签引入进来,然后在 xml 配置文件中用${}引用就可以了。

可以用 resource 引用应用里面的相对路径,也可以用 url 指定本地服务器或者网络的绝对路径。
我们为什么要把这些配置独立出来?有什么好处?或者说,公司的项目在打包的时候,有没有把 properties 文件打包进去?

  1. 提取,利于多处引用,维护简单;
  2. 把配置文件放在外部,避免修改后重新编译打包,只需要重启应用;
  3. 程序和配置分离,提升数据的安全性,比如生产环境的密码只有运维人员掌握。
settings

setttings 里面是 MyBatis 的一些核心配置,settings常见属性及默认配置open in new window

typeAliases

TypeAlias 是类型的别名,跟 Linux 系统里面的 alias 一样,主要用来简化全路径类名的拼写。比如我们的<font color="red">参数类型</font><font color="red">返回值类型</font>都可能会用到我们的 Bean,如果每个地方都配置全路径的话,那么内容就比较多,还可能会写错。

我们可以为自己的 Bean 创建别名,既可以指定单个类,也可以指定一个 package,自动转换。配置了别名以后,只需要写别名就可以了,比如 com.gupaoedu.domain.Blog 都只要写 blog 就可以了。
MyBatis 里面有系统预先定义好的类型别名,在 TypeAliasRegistry 中。

typeHandlers 【重点】

由于 Java 类型和数据库的 JDBC 类型不是一一对应的(比如 String 与 varchar),所以我们把 Java 对象转换为数据库的值,和把数据库的值转换成 Java 对象,需要经过一定的转换,这两个方向的转换就要用到 TypeHandler。
有的同学可能会有疑问,我没有做任何的配置,为什么实体类对象里面的一个 String 属性,可以保存成数据库里面的 varchar 字段,或者保存成 char 字段?

这是因为 MyBatis 已经内置了很多 TypeHandler(在 type 包下),它们全部全部注册在 TypeHandlerRegistry 中,他们都继承了抽象类 BaseTypeHandler,泛型就是要处理的 Java 数据类型。

image-20200410211527673

当我们做数据类型转换的时候,就会自动调用对应的 TypeHandler 的方法。
如果我们需要自定义一些类型转换规则,或者要在处理类型的时候做一些特殊的动作,就可以编写自己的 TypeHandler,跟系统自定义的 TypeHandler 一样,继承抽象类BaseTypeHandler<T>。有 4 个抽象方法必须实现,我们把它分成两类:

set 方法从 Java 类型转换成 JDBC 类型的,get 方法是从 JDBC 类型转换成 Java 类型的。

从 Java 类型到 JDBC 类型从 JDBC 类型到 Java 类型
setNonNullParameter:设置非空参数getNullableResult:获取空结果集(根据列名),一般都是调用这个getNullableResult:获取空结果集(根据下标值)getNullableResult:存储过程用的

比如我们想要在获取或者设置 String 类型的时候做一些特殊处理,我们可以写一个 String 类型的 TypeHandler(mybatis-standalone 工程)。

第一步:继承 BaseTypeHandler<T>

public  class MyTypeHandler  extends BaseTypeHandler<String> {
    public void setNonNullParameter(PreparedStatement ps,  int i, String parameter, JdbcType jdbcType) throws SQLException {
        // 设置 String 类型的参数的时候调用,Java 类型到 JDBC 类型
        System. out .println( "------------setNonNullParameter1 :" +parameter);
        ps.setString(i, parameter);
    }
    public String getNullableResult(ResultSet rs, String columnName) s throws SQLException {
        // 根据列名获取 String 类型的参数的时候调用,JDBC 类型到 java 类型
        System. out .println( "---------------getNullableResult1 :" +columnName);
        return rs.getString(columnName);
    }
    // 后面两个方法省略…………
}

第二步,在 mybatis-config.xml 文件中注册:

<typeHandlers>
    <typeHandler handler ="com.gupaoedu.type.MyTypeHandler"></typeHandler>
</typeHandlers>

第三步,在我们需要使用的字段上指定,比如:
插入值的时候,从 Java 类型到 JDBC 类型,在字段属性中指定 typehandler:

<insert  id ="insertBlog"  parameterType ="com.gupaoedu.domain.Blog">
    insert into blog (bid, name, author_id)
    values (#{bid,jdbcType=INTEGER},
    #{name,jdbcType=VARCHAR,typeHandler=com.gupaoedu.type.MyTypeHandler},
    #{authorId,jdbcType=INTEGER})
</insert>

返回值的时候,从 JDBC 类型到 Java 类型,在 resultMap 的列上指定 typehandler:

<result column ="name" property ="name" jdbcType ="VARCHAR" typeHandler ="com.gupaoedu.type.MyTypeHandler"/>

【思考,不强制要求完成】
如果我们的对象里面有复杂对象,比如 Blog 里面包括了一个 Comment 对象,这个时候 Comment 对象的全部属性不能直接映射到数据库的一个字段。

要求:创建一个 TypeHandler,可以将任意的对象转换为 json 字符串,保存到数据库的 VARCHAR 类型中。在从数据库查询的时候,再转换为原来的 Java 对象。

  1. 在数据库表添加一个 VARCHAR 字段;
  2. 在 Blog 对象中添加一个 Comment 属性,字段 Integer id;String content;
  3. JSON 工具没有要求,jackson 或者 fastjson、gson 都可以。
  4. 在查询和插入的 statement 上使用这个 TypeHandler。
objectFactory 【重点】

当我们把数据库返回的结果集转换为实体类的时候,需要创建对象的实例,由于我们不知道需要处理的类型是什么,有哪些属性,所以不能用 new 的方式去创建。在MyBatis 里面,它提供了一个工厂类的接口,叫做 ObjectFactory,专门用来创建对象实例,里面定义了 4 个方法。

image-20200410212020523

方法作用
void setProperties(Properties properties);设置参数时调用
<T> T create(Class<T> type);创建对象(调用无参构造函数)
<T> T create(Class<T> type, List<Class<?>>``constructorArgTypes, List<Object> constructorArgs);创建对象(调用带参数构造函数)
<T> n boolean isCollection(Class<T> type);判断是否集合

ObjectFactory 有一个默认的实现类 DefaultObjectFactory,创建对象的方法最终都调用了 instantiateClass(),是通过反射来实现的。

自定义 ObjectFactory

继承 DefaultObjectFactory 来实现(不需要再实现 ObjectFactory 接口)。

public class MyObjectFactory extends DefaultObjectFactory {
    @Override
    public Object create(Class type) {
        System.out.println("创建对象方法:" + type);
        if (type.equals(Blog.class)) {
            Blog blog = (Blog) super.create(type);
            blog.setName("object factory");
            blog.setBid(1111);
            blog.setAuthorId(2222);
            return blog;
        }
        Object result = super.create(type);
        return result;
    }
}

我们可以直接用自定义的工厂类来创建对象:

public class ObjectFactoryTest {
    public static void main(String[] args) {
        MyObjectFactory factory = new MyObjectFactory();
        Blog myBlog = (Blog) factory.create(Blog.class);
        System.out.println(myBlog);
    }
}

这样我们就直接拿到了一个对象。
如果在 config 文件里面注册,在创建对象的时候会被自动调用:

<objectFactory  type ="org.mybatis.example.MyObjectFactory">
    <!-- 对象工厂注入的参数 -->
    <property name="gupao" value ="666"/>
</objectFactory>

这样,就可以让 MyBatis 的创建实体类的时候使用我们自己的对象工厂。

应用场景举例
比如有一个新锐手机品牌在一个电商平台上面卖货,为了让预约数量好看一点,只要有人预约,预约数量就自动乘以 3。这个时候就可以创建一个 ObjectFactory,只要是查询销量,就把它的预约数乘以 3 返回这个实体类。

  1. 什么时候调用了 objectFactory.create()?
    创建 DefaultResultSetHandler 的时候,和创建对象的时候。
  2. 创建对象后,已有的属性为什么被覆盖了?
    在 DefaultResultSetHandler 类的 395 行 getRowValue() 方法里面里面调用了applyPropertyMappings()。
  3. 返回结果的时候,ObjectFactory 和 TypeHandler 哪个先工作?
    先是 ObjectFactory,再是 TypeHandler。肯定是先创建对象。
    PS:step out 可以看到一步步调用的层级。
plugins

插件是 MyBatis 的一个很强大的机制,跟很多其他的框架一样,MyBatis 预留了插件的接口,让 MyBatis 更容易扩展。
根据官方的定义,插件可以拦截这四个对象的这些方法,我们把这四个对象称作MyBatis 的四大对象。我们会在带大家阅读源码,知道了这 4 大对象的作用之后,再来分析自定义插件的开发和插件open in new window运行的原理。

类 ( 或 接口)方法
Executorupdate, query, flushStatements, commit, rollback,getTransaction, close, isClosed
ParameterHandlergetParameterObject, setParameters
ResultSetHandlerhandleResultSets, handleOutputParameters
StatementHandlerprepare, parameterize, batch, update, query
environment(s)

environments 标签用来管理数据库的环境,比如我们可以有开发环境、测试环境、生产环境的数据库。可以在不同的环境中使用不同的数据库地址或者类型。

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>

一个 environment 标签就是一个数据源,代表一个数据库。这里面有两个关键的标签,一个是事务管理器,一个是数据源。

####### transactionManager

如果配置的是 JDBC,则会使用 Connection 对象的 commit()、rollback()、close()管理事务。

如果配置成 MANAGED,会把事务交给容器来管理,比如 JBOSS,Weblogic。因为我们跑的是本地程序,如果配置成 MANAGE 不会有任何事务。
如 果 是 Spring + MyBatis ,则没有必要配置,因为我们会直接在applicationContext.xml 里面配置数据源,覆盖 MyBatis 的配置。

####### dataSource

将在下一节(settings)详细分析。在跟 Spring 集成的时候,事务和数据源都会交给 Spring 来管理。

mappers

<mappers>标签配置的是我们的映射器,也就是 Mapper.xml 的路径。这里配置的目的是让 MyBatis 在启动的时候去扫描这些映射器,创建映射关系。

我们有四种指定 Mapper 文件的方式open in new window

  • 使用<font color="red">相对</font><font color="red">类路径</font>的资源引用(resource)
  • 使用完全限定资源定位符(<font color="red">绝对路径</font>)(URL)
  • 使用映射器接口实现类的完全限定类名
  • 将包内的映射器接口实现全部注册为映射器(<font color="red">最常用</font>

思考:

接口跟 statement 是怎么绑定起来的?——method 有方法全限定名,比如:	com.gupaoedu.mapper.BlogMapper.selectBlogById, 跟 namespace 里面的 statement ID 是相同的。

在哪一步拿到 SQL 的?——ms 里面有 SQL。

// DefaultSqlSession.selectList()
MappedStatement ms =  configuration.getMappedStatement(statement);
settings
属性名含义简介有效值默认值
cacheEnabled是否使用缓存是整个工程中所有映射器配置缓存的开关,即是一个全局缓存开关true/falseTRUE
lazyLoadingEnabled是否开启延迟加载控制全局是否使用延迟加载(association、collection)。当有特殊关联关系需要单独配置时,可以使用 fetchType属性来覆盖此配置true/falseFALSE
aggressiveLazyLoading是否需要侵入式延迟加载开启时,无论调用什么方法加载某个对象,都会加载该对象的所有属性,关闭之后只会按需加载true/falseFALSE
defaultExecutorType设置默认的执行器有三种执行器:SIMPLE为普通执行器;REUSE执行器会重用与处理语句;BATCH执行器将重用语句并执行批量 更新SIMPLE/REUSE/BATCHSIMPLE
lazyLoadTriggerMethods指定哪个对象的方法触发一次延迟加载配置需要触发延迟加载的方法的名字,该方法就会触发一次延迟加载一个逗号分隔的方法名称列表equals,clone,hashCode,toString
localCacheScopeMyBatis利用本地缓存机制(LocalCache)防止循环引用(circularreferences)和加速重复嵌套查询默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession的不同调用将不会共享数据SESSION/STATEMENTSESSION
logImpl日志实现指定 MyBatis所用日志的具体实现,未指定时将自动查找SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING
multipleResultSetsEnabled是否允许单一语句返回多结果集即 Mapper配置中一个单一的 SQL配置是否能返回多个结果集true/falseTRUE
useColumnLabel使用列标签代替列名设置是否使用列标签代替列名true/falseTRUE
useGeneratedKeys是否支持JDBC自动生成主键设置之后,将会强制使用自动生成主键的策略true/falseFALSE
autoMappingBehavior指定 MyBatis自动映射字段或属性的方式有三种方式,NONE 时将取消自动映射;PARTIAL 时只会自动映射没有定义结 果集的结果映射;FULL时会映射任意 复杂的结果集NONE/PARTIAL/FULLPARTIAL
autoMappingUnknown``ColumnBehavior设置当自动映射时发现未知列的动作有三种动作:NONE 时不做任何操作;WARNING 时会输出提醒日志;FAILING 时会抛出SqlSessionException 异常表示映射失败NONE/WARNING/FAILINGNONE
defaultStatementTimeout设置超时时间该超时时间即数据库驱动连接数据库时,等待数据库回应的最大秒数任意正整数
defaultFetchSize设置驱动的结果集获取数量(fetchSize)的提示值为了防止从数据库查询出来的结果过多,而导致内存溢出,可以通过设置 fetchSize参数来控制结果集的数量任意正整数
safeRowBoundsEnabled允许在嵌套语句中使用分页(RowBound,即行内嵌套语句)如果允许在 SQL的行内嵌套语句中使 用分页,就设置该值为falsetrue/falseFALSE
safeResultHandlerEnabled允许在嵌套语句中使用分页(ResultHandler,即结果集处理)如果允许在 SQL的结果集使用分页,就设置该值为falsetrue/falseTRUE
mapUnderscore``ToCamelCase是否开启驼峰命名规则(camelcase)映射表明数据库中的字段名称与工程中 Java 实体类的映射是否采用驼峰命名规则校验true/falseFALSE
jdbcTypeForNullJDBC类型的默认设置当没有为参数提供特定的JDBC类型时,为空值指定 JDBC 类型。某些驱动 需要指定列的JDBC类型,多数情况直接用一般类型即可,比如 NULL、ARCHAR或OTHER常用NULL、VARCHAR、OTHEROTHER
defaultScriptingLanguage动态SQL默认语言指定动态SQL生成的默认语言一个类型别名或者 一个类的全路径名org.apach e.ibatis. scripting .xmltags. XMLLangua geDriver
callSettersOnNulls是否在控制情况下调用Set方法指定当结果集中值为 null时是否调用映射对象的sette(rmap对象时为put)方法,这对于有 Map.keySet()依赖或null值初始化时是有用的。注意基本类型是不能设置成null的true/falseFALSE
returnInstance``ForEmptyRow返回空实体集对象当返回行的所有列都是空时,MyBatis 默认返回null。当开启这个设置时, MyBatis 会返回一个空实例。请注意,它也适用于嵌套的结果集(从 MyBatis3.4.2版本开始)true/falseFALSE
logPrefix日志前缀指定 MyBatis所用日志的具体实现,未指定时将自动查找任意字符串
vfsImplvfs实现指定 vfs的实现自定义 VFS的实现的类的全限定名,以逗号分隔
useActualParamName使用方法签名允许使用方法签名中的名称作为语句 参数名称。要使用该特性,工程必须采用 Java8编译,并且加上-parameters 选项(从 MyBatis3.4.1版本开始)true/falseTRUE
configurationFactory配置工厂指定提供配置示例的类。返回的配置实 例用于加载反序列化的懒加载参数。这个类必须有一个签名的静态配置 getconfiguration()方法(从 MyBatis3.2.3版本开始)一个类型别名或者 一个类型的全路径名
Mapper.xml 映射配置文件 【重点】

映射器里面最主要的是配置了 SQL 语句,也解决了我们的参数映射和结果集映射的问题。一共有 8 个标签open in new window

标签作用
cache给定命名空间的缓存配置(是否开启二级缓存)
cache-ref其他命名空间缓存配置的引用
resultMap是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象
sql可被其他语句引用的可重用语句块
insert映射插入语句
update映射更新语句
delete映射删除语句
select映射查询语句
总结
配置名称配置含义配置简介
configuration包裹所有配置标签整个配置文件的顶级标签
properties属性该标签可以引入外部配置的属性,也可以自己配置。该配置标签所在的同一个配置文件的其他配置均可以引用此配置中的属性
setting全局配置参数用来配置一些改变运行时行为的信息,例如是否使用缓存机制,是否使用延迟加载,是否使用错误处理机制等。
typeAliases类型别名用来设置一些别名来代替 Java 的长类型声明(如 java.lang.int变为 int),减少配置编码的冗余
typeHandlers类型处理器将数据库获取的值以合适的方式转换为 Java 类型,或者将 Java类型的参数转换为数据库对应的类型
objectFactory对象工厂实例化目标类的工厂类配置
plugins插件可以通过插件修改 MyBatis 的核心行为,例如对语句执行的某一点进行拦截调用
environments环境集合属性对象数据库环境信息的集合。在一个配置文件中,可以有多种数据库环境集合,这样可以使 MyBatis 将 SQL 同时映射至多个数据库
environment环境子属性对象数据库环境配置的详细配置
transactionManager事务管理指定 MyBatis 的事务管理器
dataSource数据源使用其中的 type 指定数据源的连接类型,在标签对中可以使用property 属性指定数据库连接池的其他信息
mappers映射器配置 SQL 映射文件的位置,告知 MyBatis 去哪里加载 SQL 映射文件

三、mybatis最佳实践

动态 SQL

为什么需要 动态 SQL ?

由于前台传入的查询参数不同,所以写了很多的 if else,还需要非常注意 SQL 语句里面的 and、空格、逗号和转移的单引号这些,拼接和调试 SQL 就是一件非常耗时的工作。

MyBaits 的动态 SQL 就帮助我们解决了这个问题,它是基于 OGNL 表达式的。

动态标签有哪些 ?

按照官网的分类,MyBatis 的动态标签主要有四类:

  • if
  • choose (when, otherwise),
  • trim (where, set)
  • foreach

if —— 需要判断的时候,条件写在 test 中,以下语句可以用<where>改写

<select  id ="selectDept"  parameterType ="int" resultType ="com.gupaoedu.crud.bean.Department">
    select * from tbl_dept where 1=1
    <if  test="deptId != null">
        and dept_id = #{deptId,jdbcType=INTEGER}
    </if>
</select>

choose (when, otherwise) —— 需要选择一个条件的时候

<select  id="getEmpList_choose"  resultMap ="empResultMap" parameterType ="com.gupaoedu.crud.bean.Employee">
    SELECT * FROM tbl_emp e
    <where>
        <choose>
            <when test ="empId !=null">
                e.emp_id = #{emp_id, jdbcType=INTEGER}
            </when>
            <when  test ="empName != null and empName != ''">
                AND e.emp_name LIKE CONCAT(CONCAT('%', #{emp_name,
                jdbcType=VARCHAR}),'%')
            </when>
            <when testl ="email = != null ">
                AND e.email = #{email, jdbcType=VARCHAR}
            </when>
            < otherwise>
            </otherwise>
        </choose>
    </where>
</select>

trim (where, set) ——需要去掉 where、and、逗号之类的符号的时候。
注意最后一个条件 dId 多了一个逗号,就是用 trim 去掉的:

<update id ="updateByPrimaryKeySelective" parameterType ="com.gupaoedu.crud.bean.Employee">
    update tbl_emp
    <set>
        <if test="empName != null">
            emp_name = #{empName,jdbcType=VARCHAR},
        </if>
        <if test="gender != null">
            gender = #{gender,jdbcType=CHAR},
        </if>
        <if test="email != null">
            email = #{email,jdbcType=VARCHAR},
        </if>
        <if test="dId != null">
            d_id = #{dId,jdbcType=INTEGER},
        </if>
    </set>
    where emp_id = #{empId,jdbcType=INTEGER}
</update>

trim 用来指定或者去掉前缀或者后缀:

<insert id="insertSelective" parameterType="com.gupaoedu.crud.bean.Employee">
    insert into tbl_emp
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="empId != null">
            emp_id,
        </if>
        <if test="empName != null">
            emp_name,
        </if>
        <if test="dId != null">
            d_id,
        </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
        <if test="empId != null">
            #{empId,jdbcType=INTEGER},
        </if>
        <if test="empName != null">
            #{empName,jdbcType=VARCHAR},
        </if>
        <if test="dId != null">
            #{dId,jdbcType=INTEGER},
        </if>
    </trim>
</insert>

foreach —— 需要遍历集合的时候:

<delete id="deleteByList" parameterType="java.util.List">
    delete from tbl_emp where emp_id in
    <foreach collection="list" item="item" open="(" separator="," close=")">
        #{item.empId,jdbcType=VARCHAR}
    </foreach>
</delete>

动态 SQL 主要是用来解决 SQL 语句生成的问题。

批量操作

(spring-mybatis 工程单元测试目录,MapperTest 类)
我们在生产的项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况(批量新增商户、批量修改商户信息),当数据量非常大,比如超过几万条的时候,在Java 代码中循环发送 SQL 到数据库执行肯定是不现实的,因为这个意味着要跟数据库创建几万次会话,即使我们使用了数据库连接池技术,对于数据库服务器来说也是不堪重负的。

在 MyBatis 里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直接传入一个 List、Set、Map 或者数组,配合动态 SQL 的标签,MyBatis 会自动帮我们生成语法正确的 SQL 语句。

批量插入
批量插入的语法是这样的,只要在 values 后面增加插入的值就可以了。

insert into tbl_emp (emp_id, emp_name, gender,email, d_id) 
values ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) ,( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? )

在 Mapper 文件里面,我们使用 foreach 标签拼接 values 部分的语句:

<!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List" useGeneratedKeys="true">
    <selectKey resultType="long" keyProperty="id" order="AFTER">
        SELECT LAST_INSERT_ID()
    </selectKey>
    insert into tbl_emp (emp_id, emp_name, gender,email, d_id)
    values
    <foreach collection="list" item="emps" index="index" separator=",">
        ( #{emps.empId},#{emps.empName},#{emps.gender},#{emps.email},#{emps.dId} )
    </foreach>
</insert>

Java 代码里面,直接传入一个 List 类型的参数。
效率要比循环发送 SQL 执行要高得多。减少了跟数据库交互的次数,并且避免了开启和结束事务的时间消耗。

批量更新
批量更新的语法是这样的,通过 case when,来匹配 id 相关的字段值。

update tbl_emp set
	emp_name =
    	case emp_id
            when ? then ?
            when ? then ?
            when ? then ? end ,
    gender =
        case emp_id
            when ? then ?
            when ? then ?
            when ? then ? end ,
    email =
        case emp_id
            when ? then ?
            when ? then ?
            when ? then ? end
where emp_id in ( ? , ? , ? )

所以在 Mapper 文件里面最关键的就是 case when 和 where 的配置。

需要注意一下 open 属性和 separator 属性。

<update id="updateBatch">
    update tbl_emp set
    emp_name =
    <foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
        when #{emps.empId} then #{emps.empName}
    </foreach>
    ,gender =
    <foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
        when #{emps.empId} then #{emps.gender}
    </foreach>
    ,email =
    <foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
        when #{emps.empId} then #{emps.email}
    </foreach>
    where emp_id in
    <foreach collection="list" item="emps" index="index" separator="," open="("
             close=")">
        #{emps.empId}
    </foreach>
</update>

批量删除也是类似的。

Batch Executor

当然 MyBatis 的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的时候,拼接出来的 SQL 语句过大。
MySQL 的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是4M,需要修改默认配置才可以解决这个问题。
<font color="red">Caused by: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (7188967 >
4194304). You can change this value on the server by setting the max_allowed_packet' variable.</font>

在我们的全局配置文件中,可以配置默认的 Executor 的类型。其中有一种BatchExecutor。

<setting  name="defaultExecutorType" value="BATCH" />

也可以在创建会话的时候指定执行器类型:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH );

BatchExecutor 底层是对 JDBC ps.addBatch()的封装,原理是攒一批 SQL 以后再发送
问题:三种执行器的区别是什么?Simple、Reuse、Batch

嵌套(关联)查询/ N+1/ 延迟加载

我们在查询业务数据的时候经常会遇到跨表关联查询的情况,比如查询员工就会关联部门(一对一),查询成绩就会关联课程(一对一),查询订单就会关联商品(一对多),等等。

1557501420981

我们映射结果有两个标签,一个是 resultType,一个是 resultMap。resultType 是 select 标签的一个属性,适用于返回 JDK 类型(比如 Integer、String等等)和实体类。这种情况下结果集的列和实体类的属性可以直接映射。如果返回的字段无法直接映射,就要用 resultMap 来建立映射关系。

对于关联查询的这种情况,通常不能用 resultType 来映射。用 resultMap 映射,要么就是修改 dto(Data Transfer Object),在里面增加字段,这个会导致增加很多无关的字段。要么就是引用关联的对象,比如 Blog 里面包含了一个 Author 对象,这种情况下就要用到关联查询(association,或者嵌套查询),MyBatis 可以帮我们自动做结果的映射。

一对一的关联查询有两种配置方式:

  1. 嵌套结果:

    <resultMap id ="BlogWithAuthorResultMap" type ="com.gupaoedu.domain.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType ="INTEGER"/>
        <result  column" ="name"  property" ="name"  jdbcType ="VARCHAR"/>
        <!-- 联合查询,将 author 的属性映射到 ResultMap -->
        <association property="author" javaType ="com.gupaoedu.domain.Author">
            <id column="author_id"  property ="authorId"/>
            <result column="author_name" property ="authorName"/>
        </association>
    </resultMap>
    
  2. 嵌套查询:

    <!-- 另一种联合查询 (一对一)的实现,但是这种方式有“N+1”的问题 -->
    <resultMap id="BlogWithAuthorQueryMap" type ="com.gupaoedu.domain.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType ="INTEGER"/>
        <result column="name" property="name" jdbcType ="VARCHAR"/>
        <association property="author" javaType ="com.gupaoedu.domain.Author"
                     column="author_id"  select ="selectAuthor"/> <!-- selectAuthor 定义在下面-->
    </resultMap>
    <!-- 嵌套查询 -->
    <select id="selectAuthor" parameterType="int" resultType ="com.gupaoedu.domain.Author">
        select author_id authorId, author_name authorName from author where author_id = #{authorId}
    </select>
    

其中第二种方式:嵌套查询,由于是分两次查询,当我们查询了员工信息之后,会再发送一条 SQL 到数据库查询部门信息。

我们只执行了一次查询员工信息的 SQL(所谓的 1),如果返回了 N 条记录,就会再发送 N 条到数据库查询部门信息(所谓的 N),这个就是我们所说的 N+1 的问题。
这样会白白地浪费我们的应用和数据库的性能。
如果我们用了嵌套查询的方式,怎么解决这个问题?能不能等到使用部门信息的时候再去查询?这个就是我们所说的延迟加载,或者叫懒加载。

在 MyBatis 里面可以通过 开启延迟加载的开关来解决这个问题。

在 settings 标签里面可以配置:

<!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value ="true"/>
<!--当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过 select 标签的fetchType 来覆盖-->
<setting name="aggressiveLazyLoading" value ="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认 JAVASSIST -->
<setting name="proxyFactory" value" ="CGLIB" />

lazyLoadingEnabled 决定了是否延迟加载。
aggressiveLazyLoading 决定了是不是对象的所有方法都会触发查询。

MBG 与 与 Example

我们在项目中使用 MyBaits 的时候,针对需要操作的一张表,需要创建实体类、Mapper 映射器、Mapper 接口,里面又有很多的字段和方法的配置,这部分的工作是非常繁琐的。而大部分时候我们对于表的操作是相同的,比如根据主键查询、根据 Map查询、单条插入、批量插入、根据主键删除等等等等。当我们的表很多的时候,意味着有大量的重复工作。所以有没有一种办法,可以根据我们的表,自动生成实体类、Mapper映射器、Mapper 接口,里面包含了我们需要用到的这些基本方法和 SQL 呢?

MyBatis 也提供了一个这样的东西,叫做 MyBatis Generator,简称 MBG。我们只需要修改一个配置文件,使用相关的 jar 包命令或者 Java 代码就可以帮助我们生成实体类、映射器和接口文件。不知道用 MyBatis 的同学有没有跟当年的我一样,还是实体类的一个一个字段,接口的一个一个方法,映射器的一条一条 SQL 去写的。

MBG 的配置文件里面有一个 Example 的开关,这个东西用来构造复杂的筛选条件的,换句话说就是根据我们的代码去生成 where 条件(类似于 Tom 老师的自动生成where 条件的方式)。
原理:在实体类中包含了两个有继承关系的 Criteria,用其中自动生成的方法来构建查询条件。把这个包含了 Criteria 的实体类作为参数传到查询参数中,在解析 Mapper映射器的时候会转换成 SQL 条件。

翻页

在写存储过程的年代,翻页也是一件很难调试的事情,我们要实现数据不多不少准确地返回,需要大量的调试和修改。但是如果自己手写过分页,就能清楚分页的原理。

逻辑 翻页与物理翻页

在我们查询数据库的操作中,有两种翻页方式,一种是逻辑翻页(假分页),一种是物理翻页(真分页)。逻辑翻页的原理是把所有数据查出来,在内存中删选数据。 物理翻页是真正的翻页,比如 MySQL 使用 limit 语句,Oracle 使用 rownum 语句,SQLServer 使用 top 语句。

逻辑翻页
MyBatis 里面有一个逻辑分页对象 RowBounds,里面主要有两个属性,offset 和 limit(从第几条开始,查询多少条)。
我们可以在Mapper接口的方法上加上这个参数,不需要修改xml里面的SQL语句。

int start = 10; // offset,从第几行开始查询
int pageSize = 5; // limit,查询多少条
RowBounds rb = w new RowBounds(start, pageSize);
List<Blog> list = mapper.selectBlogList(rb);
for(Blog b :list){
    System. out .println(b);
}

它的底层其实是对 ResultSet 的处理。它会舍弃掉前面 offset 条数据,然后再取剩下的数据的 limit 条。

// DefaultResultSetHandler.java
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)  throws SQLException {
    DefaultResultContext<Object> resultContext = w new DefaultResultContext();
    ResultSet resultSet = rsw.getResultSet();
    this.skipRows(resultSet, rowBounds);
    while( this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() &&
          resultSet.next()) {
        ResultMap discriminatedResultMap =  this.resolveDiscriminatedResultMap(resultSet, resultMap, (String) null);
        Object rowValue =  this.getRowValue(rsw, discriminatedResultMap, (String) null);
        this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
}

很明显,如果数据量大的话,这种翻页方式效率会很低(跟查询到内存中再使用subList(start,end)没什么区别)。所以我们要用到物理翻页。

物理翻页
物理翻页是真正的翻页,它是通过数据库支持的语句来翻页。
第一种简单的办法就是传入参数(或者包装一个 page 对象),在 SQL 语句中翻页。

<select  id="selectBlogPage"  parameterType="map"  resultMap="BaseResultMap">
    select * from blog limit #{curIndex} , #{pageSize}
</select>

第一个问题是我们要在 Java 代码里面去计算起止序号;第二个问题是:每个需要翻页的 Statement 都要编写 limit 语句,会造成 Mapper 映射器里面很多代码冗余。那我们就需要一种通用的方式,不需要去修改配置的任何一条 SQL 语句,只要在我们需要翻页的地方封装一下翻页对象就可以了。

我们最常用的做法就是使用翻页的插件,这个是基于 MyBatis 的拦截器实现的,比如 PageHelper。

// pageSize 每一页几条
PageHelper.startPage(pn, 10);
List<Employee> emps = employeeService.getAll();
// navigatePages 导航页码数
PageInfo page = new PageInfo(emps, 10);
return Msg.success ().add( "pageInfo", page);

PageHelper 是通过 MyBatis 的拦截器实现的,插件的具体原理我们后面的课再分析。简单地来说,它会根据 PageHelper 的参数,改写我们的 SQL 语句。比如 MySQL会生成 limit 语句,Oracle 会生成 rownum 语句,SQL Server 会生成 top 语句。

通用 Mapper

问题:当我们的表字段发生变化的时候,我们需要修改实体类和 Mapper 文件定义的字段和方法。如果是增量维护,那么一个个文件去修改。如果是全量替换,我们还要去对比用 MBG 生成的文件。字段变动一次就要修改一次,维护起来非常麻烦。
解决这个问题,我们有两种思路。

第 一 个 , 因为 MyBatis 的 Mapper 是支持继承open in new window的所以我们可以把我们的Mapper.xml 和 Mapper 接口都分成两个文件。一个是 MBG 生成的,这部分是固定不变的。然后创建 DAO 类继承生成的接口,变化的部分就在 DAO 里面维护。

编写一个支持泛型的通用接口,比如叫 GPBaseMapper<T>,把实体类作为参数传入。这个接口里面定义了大量的增删改查的基础方法,这些方法都是支持泛型的。自定义的 Mapper 接口继承该通用接口 , 例如 BlogMapper extends GPBaseMapper<Blog>,自动获得对实体类的操作方法。遇到没有的方法,我们依然可以在我们自己的 Mapper 里面编写。

我们能想到的解决方案,早就有人做了这个事了,这个东西就叫做 通用Mapperopen in new window

用途:主要解决单表的增删改查问题,并不适用于多表关联查询的场景。除了配置文件变动的问题之外,通用 Mapper 还可以解决:

  • 每个 Mapper 接口中大量的重复方法的定义;
  • 屏蔽数据库的差异;
  • 提供批量操作的方法;
  • 实现分页。

MyBatis-Plus

MyBatis-Plusopen in new window 是原生 MyBatis 的一个增强工具,可以在使用原生 MyBatis 的所有功能的基础上,使用 plus 特有的功能。
MyBatis-Plus 的核心功能:
通用 CRUD:定义好 Mapper 接口后,只需要继承 BaseMapper<T> 接口即可获得通用的增删改查功能,无需编写任何接口方法与配置文件。
条件构造器:通过 EntityWrapper<T>(实体包装类),可以用于拼接 SQL 语句,并且支持排序、分组查询等复杂的 SQL。
代码生成器:支持一系列的策略配置与全局配置,比 MyBatis 的代码生成更好用。

另外 MyBatis-Plus 也有分页的功能。

上次编辑于:
贡献者: soulballad