Mybatis学习笔记

2022-09-20 11:03:50 浏览数 (1)

1、概述

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

Mybatis与其他的ORM框架不同,不是将对象与数据库表联系起来,而是将Java方法与SQL语句相关联。相比 Hibernate (全自动ORM框架),Mybatis属于是半自动ORM,在Mybatis上进行SQL优化/SQL语句修改等操作会比较简单,在Hibernate上还需要转换为HQL语言。

2、简单配置

1)环境准备

使用maven进行依赖管理的项目中,只需要引入mybatis的坐标(以及mysql相关),这里以mybatis 3.5.7为例:

代码语言:javascript复制
<dependency>
    <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

为了显示清晰还可以引入log4j日志依赖,之后在类路径下添加配置文件:

代码语言:javascript复制
log4j.rootLogger=error, stdout
log4j.logger.top.jtszt=TRACE
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

之后准备一个数据库表用于CRUD操作(同时写入几条测试数据):

代码语言:javascript复制
CREATE DATABASE mybatis_db2;
USE mybatis_db2;
CREATE TABLE t_employee2(
    id INT PRIMARY KEY AUTO_INCREMENT,
    ename VARCHAR(50),
    gender INT,
    email VARCHAR(50),
    content VARCHAR(50)
);

之后在项目中准备一个实体类,属性与数据库字段对应,用于封装对象。

以及创建一个接口类,先写一个获取全部信息的方法:

代码语言:javascript复制
public interface EmployeeMapper {
    List<Employee> getAll();
}

因为Mybatis使用了Java动态代理直接通过接口调用对应方法,因此这里只需要写接口类即可,无需写实现类。

2)写配置

基于 MyBatis 的应用都需要一个配置文件,在其中配置 Configuration 实例,用于后续构建 SqlSessionFactoryBuilder 。而使用SqlSessionFactoryBuilder 可以构建出 SqlSessionFactory 实例,这是应用的核心。配置文件的样例在官方文档中也有给出:

代码语言:javascript复制
<?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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql:///mybatis_db"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/firstMapper.xml"/>
  </mappers>
</configuration>

之后需要配置上SQL的映射文件,我们在资源目录下创建一个mapper.firstMapper.xml文件:

代码语言:javascript复制
<?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="top.jtszt.mapper.EmployeeMapper">
    
    <select id="getAll" resultType="top.jtszt.entity.Employee">
        select * from t_employee
    </select>
</mapper>

3)测试

简单的配置已经完成了,现在就可以尝试获取查询结果了,这里写一个测试去获取查询结果:

代码语言:javascript复制
public class EmployeeTest {

    SqlSessionFactory sqlSessionFactory;

    @Before
    public void initSqlSessionFactory(){
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(inputStream!=null){
                sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            }
        }
    }

    @Test
    public void test01() {
        try (SqlSession session = sqlSessionFactory.openSession()) {
            EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
            List<Employee> ls = mapper.getAll();
            System.out.println(ls);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

首先我们会通过Resources类的 getResourceAsStream 方法将配置文件加载成输入流,之后new一个 SqlSessionFactoryBuilder 对象,调用其 build 方法并传入配置文件输入流,生成一个sqlSessionFactory,看这名字也清楚,它就是用来造sqlSession的,它里面有一个openSession方法,调用返回一个SqlSession。

3、全局配置文件

在Mbatis的全局配置中 <configuration> 标签下可配置多个标签,每个标签代表一个属性,并且他们的配置是有顺序的,从上至下依次为:

1、properties 2、settings 3、typeAliases

4、typeHandlers 5、objectFactory

6、objectWrapperFactory 7、reflectorFactory

8、plugins 9、environments

10、databaseIdProvider 11、mappers

接下来详细介绍一些重要的标签的含义及使用。

1)properties

全局配置文件中 properties 属性的作用是定义全局配置变量,可以用它来加载外部化的 properties 配置文件。

①全局配置变量

首先定义两个全局配置变量,之后就可以在配置数据源的时候使用 ${key} 进行值绑定。

代码语言:javascript复制
<properties>
    <property name="jdbc.user" value="root"/>
    <property name="jdbc.password" value="root"/>
</properties>
代码语言:javascript复制
<dataSource type="POOLED">
    <property name="username" value="${jdbc.user}"/>
    <property name="password" value="${jdbc.password}"/>
    ...
</dataSource>
②加载外部配置

也可以使用properties来引入外部的配置文件,改变上面配置中数据源的硬编码,假设现在有一个外部配置文件为 db.properties

代码语言:javascript复制
jdbc.username=root
jdbc.password=root
jdbc.jdbcUrl=jdbc:mysql:///mybatis_db
jdbc.driverClass=com.mysql.jdbc.Driver

使用properties配置外部配置,

代码语言:javascript复制
<properties resource="db.properties"/>

之后就可以在配置中使用 ${key} 取出对应属性的值。

代码语言:javascript复制
<dataSource type="POOLED">
    <property name="driver" value="${jdbc.driverClass}"/>
    <property name="url" value="${jdbc.jdbcUrl}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</dataSource>

2)setting

