Mybatis 从入门到起飞 ~ 芜湖 ~

155 阅读24分钟

简介

官网地址:mybatis.org/mybatis-3/z…

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

“半自动”的ORM框架能够很好的解决上面所讲的Hibernate的几个问题,半自动化”是相对于Hibernate的全自动化来说的。它的封装程度没有Hibernate那么高,不会自动生成全部的SQL语句,主要解决的是SQL和对象的映射问题。

MyBatis的前身是ibatis,2001年开始开发,是“internet”和“abatis ['æbətɪs](障碍物)”两个单词的组合。04年捐赠给Apache。2010年更名为MyBatis。 在MyBatis里面,SQL和代码是分离的,所以会写SQL基本上就会用MyBatis,没有额外的学习成本。

简单案例

接下来我们就通过实际的案例代码来演示下MyBatis的具体使用。该部分项目没有使用springboot集成mybatis的操作,完全是mybatis的基本操作。

环境准备

创建一个普通的Maven项目,然后添加对应的Mybatis和MySQL的相关依赖。

	<dependency> 
    	<groupId>org.mybatis</groupId> 
    	<artifactId>mybatis</artifactId>
    	<version>3.5.4</version> 
    </dependency> 
    <dependency> 
        <groupId>mysql</groupId> 
        <artifactId>mysql-connector-java</artifactId> 
        <version>8.0.11</version> 
    </dependency>

POJO对象

我们的案例通过数据库中的 T_USER 表来讲解,创建的对应的POJO对象为,有用到Lombok,大家可以自行添加对应的依赖。

@Data
public class User implements Serializable {
    private Integer id;

    private String userName;

    private String realName;

    private String password;

    private Integer age;

    private Integer dId;

    private Dept dept;

}

配置文件

在MyBatis中我们需要添加全局的配置文件和对应的映射文件。

全局配置文件,这里面是对MyBatis的核心行为的控制。

<?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="db.properties"></properties>
    <settings>
        <!-- 打印查询语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING"/>

        <!-- 控制全局缓存(二级缓存),默认 true-->
        <setting name="cacheEnabled" value="true"/>

        <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
        <setting name="lazyLoadingEnabled" value="true"/>

        <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
        <setting name="aggressiveLazyLoading" value="false"/>

        <!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
        <!--<setting name="proxyFactory" value="CGLIB" />-->

        <!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
        <!--
                <setting name="localCacheScope" value="STATEMENT"/>
        -->

        <setting name="localCacheScope" value="SESSION"/>
    </settings>

    <typeAliases>
        <!-- 这种方法需要一个一个配置别名,比较繁琐 -->
        <!--<typeAlias alias="user" type="com.demo.domain.User" />-->
        <package name="com.demo.domain"/>
    </typeAliases>

    <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>

    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>

</configuration>

关联的映射文件,通常来说一张表对应一个,我们会在这个里面配置我们增删改查的SQL语句,以及参数和返回的结果集的映射关系。

<mapper namespace="com.demo.mapper.UserMapper">

  <resultMap id="BaseResultMap" type="user">
    <id property="id" column="id" jdbcType="INTEGER"/>
    <result property="userName" column="user_name" jdbcType="VARCHAR" />
    <result property="realName" column="real_name" jdbcType="VARCHAR" />
    <result property="password" column="password" jdbcType="VARCHAR"/>
    <result property="age" column="age" jdbcType="INTEGER"/>
    <result property="dId" column="d_id" jdbcType="INTEGER"/>
  </resultMap>

    <sql id="baseSQL">
        id,user_name,real_name,password,age,d_id
    </sql>

  <select id="selectUserById" resultType="user" statementType="PREPARED"  >
        select
         id,
         user_name userName,
         real_name realName,
         password,
         age,
         d_id
         from t_user where id = #{id}
    </select>

  <!-- $只能用在自定义类型和map上 -->
  <select id="selectUserByBean"  parameterType="user" resultMap="BaseResultMap" >
        select * from t_user where user_name = '${userName}'
    </select>

  <select id="selectUserList" resultMap="BaseResultMap" >
        select * from t_user
    </select>

</mapper>

数据库属性的配置文件一并贴出。

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456

编程式的使用

环境准备好后我们就可以来使用其帮助我们实现数据库的操作了。在MyBatis中的使用方式有两种,首先来看下第一种编程式的方式。

/**
 * MyBatis API 的使用
 *
 * @throws Exception
 */
@Test
public void test1() throws Exception {
    // 1.获取配置文件
    InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
    // 2.加载解析配置文件并获取SqlSessionFactory对象
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
    // 3.根据SqlSessionFactory对象获取SqlSession对象
    SqlSession sqlSession = factory.openSession();
    // 4.通过SqlSession中提供的 API方法来操作数据库
    List<User> list = sqlSession.selectList("com.demo.mapper.UserMapper.selectUserList");
    for (User user : list) {
        System.out.println(user);
    }
    // 5.关闭会话
    sqlSession.close();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLRechKD-1635820957336)(Mybatis高级应用.assets/image-20211101162710582.png)]

