浅谈Mybatis

MyBatis 是轻量级的对 JDBC 进行半封装 的 ORM 框架。

  • 半封装 :相较 Hibernate 的 全封装 ,需要手动编写 sql 。

  • ORM :(Object Relational Mapping,对象关系映射),将 Java对象 和 接口 映射成数据库中的记录。

本 wiki 基于 springboot 2.x 和 mysql 编写。

核心配置

pom.xml

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>

*Application.java

@MapperScan 用于扫描 DAO层 interface ,并生成对应的代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package tk.gushizone.mybatis;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(basePackages = {"tk.gushizone.**.dao"})
public class MyBatisApplication {

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

application.yml

1
2
mybatis:
mapper-locations: classpath*:tk/gushizone/**/dao/*.xml,classpath*:mapper/*.xml

接口式编程

Mybatis 提供了面向接口的编程方式,只需要 interface + sql 就可以使用面向对象的方式操作数据库。

*Mapper.java

多个入参时需要包装或使用 @Param 注解,因为 java 编译后不会保存形参名。

1
2
3
4
5
6
7
8
9
package tk.gushizone.mybatis.dao;

import tk.gushizone.mybatis.pojo.Message;

public interface MessageMapper {

Message selectByPrimaryKey(Integer id);

}

*Mapper.xml

  • mybatis 不允许重载,也不会校验参数,可以省略参数属性。
  • 结果集列名会和entity映射,当表连接时避免同名冲突需要取别名(a.id, b.id 都是id,JDBC返回结果不会带前缀)。
  • <![CDATA[ ]]> 包裹的文本会被 xml解析器 忽略,不需要转义。
  • mybatis 不允许在 sql 中使用 ; 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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="tk.gushizone.mybatis.dao.MessageMapper">

<resultMap id="BaseResultMap" type="tk.gushizone.mybatis.pojo.Message">
<id column="ID" property="id" jdbcType="INTEGER"/>
<result column="COMMAND" property="command" typeHandler="tk.gushizone.mybatis.enumeration.CodeEnumTypeHandler"/>
<result column="DESCRIPTION" property="description" jdbcType="VARCHAR"/>
<result column="CONTENT" property="content" jdbcType="VARCHAR"/>
<result column="IS_DELETED" property="isDeleted" jdbcType="BIT"/>
<result column="CREATE_TIME" property="createTime" jdbcType="TIMESTAMP"/>
<result column="UPDATE_TIME" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>

<sql id="Base_Column_List">
ID, COMMAND, `DESCRIPTION`, CONTENT, IS_DELETED, CREATE_TIME, unix_timestamp(UPDATE_TIME) as UPDATE_TIME
</sql>

<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select
<include refid="Base_Column_List"/>
from message
where ID = #{id,jdbcType=INTEGER}
</select>

</mapper>

*pojo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package tk.gushizone.mybatis.pojo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.ibatis.type.EnumOrdinalTypeHandler;
import org.apache.ibatis.type.EnumTypeHandler;
import tk.gushizone.mybatis.enumeration.CodeEnumTypeHandler;
import tk.gushizone.mybatis.enumeration.CommandEnum;

import java.time.LocalDateTime;

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

private Integer id;

private CommandEnum command;

private String description;

private String content;

private Boolean isDeleted;

private LocalDateTime createTime;

private Long updateTime;

}

测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package tk.gushizone.mybatis.test;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import tk.gushizone.mybatis.MyBatisApplication;
import tk.gushizone.mybatis.dao.MessageMapper;
import tk.gushizone.mybatis.pojo.Message;

import javax.annotation.Resource;
import java.util.List;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MyBatisApplication.class)
public class MyBatisApplicationTest {

@Resource
private MessageMapper messageMapper;

@Test
public void testRetrieve() {
Message message = messageMapper.selectByPrimaryKey(1);
log.warn(message.toString());
}

}

常用标签

mapper中支持的标签

功能 标签名称
定义 SQL 语句 insert / delete / update / select
配置 java对象 属性与查询 结果集 列名对应关系 resultMap
遍历与逻辑 foreach / if / choose
格式化 where / set / trim
配置关联关系 collection / association
定义与引用常量 sql / inxlude

foreach

遍历集合

对于数array和list,index表示索引。

1
2
3
<foreach collection="array" index="i" item="item"></foreach>

<foreach collection="list" index="i" item="item"></foreach>

对于map,index表示key。

1
<foreach collection="map" index="key" item="item"></foreach>

choose

和 带break的switch 用法相同,也可以起到 if/else 作用。

1
2
3
4
5
<choose>
<when test=""></when>
<when test=""></when>
<otherwise></otherwise>
</choose>

trim

动态添加和忽略前后缀

1
2
<!-- 相当于set标签 -->
<trim prefix="set" suffixOverrides=","></trim>
1
2
<!-- 相当与where标签 -->
<trim prefix="where" prefixOverrides="and/or"></trim>

collection

一对多,主表对应子表。

mybatis 一对多,会自动去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package tk.gushizone.mybatis.pojo;

import lombok.Data;

import java.util.Date;
import java.util.List;

@Data
public class Command {

private Integer id;

private String name;

private String description;

private Date createTime;

private Date updateTime;

private List<CommandContent> commandContents;
}
1
2
3
4
5
6
7
8
<resultMap id="CommandWithContentResultMap" type="tk.gushizone.mybatis.pojo.Command" >
<id column="ID" property="id" jdbcType="INTEGER" />
<result column="NAME" property="name" jdbcType="VARCHAR" />
<result column="DESCRIPTION" property="description" jdbcType="VARCHAR" />
<result column="CREATE_TIME" property="createTime" jdbcType="TIMESTAMP" />
<result column="UPDATE_TIME" property="updateTime" jdbcType="TIMESTAMP" />
<collection property="commandContents" resultMap="CommandContentResultMap" />
</resultMap>

association

一对一/多对一,子表对应主表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package tk.gushizone.mybatis.pojo;

import lombok.Data;

import java.util.Date;

@Data
public class CommandContent {

private Integer id;

private String content;

private Integer commandId;

private Date createTime;

private Date updateTime;

private Command command;
}
1
2
3
4
5
6
7
8
<resultMap id="ContentWithCommandResultMap" type="tk.gushizone.mybatis.pojo.CommandContent" >
<id column="ID" property="id" jdbcType="INTEGER" />
<result column="CONTENT" property="content" jdbcType="VARCHAR" />
<result column="COMMAND_ID" property="commandId" jdbcType="INTEGER" />
<result column="CREATE_TIME" property="createTime" jdbcType="TIMESTAMP" />
<result column="UPDATE_TIME" property="updateTime" jdbcType="TIMESTAMP" />
<association property="command" resultMap="CommandResultMap" />
</resultMap>

sql / include

定义常量与引用常量

1
2
3
4
5
6
7
8
9
10
<sql id="Base_Column_List">
ID, COMMAND, `DESCRIPTION`, CONTENT, IS_DELETED, CREATE_TIME, unix_timestamp(UPDATE_TIME) as UPDATE_TIME
</sql>

<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer">
select
<include refid="Base_Column_List"/>
from message
where ID = #{id,jdbcType=INTEGER}
</select>

常用属性

属性 说明
parameterType 接受参数映射,接受 java类型 ,一般为 参数类型 或 java.utl.Map 。
parameterMap 接受参数映射,可以通过 parameterMap标签 配置。 官方不推荐使用。
resultType 接受查询结果映射,接受 java类型 ,一般为 返回值类型 或 java.utl.Map 。
resultMap 接受查询结果映射,可以通过 resultMap标签 配置。

OGNL

mapper 配置文件的 标签属性 中支持 OGNL表达式 ,这里是列举 MyBatis 中常用到的 OGNL表达式

OGNL 支持使用 java方法

注意在标签语言中需要转义字符。

1
2
3
4
/** java */
str != null && !"".equals(str.trim())
/** OGNL */
str != null and !''.equals(str.trim())
操作符 操作符(注意在标签语言中需要转义字符,如: && 等)
支持java的操作符 + 、 - 、 * 、 / 、 == 、 != 、 ` 、&&` 等
特有的操作符 and 、 or 、 mod 、 in 、 not in

基本取值