这里的配置主要影响了Mybatis的行为,说是最重要的配置也不为过,包括缓存、延迟加载、驼峰映射等配置,所有可配置的属性在官方文档中都有列举,这里只记录几个比较重要的。

①mapUnderscoreToCamelCase

设置为true之后即为开启驼峰命名规则映射,什么作用?

我们都知道在MySQL中字段的命名是可以使用下划线的,比如描述书名的字段可设置为 book_name ,而Java中的属性命名推荐是驼峰命名规则 bookName 。我们在使用 JDBC 进行查询的时候就可以体会到,如果正常查询该字段是无法封装到实体类中的,需要使用别名查询指定才可。

在Mybatis中只要我们开启了驼峰命名映射,那么Mybatis会帮我们自动封装,将字段名转换为符合Java命名规范的驼峰式命名。

代码语言:javascript复制
<!-- 开启驼峰命名映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
②lazyLoadingEnabled

设置为true即为开启延迟加载,搭配缓存可以达到提升查询效率的作用。

代码语言:javascript复制
<setting name="lazyLoadingEnabled" value="true"/>
③logImpl

用于设置日志提供商,不配置会自动查找,可用于多日志的情况下日志的选用。比如我们这里配置的是log4j的,那么可以显式地配置。

代码语言:javascript复制
<setting name="logImpl" value="LOG4J"/>

3)typeAliases

该属性标签可用于设置多个类型别名,设置之后对于全类名可以只写类名,但是建议写全类名,比较清晰。可使用 typeAliase 标签对别名进行设置,其中 alias 属性为别名内容(不设置默认为类名) ,type 属性为要用别名替换的内容。

代码语言:javascript复制
<typeAliases>
    <typeAlias alias="Employee" type="top.jtszt.bean.Employee"/>
</typeAliases>

也可以使用 package 标签进行批量别名设置, name属性为包名。设置之后被扫描的包中的实体类别名为类名的小驼峰形式。如果需要制定别名,可以在类上标注 @Alias("xx") 指定。

代码语言:javascript复制
<package name="top.jtszt.bean"/>

Mybatis中别名不区分大小写。

4)typeHandlers

MyBatis 在设置预编译SQL语句中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。

简单地说,我们在使用Mybatis的增删改查时可能有传入参数,那么传入的参数是Java类型,而到数据库中运行就要进行转换,那么这个参数传到预编译SQL之前的转换就是由类型处理器完成的。Mybatis中预提供一些类型处理器:

类型处理器

Java 类型

JDBC 类型

IntegerTypeHandler

java.lang.Integer, int

数据库兼容的 NUMERIC 或 INTEGER

LongTypeHandler

java.lang.Long, long

数据库兼容的 NUMERIC 或 BIGINT

FloatTypeHandler

java.lang.Float, float

数据库兼容的 NUMERIC 或 FLOAT

DoubleTypeHandler

java.lang.Double, double

数据库兼容的 NUMERIC 或 DOUBLE

...

除此之外也可以自定义typeHandler ,只需要实现 TypeHandler 接口, 或继承 BaseTypeHandler类, 并且将它映射到一个 JDBC 类型。其中的实现方法类似 PreparedStatement 的setObject方法,对类型进行转换或者其他处理。

现在我们有一个Info的实体类,包括title和content两个属性,而Employee表中的信息字段为varchar,只显示content内容,因此我们以此实现类型的转换。先写一个转换器类:

代码语言:javascript复制
public class MyTypeHandler implements TypeHandler<Info> {
    @Override
    public void setParameter(PreparedStatement ps, int i, Info info, JdbcType jdbcType) throws SQLException {
        ps.setObject(i, info.getContent());
    }
    @Override
    public Info getResult(ResultSet rs, String s) throws SQLException {
        Info info = new Info();
        info.setContent(rs.getString(s));
        return info;
    }
    @Override
    public Info getResult(ResultSet rs, int i) throws SQLException {
        Info info = new Info();
        info.setContent(rs.getString(i));
        return info;
    }
    @Override
    public Info getResult(CallableStatement cs, int i) throws SQLException {
        Info info = new Info();
        info.setContent(cs.getString(i));
        return info;
    }
}

写完类型处理器类之后配置上它,这里的JavaType 指向的是Java类型,jdbcType指向的是需要转换为的jdbc类型:

代码语言:javascript复制
<typeHandlers>
    <typeHandler handler="top.jtszt.handler.MyTypeHandler" javaType="top.jtszt.entity.Info" jdbcType="VARCHAR"/>
</typeHandlers>

这样我们再运行查询语句,就可以看见被封装好的info字段了。

5)environments

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中。现实中可以将开发、测试和生产环境配上不同的配置(比如配置不同的数据库),我们可以在创建 SqlSessionFactory 的时候指定环境,如果不指定则为默认。在配置文件中可以配置多个环境,但默认的只能有一个。