这种方式其实就是通过SqlSession中给我们提供的相关的API方法来执行对应的CRUD操作,查找我们写的SQL语句是通过 namespace+"."+id的方式实现的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cOGU5Ipt-1635820957337)(Mybatis高级应用.assets/image-20211101163043616.png)]

这样的调用方式,解决了重复代码、资源管理、SQL耦合、结果集映射这4大问题。

不过,这样的调用方式还是会存在一些问题:

  1. Statement ID是硬编码,维护起来很不方便;
  2. 不能在编译时进行类型检查,如果namespace或者Statement ID输错了,只能在运行的时候报错。

所以我们通常会使用第二种方式,也是新版的MyBatis里面推荐的方式:定义一个Mapper接口的方式。这个接口全路径必须跟Mapper.xml里面的namespace对应起来,方法也要跟Statement ID一一对应。

代理方式的使用

我们还可以通过SqlSession中提供的getMapper方法来获取声明接口的代理对象来处理。实现如下:

接口声明

我们需要声明一个Dao的接口。然后在接口中定义相关的方法。

public interface UserMapper {

    public List<User> selectUserList();

}

映射文件

我们通过getMapper的方式来使用的话,我们需要添加对应的映射文件,在映射文件中我们需要将namespace声明为上面接口的全类路径名,同时对应的sql标签的id要和方法名称一致。最后我们还有保证映射文件的名称和接口的名称要一致。在文件很多的情况能很好的管理。
在这里插入图片描述

getMapper

最后我们在通过getMapper方法来获取声明的Dao接口的代码对象来实现数据库操作。

/**
 * MyBatis getMapper 方法的使用
 */
@Test
public void test2() throws Exception {
    // 1.获取配置文件
    InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
    // 2.加载解析配置文件并获取SqlSessionFactory对象
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
    // 3.根据SqlSessionFactory对象获取SqlSession对象
    SqlSession sqlSession = factory.openSession();
    // 4.通过SqlSession中提供的 API方法来操作数据库
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> list = mapper.selectUserList();
    for (User user : list) {
        System.out.println(user);
    }
    // 5.关闭会话
    sqlSession.close();
}

通过执行接口方法,来执行映射器中的SQL语句。

最后总结下MyBatis的特点:

  1. 使用连接池对连接进行管理
  2. SQL和代码分离,集中管理
  3. 结果集映射
  4. 参数映射和动态SQL
  5. 重复SQL的提取
  6. 缓存管理
  7. 插件机制

Hibernate和MyBatis跟DbUtils、Spring JDBC一样,都是对JDBC的一个封装,我们去看源码,最后一定会看到Connection、Statement和ResultSet这些对象。对应的选择:

  1. 在一些业务比较简单的项目中,我们可以使用Hibernate;
  2. 如果需要更加灵活的SQL,可以使用MyBatis,对于底层的编码,或者性能要求非常高的场合,可以用JDBC;
  3. 实际上在我们的项目中,MyBatis和Spring JDBC是可以混合使用的;
  4. 当然,我们也根据项目的需求自己写ORM框架。

Mybatis核心配置

在MyBatis中我们发现其实他最核心的应该是那两个配置文件,一个全局配置文件,一个映射文件。我们只要把这两个文件弄清楚,其实对于MyBatis的使用就掌握了大部分。接下来我们详细的给大家来介绍下这两个配置文件。

相关信息可至官网查看。

全局配置文件

MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
在这里插入图片描述

configuration

configuration是整个配置文件的根标签,实际上也对应着MyBatis里面最重要的配置类Configuration。它贯穿MyBatis执行流程的每一个环节。我们打开这个类看一下,这里面有很多的属性,跟其他的子标签也能对应上。

properties

第一个一级标签是properties,用来配置参数信息,比如最常见的数据库连接信息。为了避免直接把参数写死在xml配置文件中,我们可以把这些参数单独放在properties文件中,用properties标签引入进来,然后在xml配置文件中用${}引用就可以了。可以用resource引用应用里面的相对路径,也可以用url指定本地服务器或者网络的绝对路径。

<?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="db.properties"></properties>

    <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>

    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>

</configuration>

settings

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 具体的配置项较多,如有需要可以到官网查看。

设置的相关案例:

<settings>
    <!-- 打印查询语句 -->
    <setting name="logImpl" value="STDOUT_LOGGING"/>

    <!-- 控制全局缓存(二级缓存),默认 true-->
    <setting name="cacheEnabled" value="true"/>

    <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
    <setting name="lazyLoadingEnabled" value="true"/>

    <!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
    <setting name="aggressiveLazyLoading" value="false"/>

    <!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
    <!--<setting name="proxyFactory" value="CGLIB" />-->

    <!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
    <!--
            <setting name="localCacheScope" value="STATEMENT"/>
    -->

    <setting name="localCacheScope" value="SESSION"/>