  • 单一入参下,mybatis 提供了默认的取值方式,可以使用 @Param 取别名。

  • 多个入参时需要包装或使用 @Param 注解,因为 java 编译后不会保存形参名。

单入参类型 取值方式 说明
String 与基本类型 _parameter 当只有一个参数时 #{_parameter} 中 _parameter 可以是任意值,一般用参数名。
Map / 自定义包装类型 key / 属性名 若参超过 5 个,推荐使用这种方式。
Collection collection 相应的 Array 使用 array , List 也可以使用 list 。

#{} & ${}

取值符 是否存在 sql 注入风险 说明
#{} 否 sql 语句中的 #{} 并不是 ognl ,其由 mybatis 处理,类似 预备语句 :1) #{} 会被替换为 ? ;2) 根据 ? 的次序,会被填入对应的参数,并加上 '' 。
${} 是 ${} 会被直接替换为指定字符串,并且不会添加 '' 。

常见问题

  • 必须的无参构造器 :因为 mybatis 使用反射动态生成对象实例,必须含有无参构造器。

模糊查询

mysql

对于 mysql ,可以使用两种方式进行模糊查询。

若使用中文搜索,确保 spring.datasource.url 配置了 characterEncoding=utf8 。

1
2
3
4
5
6
7
8
9
10
<select id="selectBySearch" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from message
<where>
`description` like CONCAT('%',#{search},'%')
OR
content like '%' #{search} '%'
</where>
</select>

oracle

1
2
3
<select id="searchUserBySearchName" parameterType="java.lang.String" resultType="org.demo.entity.User">
select * from t_user where user_name like '%'||#{search_name}||'%'
</select>

sqlserver

1
2
3
<select id="searchUserBySearchName" parameterType="java.lang.String" resultType="org.demo.entity.User">
select * from t_user where user_name like '%'+#{search_name}+'%'
</select>

获取自增主键

*Mapper.java

1
int insert(Message record);

*Mapper.xml

  • useGeneratedKeys="true" : 启用自增主键。
  • keyProperty="id" : 指定自增主键赋值属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into message (COMMAND,
`DESCRIPTION`,
CONTENT,
IS_DELETED,
CREATE_TIME,
UPDATE_TIME)
values (#{command.code,jdbcType=TINYINT},
#{description,jdbcType=VARCHAR},
#{content,jdbcType=VARCHAR},
#{isDeleted,jdbcType=BIT},
#{createTime,jdbcType=TIMESTAMP},
from_unixtime(#{updateTime}))
</insert>

测试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testCreate() {
Message newMessage = Message.builder()
.command(CommandEnum.JOKE)
.description("段子描述新增")
.content("段子内容新增")
.isDeleted(false)
.createTime(LocalDateTime.now())
.updateTime(System.currentTimeMillis() / 1000)
.build();

int row = messageMapper.insert(newMessage);
log.warn("操作记录数:{}, 自增主键:{}。", row, newMessage.getId());
}
1
操作记录数:1, 自增主键:7。

枚举映射

mybatis 提供了默认的枚举处理器,但一般无法满足需求,需要自定义类型处理器。

默认枚举处理器 说明
EnumTypeHandler name() 作为取值。
EnumOrdinalTypeHandler ordinal() 作为取值。

*BaseEnum.java

定义基础属性接口作为共通映射。

1
2
3
4
5
package tk.gushizone.mybatis.enumeration;

public interface BaseCodeEnum {
Integer getCode();
}

*Enum.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package tk.gushizone.mybatis.enumeration;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
@AllArgsConstructor
public enum CommandEnum implements BaseCodeEnum{

JOKE(0, "段子"),
NEWS(1, "新闻"),
ENTERTAINMENT(2, "娱乐"),
MOVIE(3, "电影"),
LOTTERY(4, "彩票"),;

private Integer code;
private String desc;
}

*TypeHandle.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package tk.gushizone.mybatis.enumeration;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
*
* @author gushizone@gmail.com
* @date 2019-12-26 15:39
*/
public class CodeEnumTypeHandler<E extends BaseCodeEnum> extends BaseTypeHandler<BaseCodeEnum> {

private Class<E> type;

public CodeEnumTypeHandler(Class<E> typeHandlerClass) {
if (typeHandlerClass == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = typeHandlerClass;
}

/**
* 用于定义设置参数时,该如何把Java类型的参数转换为对应的数据库类型
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, BaseCodeEnum parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter.getCode());
}

/**
* 用于定义通过字段名称获取字段数据时,如何把数据库类型转换为对应的Java类型
*/
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
Integer code = rs.getInt(columnName);
return rs.wasNull() ? null : codeOf(code);
}

/**
* 用于定义通过字段索引获取字段数据时,如何把数据库类型转换为对应的Java类型
*/
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Integer code = rs.getInt(columnIndex);
return rs.wasNull() ? null : codeOf(code);
}

/**
* 用定义调用存储过程后,如何把数据库类型转换为对应的Java类型
*/
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Integer code = cs.getInt(columnIndex);
return cs.wasNull() ? null : codeOf(code);
}

private E codeOf(Integer code){
for (E baseEnum : type.getEnumConstants()) {
if (baseEnum.getCode().equals(code)) {
return baseEnum;
}
}
return null;
}
}

*Mapper.xml

1
<result column="COMMAND" property="command" typeHandler="tk.gushizone.mybatis.enumeration.CodeEnumTypeHandler"/>