代码语言:javascript复制
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="url" value="${jdbc.url}"/>
                ...
            </dataSource>
        </environment>
        <environment id="production">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="url" value="${prod.url}"/>
                ...
            </dataSource>
        </environment>
    </environments>
①transactionManager

这里还涉及到 environment 中的一个子标签 <transactionManager> ,这个标签可以指定使用的事务管理器。在 MyBatis 中有两种类型的事务管理器(JDBC、MANAGED),JDBC没什么好说的,而配成MANAGED 则表示从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期,默认情况下它会关闭连接。

②dataSource

它配置的是数据源,可以有三种选择(UNPOOLED、POOLED、JNDI)。

UNPOOLED:数据源的实现会在每次请求时打开和关闭连接,也就是没有连接池;

POOLED:连接池方式的数据源,可以避免创建新连接的消耗;

JNDI:这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用。

而在后续使用Spring整合之后,我们会让Spring进行事务管理,因此这里可不配置数据源以及事务管理器。

6)databaseIdProvider

MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。MyBatis 会加载带有匹配当前数据库 databaseId 属性和所有不带 databaseId 属性的语句。如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。

如果想要配置针对多厂商数据库的支持,则需要在配置文件中加入databaseIdProvider定义:

代码语言:javascript复制
<databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/>
    <property name="DB2" value="db2"/>
    <property name="Oracle" value="oracle" />
</databaseIdProvider>

这样配置以后,只要我们在映射文件中的标签上加上 databaseId 属性即可。

7)mappers

这个属性标签就是指向SQL映射文件的,下面有两个标签,<mapper><package> ,其中mapper标签的 resource 表示直接从类路径下取, class 表示从映射器接口实现类的全类名获取(如果对应映射的是一个接口,那么xml文件也应该在同个包下),url 表示从网络资源/磁盘路径下获取。package标签下的 name 表示将该包内的映射器接口实现全部注册为映射器(如果xml文件在resources目录下,那么需要在该目录下也创建一个同包名的包,将文件置于其中)。

代码语言:javascript复制
<mappers>
    <mapper resource="mapper/EmployeeMapper.xml"/> 
    <mapper class="top.jtszt.mapper.EmployeeMapper"/>
    <package name="top.jtszt.mapper"/> 
</mappers>

4、SQL映射文件

在mapper文件中,最大的就是一个mapper标签,其中 namespace属性为命名空间,指向的是该映射文件对应的接口(对于哪个接口方法的映射/实现)。命名空间的作用是利用更长的全限定名来将不同的语句隔离开来,同时也实现了对于自定义的mapper接口的绑定。也就是说从命名空间中可以看到这个映射文件对应的接口类,并且这里的SQL语句可以依据命名空间隔离开。

代码语言:javascript复制
<mapper namespace="top.jtszt.mapper.EmployeeMapper"></mapper>

接下来就可以了解这里面的子标签了,首先是增删改查标签。

1)select

它的作用就是对查询语句进行映射,基本形式为:

代码语言:javascript复制
<select id="xx">SQL语句</select>
①标签属性

首先是这里的id,指的是“这个SQL语句是对哪个方法的实现”,它在当前命名空间必须是唯一的。

接下来我们重新写一个映射语句,表示 “根据id查询出员工信息”:

代码语言:javascript复制
<select id="getById" parameterType="integer" useCache="true" timeout="5" statementType="PREPARED" resultType="top.jtszt.entity.Employee">
    select * from t_employee where id=#{id}
</select>

parameterType :表示的是传入参数的类型,实际上这个属性不用设置,Mybatis会帮我们推断出类型;•useCache:为true表示将结果保存至二级缓存中,当然它默认值就为true;•flushCache:执行后本地缓存与二级缓存都会被清空•timeout :设置在抛出异常之前驱动程序等待数据库返回请求结果的秒数,默认不设置;•statementType :设置底层使用的Statement类型,默认为PREPARED代表PreparedStatement;•databaseId:数据库厂商标识,这个属性就是搭配全局配置文件中的databaseIdProvider的;•resultType :设置结果集的类型,如果是返回集合,则应该填其中元素的类型;

这里说到的resultType还有一个与它类似的属性 resultMap ,它表示对于外部 resultMap 的命名引用。而resultMap是啥?

现在假设我们数据库表中存在一个passwd字段,而在实体类中我们定义属性为password,这时再进行查询一定会报错,因为Mybatis无法进行自动封装了。

此时就可以写一个resultMap来自定义规则(resultMap为mapper的子标签):

代码语言:javascript复制
<resultMap id="myResultMap" type="top.jtszt.entity.Employee">
    <id property="id" column="id"/>
    <result property="ename" column="ename"/>
    <result property="gender" column="gender"/>
    <result property="email" column="email"/>
    <result property="info" column="content"/>
    <result property="password" column="passwd"/>
</resultMap>

写完自定义规则之后,再将select标签的resultType更换为resultMap,之后再查询就可:

代码语言:javascript复制
<select id="getAll" resultMap="myResultMap">
    select * from t_employee
</select>

