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
实例,这是应用的核心。配置文件的样例在官方文档中也有给出:
<?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}
进行值绑定。
<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
,
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}
取出对应属性的值。
<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
属性为要用别名替换的内容。
<typeAliases>
<typeAlias alias="Employee" type="top.jtszt.bean.Employee"/>
</typeAliases>
也可以使用 package
标签进行批量别名设置, name
属性为包名。设置之后被扫描的包中的实体类别名为类名的小驼峰形式。如果需要制定别名,可以在类上标注 @Alias("xx")
指定。
<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
的时候指定环境,如果不指定则为默认。在配置文件中可以配置多个环境,但默认的只能有一个。
<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
定义:
<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目录下,那么需要在该目录下也创建一个同包名的包,将文件置于其中)。
<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语句可以依据命名空间隔离开。
<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}
获取。
// 假设一个将用户名和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
注解对参数进行自定义:
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
标签:
<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:
<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
来自定义参数名,接着写映射文件:
<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
,先看它的用法:
@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>
去配置,在注解中也可以进行配置:
@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
注解来引用:
@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补充上即可。
@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