浅谈Mybatis

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

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

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

本 wiki 基于 springboot 2.xmysql 编写。

核心配置

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的操作符 +-*/==!= 、 ` &&` 等
特有的操作符 andormodinnot 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"/>