这就是select标签中的resultMap的作用,它和resultType是互斥的。

②传参几种情况

1.传入的参数只有一个:取值是 #{随意写} 都可以获取成功;2.传入的参数有多个(1):mybatis会将参数封装到一个map中,所以要取出参数必须按照默认根据索引(0, 1, ...)或者 param1, param2, ... 获取。这种按照位置传参的方式不推荐,只要位置一变就嗝屁了;3.传入的参数有多个(2):推荐在所映射的接口对应参数前加 @Param(参数名) 来指定封装的key,这样在取值时就可以直接使用 #{参数名} 获取。一般多个参数可先封装到类中,再作为参数传递;4.传入的参数是实体类:直接使用 #{属性名} 获取;5.传入的参数是map:直接使用 #{key} 获取。

代码语言:javascript复制
// 假设一个将用户名和id传入,查询出员工信息的接口
Employee getEmpByInfo(@Param("id")Integer id, @Param("ename")String ename);

这里传入多个参数就是用到 @Param来对参数进行显式表示,之后在SQL映射文件直接获取即可。

③ #{ } 和 ${ }

对于获取参数有两种形式,{}不会预编译参数,只是简单拼接,因此它防不了sql注入。#{} 会预编译参数,可以防止sql注入。但是需要注意的是,如果传入的是表名以及order by后的参数值,那么必须用 {}。此外如果是对于属性的引用那么必须写

2) insert、update、delete

这三个标签就是用来表示增删改语句映射的,先看看他们的使用。这里需要注意的是在每次完成操作之后,都需要调用SqlSession的commit方法,将更改提交到数据库中。

①insert

假设现在需要插入一个员工信息,那么先写一个接口:

代码语言:javascript复制
int insertEmployee(Employee employee);

再写映射文件:

代码语言:javascript复制
<!-- 这里只插入两个个字段 -->
<insert id="insertEmployee" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO 
        t_employee
    SET 
        ename=#{ename},
        email=#{email}
</insert>

这里使用到的属性后面会分析,只需要清楚这里是实现插入功能即可。

②update

同样写接口:

代码语言:javascript复制
int updateEmployee(Employee employee);

接着写映射文件:

代码语言:javascript复制
<update id="updateEmployee">
    UPDATE
        t_employee
    SET
        ename=#{ename}
    WHERE
        id=#{id}
</update>
③delete

写接口:

代码语言:javascript复制
int deleteEmployee(Integer id);

写映射文件:

代码语言:javascript复制
<delete id="deleteEmployee">
    DELETE FROM
        t_employee
    WHERE
        id=#{id}
</delete>
④属性分析

由于他们的标签体中的属性大都相同,这里也就归并到一起说,并且只写出与select标签中不一致的属性,相同的就略过了。

flushCache:同样是清缓存,不同的是对于这三个标签,它默认为true,而select为false;•useGeneratedKeys:当设置为true时MyBatis会调用getGeneratedKeys 方法取出由数据库内部生成的主键(如自增id),默认值:false;•keyProperty:用于在insert、update语句中指定主键,MyBatis可以使用getGeneratedKeys的返回值或insert语句的selectKey子元素设置它的值;•keyColumn:用于在insert、update语句中指定主键列,在PostgreSQL等数据库中,当主键列不是表中的第一列的时候必须设置。

当我们的id是主键列并且自增时,可以设置useGeneratedKeys 为true以及 keyProperty 为id,之后可以不用传入id的值,Mybatis会自动帮我们把自增后的id填充上。

假设我们现在要插入一个员工的信息,那么我们可以不传入id的值:

代码语言:javascript复制
<insert id="insertEmployee" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO t_employee(ename,gender,email,content,passwd)
    VALUES(#{ename},#{gender},#{email},#{info},#{password})
</insert>

由于设置了那两个属性,这里的id也是可以自动填充的。

对于不支持自增id的数据库,我们也可以先对id值进行设置,之后再插入:

代码语言:javascript复制
<insert id="insertEmployee">
    <selectKey keyProperty="id" resultType="integer" order="BEFORE" >
        SELECT UNIX_TIMESTAMP(NOW())
    </selectKey>
    INSERT INTO t_employee(id,ename,gender,email,content,passwd)
    VALUES(#{id},#{ename},#{gender},#{email},#{info},#{password})
</insert>

这样同样不需要手动传入id值。

3)sql

这个标签是为了将可重用的sql片段抽取出来,方便复用,这里将需要查询的几个字段抽离出来:

代码语言:javascript复制
<sql id="myStatement"> ${col}.id,${col}.ename,${col}.passwd </sql>

之后在增删改查语句中使用inclue语句引入:

代码语言:javascript复制
<select id="getEmployeeById" resultMap="myResultMap">
    SELECT
    <include refid="myStatement">
        <property name="col" value="t_employee"/>    
    </include>
    FROM t_employee WHERE id=#{id}
</select>

4)resultMap

前面我们使用了这个标签去进行自定义封装,那么现在我们来仔细体会一下它的使用和作用。resultMap 标签中也有多个可用标签,他们也需要按照顺序配置(如果有的话):