</settings>

typeAliases

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

我们可以为自己的Bean创建别名,既可以指定单个类,也可以指定一个package,自动转换。

<typeAliases>
    <!-- 这种方法需要一个一个配置别名,比较繁琐 -->
    <!--<typeAlias alias="user" type="com.demo.domain.User" />-->
    <!-- 指定包名 自动转换 -->
    <package name="com.demo.domain"/>
</typeAliases>

定义别名之后,我们就可以直接使用了。
在这里插入图片描述

MyBatis里面有很多系统预先定义好的类型别名,在TypeAliasRegistry中,所以可以用string代替java.lang.String。

public TypeAliasRegistry() {
    this.registerAlias("string", String.class);
    this.registerAlias("byte", Byte.class);
    this.registerAlias("long", Long.class);
    this.registerAlias("short", Short.class);
    this.registerAlias("int", Integer.class);
    this.registerAlias("integer", Integer.class);
    this.registerAlias("double", Double.class);
    this.registerAlias("float", Float.class);
    this.registerAlias("boolean", Boolean.class);
    this.registerAlias("byte[]", Byte[].class);
    this.registerAlias("long[]", Long[].class);
    this.registerAlias("short[]", Short[].class);
    this.registerAlias("int[]", Integer[].class);
    this.registerAlias("integer[]", Integer[].class);
    this.registerAlias("double[]", Double[].class);
    this.registerAlias("float[]", Float[].class);
    this.registerAlias("boolean[]", Boolean[].class);
    this.registerAlias("_byte", Byte.TYPE);
    this.registerAlias("_long", Long.TYPE);
    this.registerAlias("_short", Short.TYPE);
    this.registerAlias("_int", Integer.TYPE);
    this.registerAlias("_integer", Integer.TYPE);
    this.registerAlias("_double", Double.TYPE);
    this.registerAlias("_float", Float.TYPE);
    this.registerAlias("_boolean", Boolean.TYPE);
    this.registerAlias("_byte[]", byte[].class);
    this.registerAlias("_long[]", long[].class);
    this.registerAlias("_short[]", short[].class);
    this.registerAlias("_int[]", int[].class);
    this.registerAlias("_integer[]", int[].class);
    this.registerAlias("_double[]", double[].class);
    this.registerAlias("_float[]", float[].class);
    this.registerAlias("_boolean[]", boolean[].class);
    this.registerAlias("date", Date.class);
    this.registerAlias("decimal", BigDecimal.class);
    this.registerAlias("bigdecimal", BigDecimal.class);
    this.registerAlias("biginteger", BigInteger.class);
    this.registerAlias("object", Object.class);
    this.registerAlias("date[]", Date[].class);
    this.registerAlias("decimal[]", BigDecimal[].class);
    this.registerAlias("bigdecimal[]", BigDecimal[].class);
    this.registerAlias("biginteger[]", BigInteger[].class);
    this.registerAlias("object[]", Object[].class);
    this.registerAlias("map", Map.class);
    this.registerAlias("hashmap", HashMap.class);
    this.registerAlias("list", List.class);
    this.registerAlias("arraylist", ArrayList.class);
    this.registerAlias("collection", Collection.class);
    this.registerAlias("iterator", Iterator.class);
    this.registerAlias("ResultSet", ResultSet.class);
}

TypeHandler

由于Java类型和数据库的JDBC类型不是一一对应的(比如String与varchar、char、text),所以我们把Java对象转换为数据库的值,和把数据库的值转换成Java对象,需要经过一定的转换,这两个方向的转换就要用到TypeHandler。

当参数类型和返回值是一个对象的时候,我没有做任何的配置,为什么对象里面的一个String属性,可以转换成数据库里面的varchar字段?

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

这个也是为什么大部分类型都不需要处理。当我们查询数据和登记数据,做数据类型转换的时候,就会自动调用对应的TypeHandler的方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oqOiIiq3-1635820957340)(F:\notes\images\image-20211101223344999.png)]

我们可以自定义一个TypeHandler来帮助我们简单的处理数据,比如查询的结果的字段如果是一个字符串,且值为"zhangsan"就修饰下这个信息。