1、constructor 2、id 3、result 4、association 5、collection 6、discriminator

①constructor

这个标签用于在实例化类时,将结果注入到构造方法中。它的使用需要有带参数的构造方法,虽然POJO中不允许带有参构造方法。

现在假设实体类中有这样一个构造方法:

代码语言:javascript复制
public Employee(Integer id, String ename) {
    this.id = id;
    this.ename = ename;
}

我们需要在封装对象的时候利用该构造器注入值,那么就可以使用constructor标签:

代码语言:javascript复制
<constructor>
    <idArg column="id" javaType="integer"/>
    <arg column="ename" javaType="String"/>
</constructor>

注意如果是参数为主键则使用的是idArg,column表示对应数据库字段。

也可以name属性搭配 @Param 注解对参数进行自定义:

代码语言:javascript复制
public Employee(@Param("id") Integer id,@Param("empName") String ename) {
    this.id = id;
    this.ename = ename;
}
代码语言:javascript复制
<constructor>
    <idArg column="id" javaType="integer" name="id"/>
    <arg column="ename" javaType="String" name="empName"/>
</constructor>
②id result

id 和 result 标签都将一个列的值映射到一个简单数据类型的属性或字段(非实体类包装),这两者的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。通常主键映射使用的是id,其他字段映射使用result

现在拉取我们之前的实例,这就是它的使用方法:

代码语言:javascript复制
<resultMap id="myResultMap" type="top.jtszt.entity.Employee">
    <id property="id" column="id"/>
    <result property="ename" column="ename"/>
    <result property="gender" column="gender"/>
    <result property="email" column="email"/>
    <result property="info" column="content"/>
    <result property="password" column="passwd"/>
</resultMap>

其中有多个可配置的属性:

property:表示映射到的字段或属性,这里对应javabean中的属性名;

column:表示数据库中的列名或者是别名;

javaType:指定Java类型,如果映射到javabean中可以忽略此属性;

jdbcType:指定JDBC类型,面向 JDBC 编程可填;

typeHandler:指定后可覆盖默认的类型处理器。

③association

现在假设员工表中还有一个字段为 department_id ,而这个部门id是外键,对应的是部门表的id,现在我们需要将员工信息查询出来的同时,把部门信息也查询出来。

首先新建一个部门表:

代码语言:javascript复制
CREATE TABLE t_department(
    id INT PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(50)
);

接着修改员工表,并写一些测试数据:

代码语言:javascript复制
ALTER TABLE t_employee
    ADD COLUMN department_id INT(11) NULL 
        AFTER passwd, 
    ADD CONSTRAINT fk_depid FOREIGN KEY(department_id) 
        REFERENCES t_department(id); 

接着我们修改实体类,添加一个部门实体类,并修改员工实体类。

代码语言:javascript复制
public class Department {
    private Integer id;
    private String name;
    ...
}
public class Employee {
    private Integer id;
    private String ename;
    private Integer gender;
    private String email;
    private Info info;
    private String password;
    private Department department;
    ...
}

现在我们改造一下查询语句,使其可以查询出员工表和部门表的所有信息:

代码语言:javascript复制
<select id="getAll" resultMap="myResultMap">
    SELECT
        e.*,
        d.id dept_id,
        d.name dept_name
    FROM
        t_employee e
    LEFT JOIN
        t_department d
    ON
        e.`department_id`=d.`id`
</select>

此时如果要将department信息封装到employee对象中,就需要用到association:

代码语言:javascript复制
<resultMap id="myResultMap" type="top.jtszt.entity.Employee">
    <!-- 其他配置省略 -->
    <association property="department" javaType="top.jtszt.entity.Department">
        <id column="dept_id" property="id"/>
        <result column="dept_name" property="name"/>
    </association>
</resultMap>

这里有两个属性说明一下:

property:表示映射到的结果,这里填的是employee对象的department属性;•javaType:表示映射到的javabean的全类名,这里填的是department全类名;

此外 association 标签下使用到的这两个标签和resultMap下的id和result表示的意义完全一致。

如果不想使用联合查询,也可以使用分步查询,执行多次sql。使用这种方法首先需要定义一个查询department的select映射,同样使用 association 标签:

代码语言:javascript复制
<association property="department" select="top.jtszt.mapper.DepartmentMapper.getDepartmentById" column="department_id"/>

这里的 select 属性指向查询部门的映射(命名空间 映射id), column 属性指向employee表中的那个外键字段。这样配置以后Mybatis会将查出来的employee的department_id传入getDepartmentById 中,将返回的对象给department属性。

④collection

对于属性中有集合类型的可以使用此标签。现在我们在部门实体类中增加一个属性,用来表示其部门下的全部员工信息:

代码语言:javascript复制
public class Department {
    private Integer id;
    private String name;
    private List<Employee> employeeList;
    ...
}

现在我们想在查询部门的时候把其员工信息也查询出来,就需要用到collection标签:

方式一:

代码语言:javascript复制
<select id="getDepartmentById" resultMap="deptMap">
        SELECT
            e.*,
            d.id dept_id,
            d.name dept_name
        FROM
            t_employee e
        LEFT JOIN
            t_department d
        ON
            e.`department_id`=d.`id`
        WHERE 
            d.id=#{id}
</select>
<resultMap id="deptMap" type="top.jtszt.entity.Department">
    <id property="id" column="dept_id"/>
    <result property="name" column="dept_name"/>
    <collection property="employeeList" ofType="top.jtszt.entity.Employee">
        <id column="id" property="id"/>
        <result column="ename" property="ename"/>
        <result property="gender" column="gender"/>
        <result property="email" column="email"/>
        <result property="info" column="content"/>
        <result property="password" column="passwd"/>
    </collection>
</resultMap>

collection 标签的property 表示映射到的属性名,ofType 表示集合中元素的类型,而在collection标签体内的标签就都是查询employee对象时的resultMap内容了,为了简便还可以直接使用 resultMap 属性引入employee的mapper中的resultMap:

代码语言:javascript复制
<resultMap id="deptMap" type="top.jtszt.entity.Department">
    <id property="id" column="dept_id"/>
    <result property="name" column="dept_name"/>
    <collection property="employeeList" ofType="top.jtszt.entity.Employee" 
                resultMap="top.jtszt.mapper.EmployeeMapper.myResultMap"/>
</resultMap>

方式二:

代码语言:javascript复制
<select id="getDepartmentById" resultMap="deptMap">
        SELECT * FROM t_department WHERE id=#{id}
</select>
<select id="getEmployeeByDeptId" 
        resultMap="top.jtszt.mapper.EmployeeMapper.myResultMap">
    SELECT * FROM t_employee WHERE department_id=#{dept_id}
</select>

<resultMap id="deptMap" type="top.jtszt.entity.Department">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <collection property="employeeList" select="getEmployeeByDeptId" 
                column="id"/>
</resultMap>
⑤discriminator

有时一个SQL可能会返回多个不同的结果集,使用鉴别器(discriminator)元素可以应对这种情况,它类似于 Java 中的 switch 语句。

5、动态SQL

动态SQL就是可以根据不同条件进行SQL的拼接查询,比如现在有一个查询页面,可以按照单条件/多条件进行查询,那么后台的SQL语句就要求做到可根据传入条件数量拼接语句,这时动态SQL就很有用。实现动态SQL有几个标签:if、choose (when, otherwise) 、trim (where, set) 、foreach。

1)if&trim

假设我们现在需要从页面获取查询条件内容,之后写根据不为空的条件进行查询,那么就可以使用if和trim/where标签搭配,先看看和where的搭配。

先写一个接口:

代码语言:javascript复制
List<Employee> getEmployeeByInfo(Employee employee);

再写映射文件:

代码语言:javascript复制
<select id="getEmployeeByInfo" resultMap="myResultMap">
    SELECT * FROM t_employee
    <where>
        <if test=" id != null ">
            AND id=#{id}
        </if>
        <if test=" ename != null and ename.trim()!='' ">
            AND ename=#{ename}
        </if>
        <!-- 其他以此类推 -->
    </where>
</select>

当我们的employee对象是空的时候,可以查询出来所有结果(没有where子句);传入几个参数就按几个参数查,可见where标签的作用是帮我们处理where子句添加以及and去除的问题。而if标签则是根据test中的条件判断标签体是否保留,test判断语句可以使用上OGNL语句。

接下来看看if和trim的搭配,还是同样的接口,修改以下映射文件:

代码语言:javascript复制
<select id="getEmployeeByInfo" resultMap="myResultMap">
    SELECT * FROM t_employee
    <trim prefix="WHERE" suffixOverrides="AND">
        <if test=" id != null ">
            id=#{id} AND
        </if>
        <if test=" ename != null and ename.trim()!='' ">
            ename=#{ename} AND
        </if>
    </trim>
</select>

trim标签可以有四个属性设置:prefix 表示在整个标签体之前加的内容; suffix 表示在整个标签之后加的内容;prefixOverrides 表示将标签体内首部的该内容去掉;suffixOverrides 表示将标签体内尾部的该内容去掉。

因此这里是代表在首部加上WHERE,而如果标签体尾部出现AND则去掉,这样也可以满足要求。

2)choose

这个就有点像switch...case了,其实就是实现条件的选择,我们还是假设对于员工信息的查询,但是现在需求变了,变成根据单条件查询了,哪个有就查哪个,还是借助刚才的接口,直接重新写映射文件即可。

代码语言:javascript复制
<select id="getEmployeeByInfo" resultMap="myResultMap">
    SELECT * FROM t_employee
    <choose>
        <when test="id!=null">
            WHERE id=#{id}
        </when>
        <when test="ename!=null and ename.trim()!=null">
            WHERE ename=#{ename}
        </when>
        <otherwise></otherwise>
    </choose>
</select>