public class MyTypeHandler extends BaseTypeHandler<String> {
    /**
     * 插入数据的时候回调的方法
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        //System.out.println("---------------setNonNullParameter1:"+parameter);
        ps.setString(i, parameter);
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String name = rs.getString(columnName);
        if ("zhangsan".equals(name)) {
            return name + "666";
        }
        return name;
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String name = rs.getString(columnIndex);
        if ("zhangsan".equals(name)) {
            return name + "666";
        }
        return name;
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String name = cs.getString(columnIndex);
        if ("zhangsan".equals(name)) {
            return name + "666";
        }
        return name;
    }
}

同时将我们的处理器在全局配置文件中注册下:

<typeHandlers>
    <typeHandler handler="com.demo.type.MyTypeHandler" jdbcType="VARCHAR" javaType="String"></typeHandler>
    <!--<typeHandler handler="com.demo.type.MyTypeHandler" ></typeHandler>-->
</typeHandlers>

然后我们在映射文件中配置对应的处理器:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VvAaMP2E-1635820957340)(F:\notes\images\image-20211101223629272.png)]

执行效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-irR6HoYm-1635820957341)(F:\notes\images\image-20211101223906153.png)]

objectFactory

当我们把数据库返回的结果集转换为实体类的时候,需要创建对象的实例,由于我们不知道需要处理的类型是什么,有哪些属性,所以不能用new的方式去创建。只能通过反射来创建。

在MyBatis里面,它提供了一个工厂类的接口,叫做ObjectFactory,专门用来创建对象的实例(MyBatis封装之后,简化了对象的创建),里面定义了4个方法。

public interface ObjectFactory {
    default void setProperties(Properties properties) {
    }

    <T> T create(Class<T> var1);

    <T> T create(Class<T> var1, List<Class<?>> var2, List<Object> var3);

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

ObjectFactory有一个默认的实现类DefaultObjectFactory。创建对象的方法最终都调用instantiateClass(),这里面能看到反射的代码。

默认情况下,所有的对象都是由DefaultObjectFactory创建。

public class DemoObjectFactory extends DefaultObjectFactory {

    /**
     * 重写通过无参构造方法创建实例的方法
     */
    @Override
    public <T> T create(Class<T> type) {
        System.out.println("Object Factory .... create ");
        if (type.equals(User.class)) {
            // 创建的类型如果是User类型 我们就自己来创建
            User user = new User();
            user.setUserName("ObjectFactory 测试");
            return (T) user;
        }
        return super.create(type);
    }
}

测试代码:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwORV40B-1635820957342)(F:\notes\images\image-20211101224542145.png)]

如果在config文件里面注册,在创建对象的时候会被自动调用:

<!-- 对象工厂 User userName -->
<objectFactory type="com.demo.objectfactory.DemoObjectFactory">
    <property name="cc" value="666"/>
</objectFactory>

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

plugins

插件是MyBatis的一个很强大的机制。跟很多其他的框架一样,MyBatis预留了插件的接口,让MyBatis更容易扩展。

mybatis.org/mybatis-3/z…

environment

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

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

transactionManager

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

如果配置成MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic。因为我们跑的是本地程序,如果配置成MANAGE不会有任何事务。

如果是Spring + MyBatis,则没有必要配置,因为我们会直接在applicationContext.xml里面配置数据源和事务,覆盖MyBatis的配置。

dataSource

数据源,顾名思义,就是数据的来源,一个数据源就对应一个数据库。在Java里面,它是对数据库连接的一个抽象。

一般的数据源都会包括连接池管理的功能,所以很多时候也把DataSource直接 称为连接池,准确的说法应该是:带连接池功能的数据源。

mappers

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

我们有四种指定Mapper文件的方式:

www.mybatis.org/mybatis-3/z…

a.使用相对于类路径的资源引用(resource)

<mappers>
    <mapper resource="mapper/UserMapper.xml"/>
</mappers>

b.使用完全限定资源定位符(绝对路径)(URL)

<mappers> 
    <mapper resource="file:///app/sale/mappers/UserMapper.xml"/> 
</mappers>

c.使用映射器接口实现类的完全限定类名

<mappers>
    <mapper class="com.demo.mapper.UserMapper"/> 
</mappers>

d.将包内的映射器接口实现全部注册为映射器(最常用)

<mappers>
    <mapper class="com.demo.mapper"/> 
</mappers>

映射文件

MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95%的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。

SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句。

cache

给定命名空间的缓存配置(是否开启二级缓存)。

cache-ref

其他命名空间缓存配置的引用。缓存相关两个注解在源码分析中会详解。

resultMap

是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。

<resultMap id="BaseResultMap" type="user">
    <id property="id" column="id" jdbcType="INTEGER"/>
    <result property="userName" column="user_name" jdbcType="VARCHAR"/>
    <result property="realName" column="real_name" jdbcType="VARCHAR"/>
    <result property="password" column="password" jdbcType="VARCHAR"/>
    <result property="age" column="age" jdbcType="INTEGER"/>
    <result property="dId" column="d_id" jdbcType="INTEGER"/>
</resultMap>

sql

可被其他语句引用的可重用语句块。

<sql id="baseSQL">
    id,user_name,real_name,password,age,d_id
</sql>

增删改查标签

针对常用的增删改查操作提供的有对应的标签来处理。

select元素的相关属性