when即相当于case,test同样是判断条件,里面可写的内容与if的test一致;otherwise表示默认执行的内容,相当于default。

3)foreach

现在需求又变了,只有id字段作为查询字段,但是它可以传入多个值,需要根据这组值查出符合条件的信息列表。实际开发中也常见,例如查询指定几个日期订单信息等。

如果需要遍历传入的条件时就需要用到foreach,按照这个需求,我们先写一个接口:

代码语言:javascript复制
List<Employee> getEmployeeByIds(@Param("ids")List<Integer> ids);

这里使用@Param 来自定义参数名,接着写映射文件:

代码语言:javascript复制
<select id="getEmployeeByIds" resultMap="myResultMap">
    SELECT * FROM t_employee
    <trim prefix="WHERE" >
        id IN
        <foreach collection="ids" item="ids_item" 
                 separator="," open="(" close=")">
            ${ids_item}
        </foreach>
    </trim>
</select>

这里需要注意foreach的几个属性,其中collection 代表的是传入的集合的参数名,我们前面使用@Param 指定了;item 配一个变量名,表示每次遍历出的值;separator 表示每次遍历出来的值用什么风格,由于是in语句就用了逗号;open 表示遍历开始前的字符;close 表示遍历结束的字符。

4)set

在进行update操作时,使用上set标签可以减少一定的工作量。

代码语言:javascript复制
<update id="updateEmployee">
    UPDATE t_employee
    <set>
        <if test="ename != null">ename=#{ename},</if>
        <if test="email != null">email=#{email},</if>
    </set>
    WHERE id=#{id}
</update>

在update中使用set标签,它会自动帮我们加上SET前缀,并且抹去最后一个逗号。

6、基于注解配置

前面使用的配置都是基于xml的配置,实际上Mybatis3之后还提供了基于注解的配置,但是这种配置方式的表达能力和灵活性十分有限,甚至可以说复杂SQL根据注解来进行配置简直是灾难,不过简单语句配置利用注解确实很方便。

使用注解进行配置时我们只需要保留全局配置文件即可,并提供包含查询方法定义的接口类。

代码语言:javascript复制
public interface EmployeeMapperByAnnotation {
    Employee getEmployeeById(Integer id);

    int insertEmployee(Employee employee);

    int deleteEmployee(Integer id);

    int updateEmployee(Employee employee);

    List<Employee> getAll();
}

1)@Select

①基本查询

首先是用于查询的注解 @Select ,先看它的用法:

代码语言:javascript复制
@Select("SELECT * FROM t_employee")
List<Employee> getAll();

这样就相当于在mapper配置文件中写了一个<select> ,这样是可以查询出基本字段的,但实际上那些我们之前在resultMap中定义过的复杂的映射是无法完成的。

②带参
代码语言:javascript复制
@Select("SELECT * FROM t_employee WHERE id=#{id}")
Employee getEmployeeById(Integer id);

这样也可以完成根据id的查询。说到按条件查询,这里是固定条件的查询,可如果是动态SQL的编写,也就是说条件不确定的情况下,使用注解要去拼接 <script> 写成脚本的形式,这种方式太...了,这里也不写了,就提一嘴。

③结果集映射

上面我们也看到了,查询出来的有关自定义类型映射的都不能正确显示,在xml中我们使用<resultMap> 去配置,在注解中也可以进行配置:

代码语言:javascript复制
@Select("SELECT * FROM t_employee")
@Results(id = "myMap", value = {
    @Result(id = true, property = "password", column = "passwd"),
    @Result(property = "info", column = "content")
})
List<Employee> getAll();

这里的 @Results 相当于 <resultMap> 标签,它的id属性即为对该结果集映射的标识,value相当于标签体。而@Result 就相当于 <result> ,当id为true时,这个注解也相当于 <id>

注解配置的结果集也可以复用,我们可以使用 @ResultMap 注解来引用:

代码语言:javascript复制
@Select("SELECT * FROM t_employee WHERE id=#{id}")
@ResultMap("myMap")
Employee getEmployeeById(Integer id);

此外还有其他的一些注解可以和xml配置中的标签对应上,但Mybatis不建议使用注解完成复杂的SQL映射编写。

2)@Insert 、@Update、@Delete

先针对增删改注解写一个简单的实例:

代码语言:javascript复制
//插入
@Insert("INSERT INTO t_employee SET ename=#{ename},email=#{email}")
int insertEmployee(Employee employee);

//更新
@Update("UPDATE t_employee SET ename=#{ename},email=#{email} WHERE id=#{id}")
int updateEmployee(Employee employee);

//删除
@Delete("DELETE FROM t_employee WHERE id=#{id}")
int deleteEmployee(Integer id);

之前我们还配置了id的自增,这里也可以使用,需要使用到 @Options 注解,这个注解可以填充大部分的属性,之后将useGeneratedKeys以及keyProperty补充上即可。

代码语言:javascript复制
@Insert("INSERT INTO t_employee SET ename=#{ename},email=#{email}")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertEmployee(Employee employee);

这里再提一下 @Options 注解,里面可设置的属性实际上就等同于xml配置标签中的属性,需要使用到点进去方法查看即可。

3)Provider系列

Provider 型注解允许指定返回 SQL 语句的类和方法,以供运行时执行,这类注解有两个属性:type 代表一个提供 SQL 的类,method代表一个提供 SQL 的方法,这个方法需要有一个 String 类型的返回值,参数可以根据实际情况定义。

使用这个注解之前,我们先准备一个Provider类,针对查询所有信息写一个方法:

代码语言:javascript复制
public class EmployeeProvider {
    public String getAll(){
        return null;
    }
}

然后将EmployeeMapper接口的注解修改为@SelectProvider:

代码语言:javascript复制
@SelectProvider(type = EmployeeProvider.class, method = "getAll")
List<Employee> getAll();

这里的type指向我们自定义的Provider类,method指向对应的方法。之后就可以在getAll方法体内写内容了:

代码语言:javascript复制
public String getAll(){
    SQL sql = new SQL();
    sql.SELECT("*").FROM("t_employee2");
    return sql.toString();
}

这里直接调用了MyBatis提供的 API 构建出 SQL ,正是由于使用上了API,因而这里对于动态SQL也是可以写的,多加几个if语句然后调用AND方法以及WHERE方法等就可以实现,尽管这种方式写出来的方法后续有变动需要重新编译。此外还有InsertProvider、UpdateProvider、DeleteProvider,这些的用法都跟上面的实例差不多。

7、缓存

Mybatis中提供了强大的事务性查询缓存机制,在这里缓存也分为一级缓存和二级缓存。

1)一级缓存

①基本使用

Mybatis中的一级缓存比较常用,并且它是默认开启的,一级缓存基于 SqlSession ,默认情况下它会自动开启事务,所以一级缓存会自动使用。

假设我们现在在同一个sqlSession中进行两次相同的查询,那么第二次查询是不会重新去数据库中查询的,因为第一次查询之后的结果被存储到了一级缓存中。

从日志我们可以看到,从第一次出结果到第二次出结果中间是不会有新的SQL查询发起的。

②一级缓存失效

一级缓存在遇到一些特定的情况也会失效,失效之后像上面那种情况就需要重新发SQL查询了。一级缓存失效的场景主要有:

•跨sqlSession:因为一级缓存本来就是基于sqlSession的,所以不同的sqlSession之间一级缓存肯定不能相通,因而也就会出现这种失效情况;•混入增删改:如果在两次查询之间有了增删改操作,那么一级缓存会失效,我们之前将这三个标签的flushCache属性时也看到了它默认是true;•调clearCache:sqlSession中有一个方法 clearCache, 调用它会清空一级缓存

下图为没有一级缓存的情况:

2)二级缓存

二级缓存即是全局作用域的缓存,Mybatis提供二级缓存的接口和实现,缓存的实现要求实体类实现Serializable接口,并且二级缓存只有在sqlSession关闭/提交之后才会生效。

在Mybatis中二级缓存是默认开启的,关闭需要在全局配置文件中进行操作:

代码语言:javascript复制
<settings>
    <setting name="cacheEnabled" value="false"/>
</settings>

如果是xml配置则在需要使用二级缓存的mapper文件中写入cache标签,如果是基于注解配置的,就在mapper接口上标注 @CacheNamespace。这样即使我们使用两个不同的sqlSession执行相同的SQL,第二个也是刻意直接从缓存中取结果的。

这里关注一下cache标签中的几个属性:

eviction:表示缓存的回收(清除)策略,默认为LRU,可选项为FIFO/SOFT/WEAK;

type:缓存的实现,默认为本地缓存,整合第三方缓存时需要修改它;

size:缓存引用的数量,默认1024;

flushInterval:缓存定时清除时间,默认无;

readOnly:设置为true则查询后返回的对象为基于序列化的深拷贝,效率会降低但安全;

blocking:如果为true则当找不到对应数据时会一直阻塞直到有对应数据进来。

此外这里的缓存还可以整合第三方的缓存EhCache ,步骤就是导EhCache依赖,写EhCache 配置文件(官网copy即可),修改cache标签的type属性,这样就大功告成了,这里就不细写了。


参考资料:

•MyBatis 3.5.7官方文档[1]•玩转 MyBatis:深度解析与定制-LinkedBear-掘金小册[2]•MyBatis (JavaGuide)[3]•雷丰阳Spring、Spring MVC、MyBatis课程-bilibili[4]

相关链接

[1] MyBatis 3.5.7官方文档: https://mybatis.org/mybatis-3/zh/ [2] 玩转 MyBatis:深度解析与定制-LinkedBear-掘金小册: https://juejin.cn/book/6944917557878980638 [3] MyBatis (JavaGuide): https://snailclimb.gitee.io/javaguide-interview/#/./docs/e-2mybatis [4] 雷丰阳Spring、Spring MVC、MyBatis课程-bilibili: https://www.bilibili.com/video/BV1d4411g7tv?p=256

0 人点赞