属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。
parameterMap用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。
resultType期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。
resultMap对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个。
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
useCache将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
fetchSize这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。
statementType可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
resultSetTypeFORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动)。
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。
resultOrdered这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false
resultSets这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。

Insert, Update, Delete 元素的属性

属性描述
id在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。
parameterMap用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。
flushCache将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。
timeout这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
statementType可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
useGeneratedKeys(仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。
keyProperty(仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn(仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。

示例:

<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

Mybatis最佳实践

动态SQL语句

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。

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

if

需要判断的时候,条件写在test中

<select id="selectListIf" parameterType="user" resultMap="BaseResultMap">
    select
    <include refid="baseSQL"></include>
    from t_user
    <where>
        <if test="id != null">
            and id = #{id}
        </if>
        <if test="userName != null">
            and user_name = #{userName}
        </if>
    </where>
</select>

choose

需要选择一个条件的时候

<select id="selectListChoose" parameterType="user" resultMap="BaseResultMap">
    select
    <include refid="baseSQL"></include>
    from t_user
    <where>
        <choose>
            <when test="id != null">
                id = #{id}
            </when>
            <when test="userName != null and userName != ''">
                and user_name like CONCAT(CONCAT('%',#{userName,jdbcType=VARCHAR}),'%')
            </when>
            <otherwise>

            </otherwise>
        </choose>
    </where>
</select>

trim

需要去掉where、and、逗号之类的符号的时候

<!--
        trim 的使用
        替代where标签的使用
    -->
    <select id="selectListTrim" resultMap="BaseResultMap"
            parameterType="user">
        select
        <include refid="baseSQL"></include>
        <!-- <where>
            <if test="username!=null">
                and name = #{username}
            </if>
        </where> -->
        <trim prefix="where" prefixOverrides="AND |OR ">
            <if test="userName!=null">
                and user_name = #{userName}
            </if>
            <if test="age != 0">
                and age = #{age}
            </if>
        </trim>
    </select>

    <!-- 替代set标签的使用 -->
    <update id="updateUser" parameterType="User">
        update t_user
        <trim prefix="set" suffixOverrides=",">
            <if test="userName!=null">
                user_name = #{userName},
            </if>
            <if test="age != 0">
                age = #{age}
            </if>
        </trim>
        where id=#{id}
    </update>

foreach

需要遍历集合的时候

<delete id="deleteByList" parameterType="java.util.List">
    delete from t_user where id in
    <!-- ( 1 , 2  , 3) -->
    <foreach collection="list" item="item" open="(" separator="," close=")">
        #{item.id,jdbcType=INTEGER}
    </foreach>
</delete>

批量操作

我们在项目中会有一些批量操作的场景,比如导入文件批量处理数据的情况(批量新增商户、批量修改商户信息),当数据量非常大,比如超过几万条的时候,在Java代码中循环发送SQL到数据库执行肯定是不现实的,因为这个意味着要跟数据库创建几万次会话。即使在同一个连接中,也有重复编译和执行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 into t_user() values (),(),(),()
-->
<insert id="insertUserList" parameterType="java.util.List">
    insert into t_user(user_name,real_name)
    values
    <foreach collection="list" item="user" separator=",">
        (#{user.userName},#{user.realName})
    </foreach>

</insert>

Java代码里面,直接传入一个List类型的参数。

/**
 * 批量插入
 */
@Test
public void test2() throws Exception {
    init();
    long start = System.currentTimeMillis();
    UserMapper mapper = session.getMapper(UserMapper.class);
    int count = 12000;
    List<User> list = new ArrayList<>();
    for (int i = 2000; i < count; i++) {
        User user = new User();
        user.setUserName("a" + i);
        list.add(user);
    }
    mapper.insertUserList(list);
    session.commit();
    session.close();
    long end = System.currentTimeMillis();
    System.out.println("循环批量插入" + count + "条,耗时:" + (end - start) + "毫秒");
}

动态SQL批量插入效率要比循环发送SQL执行要高得多,最关键的地方就在于减少了跟数据库交互的次数,并且避免了开启和结束事务的时间消耗。

批量更新

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

update t_user set 
    user_name = 
    case id 
    when ? then ? 
    when ? then ? 
    when ? then ? end , 
	real_name = 
    case id 
    when ? then 
    ? when ? then 
    ? when ? then ? end 
    where id in ( ? , ? , ? )

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

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

    Integer updateBatch(List<Map<String, Object>> list);
<update id="updateBatch">
        update wds_document
        <trim prefix="set" suffixOverrides=",">
            <trim prefix="qualified =case" suffix="end,">
                <foreach collection="list" item="item" index="index">
                    <if test="item.qualified!=null">
                        when oid = #{item.oid}
                        then #{item.qualified}
                    </if>
                </foreach>
            </trim>
            <trim prefix="check_date =case" suffix="end,">
                <foreach collection="list" item="item" index="index">
                    <if test="item.check_date!=null">
                        when oid = #{item.oid}
                        then #{item.check_date}
                    </if>
                </foreach>
            </trim>
        </trim>
        where oid in
        <foreach collection="list" item="item" index="index" separator="," open="(" close=")">
            #{item.oid}
        </foreach>
    </update>

批量删除

批量删除也类似上方代码,合理运用相关标签。

<delete id="deleteByList" parameterType="java.util.List">
    delete from t_user where id in
    <!-- ( 1 , 2  , 3) -->
    <foreach collection="list" item="item" open="(" separator="," close=")">
        #{item.id,jdbcType=INTEGER}
    </foreach>
</delete>

BatchExecutor

当然MyBatis的动态标签的批量操作也是存在一定的缺点的,比如数据量特别大的时候,拼接出来的SQL语句过大。

MySQL的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是 4M,需要修改默认配置或者手动地控制条数,才可以解决这个问题。

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.

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

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

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

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

Executor

在这里插入图片描述

  1. SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
  2. ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
  3. BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。executeUpdate()是一个语句访问一次数据库,executeBatch()是一批语句访问一次数据库(具体一批发送多少条SQL跟服务端的max_allowed_packet有关)。BatchExecutor底层是对JDBC ps.addBatch()和ps. executeBatch()的封装。
public void testJdbcBatch() throws IOException {
    Connection conn = null;
    PreparedStatement ps = null;

    try {
        conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&serverTimezone=UTC", "root", "123456");
        ps = conn.prepareStatement("INSERT into t_user(user_name,real_name) values ( ?, ?)");

        for (int i = 1000; i < 101000; i++) {
            ps.setString(1, "a" + i);
            ps.setString(2, "b" + i);
            ps.addBatch();
        }
        // 执行批处理
        ps.executeBatch();
        ps.close();
        conn.close();
    } catch (SQLException se) {
        se.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (ps != null) ps.close();
        } catch (SQLException se2) {
        }
        try {
            if (conn != null) conn.close();
        } catch (SQLException se) {
            se.printStackTrace();
        }
    }
}

关联查询

嵌套查询

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

1对1
<!-- 嵌套查询 11 1个用户对应一个部门-->
<resultMap id="nestedMap1" type="user">
    <id property="id" column="id" jdbcType="INTEGER"/>
    <result property="userName" column="user_name" jdbcType="VARCHAR"/>
    <result property="realName" column="real_name" jdbcType="VARCHAR"/>
    <result property="password" column="password" jdbcType="VARCHAR"/>
    <result property="age" column="age" jdbcType="INTEGER"/>
    <result property="dId" column="d_id" jdbcType="INTEGER"/>
    <association property="dept" javaType="dept">
        <id column="did" property="dId"/>
        <result column="d_name" property="dName"/>
        <result column="d_desc" property="dDesc"/>
    </association>
</resultMap>

效果图:
在这里插入图片描述

1对多
<!-- 嵌套查询 1对多 1个部门有多个用户-->
<resultMap id="nestedMap2" type="dept">
    <id column="did" property="dId"/>
    <result column="d_name" property="dName"/>
    <result column="d_desc" property="dDesc"/>
    <collection property="users" ofType="user">
        <id property="id" column="id" jdbcType="INTEGER"/>
        <result property="userName" column="user_name" jdbcType="VARCHAR"/>
        <result property="realName" column="real_name" jdbcType="VARCHAR"/>
        <result property="password" column="password" jdbcType="VARCHAR"/>
        <result property="age" column="age" jdbcType="INTEGER"/>
        <result property="dId" column="d_id" jdbcType="INTEGER"/>
    </collection>
</resultMap>
<select id="queryDeptNested" resultMap="nestedMap2">
    SELECT t1.`id`
         , t1.`user_name`
         , t1.`real_name`
         , t1.`password`
         , t1.`age`
         , t2.`did`
         , t2.`d_name`
         , t2.`d_desc`
    FROM t_user t1
             RIGHT JOIN
         t_department t2
         ON t1.`d_id` = t2.`did`
</select>

在这里插入图片描述

延迟加载

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

在settings标签里面可以配置:

<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false  -->
<setting name="lazyLoadingEnabled" value="true"/>

<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>

<!--  Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<setting name="proxyFactory" value="CGLIB" />

lazyLoadingEnabled决定了是否延迟加载(默认false)。

aggressiveLazyLoading决定了是不是对象的所有方法都会触发查询。

1对1的延迟加载配置
<!-- 延迟加载 11 -->
<resultMap id="nestedMap1Lazy" type="user">
    <id property="id" column="id" jdbcType="INTEGER"/>
    <result property="userName" column="user_name" jdbcType="VARCHAR"/>
    <result property="realName" column="real_name" jdbcType="VARCHAR"/>
    <result property="password" column="password" jdbcType="VARCHAR"/>
    <result property="age" column="age" jdbcType="INTEGER"/>
    <result property="dId" column="d_id" jdbcType="INTEGER"/>
    <association property="dept" javaType="dept" column="d_id" select="queryDeptByUserIdLazy">

    </association>
</resultMap>
<resultMap id="baseDept" type="dept">
    <id column="did" property="dId"/>
    <result column="d_name" property="dName"/>
    <result column="d_desc" property="dDesc"/>
</resultMap>
<select id="queryUserNestedLazy" resultMap="nestedMap1Lazy">
    SELECT t1.`id`
         , t1.`user_name`
         , t1.`real_name`
         , t1.`password`
         , t1.`age`
         , t1.d_id
    FROM t_user t1
</select>
<select id="queryDeptByUserIdLazy" parameterType="int" resultMap="baseDept">
    select *
    from t_department
    where did = #{did}
</select>

注意:开启了延迟加载的开关,调用user.getDept()以及默认的(equals,clone,hashCode,toString)时才会发起第二次查询,其他方法并不会触发查询,比如user.getName()等获取用户基础信息的方法。

/**
 * 1对1  关联查询 延迟加载
 *
 * @throws Exception
 */
@Test
public void test03() throws Exception {
    init();
    UserMapper mapper = session.getMapper(UserMapper.class);
    List<User> users = mapper.queryUserNestedLazy();
    for (User user : users) {
        //System.out.println(user.getUserName() );
        System.out.println(user.getUserName() + "---->" + user.getDept());
    }
}

触发延迟加载的方法可以通过配置,默认equals(),clone(),hashCode(),toString()。

1对多的延迟加载的配置
<!-- 1对多 延迟加载 -->
<resultMap id="nestedMap2Lazy" type="dept">
    <id column="did" property="dId"/>
    <result column="d_name" property="dName"/>
    <result column="d_desc" property="dDesc"/>
    <collection property="users" ofType="user" column="did" select="queryUserByDeptLazy">
    </collection>
</resultMap>

<select id="queryDeptNestedLazy" resultMap="nestedMap2">
    SELECT
            , t2.`did`
            , t2.`d_name`
            , t2.`d_desc`
    FROM
        t_department t2

</select>

<select id="queryUserByDeptLazy" resultMap="BaseResultMap">
    select *
    from t_user
    where d_id = #{did}
</select>

分页操作

逻辑分页

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

接口中定义:

public List<User> queryUserList(RowBounds rowBounds);

测试类:

/**
 * 逻辑分页查询
 *
 * @throws Exception
 */
@Test
public void test01() throws Exception {
    init();
    UserMapper mapper = session.getMapper(UserMapper.class);
    RowBounds rowBounds = new RowBounds(1, 3);
    List<User> users = mapper.queryUserList(rowBounds);
    for (User user : users) {
        System.out.println(user);
    }
}

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

// DefaultResultSetHandler.java
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    DefaultResultContext<Object> resultContext = 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="selectUserPage" parameterType="map" resultMap="BaseResultMap"> 
    select * from t_user limit #{curIndex} , #{pageSize}
</select>

第一个问题是我们要在Java业务代码里面去计算起止序号;第二个问题是:每个需要翻页的Statement都要编写limit语句,会造成Mapper映射器里面很多代码冗余。

那我们就需要一种通用的方式,不需要去修改配置的任何一条SQL语句,我们只要传入当前是第几页,每页多少条就可以了,自动计算出来起止序号。

我们最常用的做法就是使用翻页的插件,比如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语句。

MBG与Example

github.com/mybatis/gen…

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

所以有没有一种办法,可以根据我们的表,自动生成实体类、Mapper映射器、Mapper接口,里面包含了我们需要用到的这些基本方法和SQL呢?

MyBatis也提供了一个代码生成器,叫做MyBatis Generator,简称MBG(它是MyBatis的一个插件)。我们只需要修改一个配置文件,使用相关的jar包命令或者Java代码就可以帮助我们生成实体类、映射器和接口文件。

MBG的配置文件里面有一个Example的开关,这个东西用来构造复杂的筛选条件的,换句话说就是根据我们的代码去生成where条件。

原理:在实体类中包含了两个有继承关系的Criteria,用其中自动生成的方法来构建查询条件。把这个包含了Criteria的实体类作为参数传到查询参数中,在解析Mapper映射器的时候会转换成SQL条件。

配置文件

我们首先需要添加代码生成器的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <!-- 数据库的驱动包路径 -->
    <classPathEntry location="C:\Users\1429\.m2\repository\mysql\mysql-connector-java\8.0.11\mysql-connector-java-8.0.11.jar" />

    <context id="DB2Tables" targetRuntime="MyBatis3">
        <!-- 去掉生成文件中的注释 -->
        <commentGenerator>
            <property name="suppressAllComments" value="true" />
        </commentGenerator>
        <!-- 数据库链接URL、用户名、密码 -->
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&amp;serverTimezone=UTC"
                        userId="root"
                        password="123456">
            <property name="nullCatalogMeansCurrent" value="true" />
        </jdbcConnection>
        <!-- <jdbcConnection driverClass="oracle.jdbc.driver.OracleDriver"
              connectionURL="jdbc:oracle:thin:@localhost:1521:XE"
              userId="car"
              password="car">
      </jdbcConnection>  -->

        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>
        <!-- 生成模型的包名和位置 -->
        <javaModelGenerator targetPackage="com.demo.domain" targetProject="./src/main/java">
            <!-- 是否在当前路径下新加一层schema,eg:fase路径com.oop.eksp.user.model, true:com.oop.eksp.user.model.[schemaName] -->
            <property name="enableSubPackages" value="false" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>
        <!-- 生成的映射文件包名和位置 -->
        <sqlMapGenerator targetPackage="com.demo.mapper"  targetProject="./src/main/java">
            <property name="enableSubPackages" value="false" />
        </sqlMapGenerator>
        <!-- 生成DAO的包名和位置 -->
        <javaClientGenerator type="XMLMAPPER" targetPackage="com.demo.mapper"  targetProject="./src/main/java">
            <property name="enableSubPackages" value="false" />
        </javaClientGenerator>

        <!-- 表名 -->
        <table  tableName="t_user" domainObjectName="User"   />

    </context>
</generatorConfiguration>

添加插件

我们需要在pom.xml中添加对应的插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-maven-plugin</artifactId>
            <version>1.3.2</version>
            <configuration>
                <!-- 指定配置文件的位置 -->
                <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
            </configuration>
        </plugin>
    </plugins>
</build>

生成

然后我们就可以利用插件帮助我们快速生成我们需要的表结构对应的相关文件。
在这里插入图片描述

通用mapper

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

方式一

第一个,因为MyBatis的Mapper是支持继承的(见:github.com/mybatis/myb…

public interface UserMapperExt extends UserMapper {
    public List<User> selectUserByName(String userName);
}

对应的映射文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.demo.mapper.UserMapperExt">
    <resultMap id="BaseResultMapExt" type="com.demo.domain.User">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="user_name" property="userName" jdbcType="VARCHAR"/>
        <result column="real_name" property="realName" jdbcType="VARCHAR"/>
        <result column="password" property="password" jdbcType="VARCHAR"/>
        <result column="age" property="age" jdbcType="INTEGER"/>
        <result column="d_id" property="dId" jdbcType="INTEGER"/>
        <result column="i_id" property="iId" jdbcType="INTEGER"/>
    </resultMap>

    <select id="selectUserByName" resultMap="BaseResultMapExt">
        select *
        from t_user
        where user_name = #{userName}
    </select>
</mapper>

在全局配置文件中我们也需要扫描:

<mappers>
    <mapper resource="mapper/UserMapper.xml"/>
    <mapper resource="mapper/UserMapperExt.xml"/>
</mappers>

所以以后只要修改Ext的文件就可以了。这么做有一个缺点,就是文件会增多。
在这里插入图片描述

方式二

既然针对每张表生成的基本方法都是一样的,也就是公共的方法部分代码都是一样的,我们能不能把这部分合并成一个文件,让它支持泛型呢?

当然可以!

编写一个支持泛型的通用接口,比如叫DemoBaseMapper,把实体类作为参数传入。这个接口里面定义了大量的增删改查的基础方法,这些方法都是支持泛型的。

自定义的Mapper接口继承该通用接口,例如UserMapper extends DemoBaseMapper,自动获得对实体类的操作方法。遇到没有的方法,我们依然可以在我们自己的Mapper里面编写。

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

github.com/abel533/Map…

用途:主要解决单表的增删改查问题,并不适用于多表关联查询的场景。

除了配置文件变动的问题之外,通用Mapper还可以解决:

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

使用方式:在Spring中使用时,引入jar包,替换applicationContext.xml中的sqlSessionFactory和configure。

<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer"> 
    <property name="basePackage" value="com.demo.crud.dao"/>
</bean>

MyBatis-Plus

官方地址:mp.baomidou.com/

MyBatis-Plus是原生MyBatis的一个增强工具,可以在使用原生MyBatis的所有功能的基础上,使用plus特有的功能。

MyBatis-Plus的核心功能:

通用 CRUD:定义好Mapper接口后,只需要继承BaseMapper 接口即可获得通用的增删改查功能,无需编写任何接口方法与配置文件。

条件构造器:通过EntityWrapper(实体包装类),可以用于拼接 SQL 语句,并且支持排序、分组查询等复杂的SQL。

代码生成器:支持一系列的策略配置与全局配置,比MyBatis的代码生成更好用。

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

综上所述,MyBatis-Plus YYDS ~ 在这里就不记录如何使用MyBatis-Plus了,会另写一篇文章介绍如何使用。

项目地址

orm-mini