MyBatis核心之映射文件编写

MyBatis核心之映射文件编写

一、MyBatis的开发环境介绍

本次案例中使用的还是上一节说的表t_user及其对应的实体类User,有必要先说明User类中的方法都附有打印信息以供理解MyBatis的运行原理,同时为了方便我们写了一个简单的工具类SqlSessionUtil

1
2
3
4
5
6
7
8
9
10
11
public class SqlSessionUtil {
public static SqlSession getSqlSession() {
try {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
return factory.openSession(true);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

image-20231124161323978

二、MyBatis的参数解析

1.测试方法

我们先来看一下UserMapper接口,其中包含了参数解析的若干个测试方法,我们下面一一查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface UserMapper {
// 方法无参数
List<User> getAllUsers();

// 方法有一个简单参数
User getUserById(Integer id);

// 方法有两个或多个简单参数
User checkUser(String username, String password);

// 方法参数是Map类型
User checkUserByMap(Map<String, Object> map);

// 方法参数是实体类类型
int insertUser(User user);

// 方法参数由Param注解标注
User checkUserByParam(@Param("username") String username, @Param("password") String password);
}

2.语句编写

重点内容是对应的UserMapper.xml如何书写,我们先给出答案,后面我们会逐个解释并测试:

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huling.mapper.UserMapper">
<select id="getAllUsers" resultType="user">
select * from t_user;
</select>

<!--user是MyBatis配置文件中设置的类型别名,对应于com.huling.pojo.User-->
<!--#{}和${}里面填写的属性值有一定规则限制-->
<select id="getUserById" resultType="user">
select * from t_user where id = #{id}
</select>

<select id="checkUser" resultType="user">
<!--select * from t_user where username = #{arg0} and password = #{arg1}-->
select * from t_user where username = '${param1}' and password = '${param2}'
</select>

<select id="checkUserByMap" resultType="user">
select * from t_user where username = #{username} and password = #{password}
</select>

<!--#{}和${}里面填写实体类的属性名(不是字段名)并使用Getter方法获取属性值-->
<insert id="insertUser">
insert into t_user values (null, #{username}, #{password}, #{age}, #{sex}, #{a})
</insert>

<select id="checkUserByParam" resultType="user">
select * from t_user where username = #{username} and password = #{password}
</select>
</mapper>

3.答案解析

对于getAllUsers方法来说,直接查询出t_user表中的所有记录并转换为Java中的User对象,这都是通过无参构造函数+Setter方法完成的,前面一节已经说过这里不多阐述,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* MyBatis配置文件使用别名包和mapper映射文件包
*/
@Test
public void test1() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.getAllUsers();
users.forEach(System.out::println);
}
}

image-20231124163133022

对于getUserById方法来说,SQL语句中获取方法参数不仅可以使用#{id},任何有效字符例如#{aaa}#{x}都可以,不过为了便于理解和维护,我们还是建议见名知意,最好使用方法参数名id(其实我们通过Param注解强制参数的键为id),测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 测试参数化查询:
* 1.${}本质是字符串拼接,存在SQL注入风险
* 2.#{}本质是预编译SQL和参数化占位符
*/
@Test
public void test2() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.getUserById(2);
System.out.println(user);
}
}

image-20231124163830236

对于checkUser方法来说,需要两个方法参数,这两个参数的名称只能是[arg0, arg1, param1, param2],其他任何名称都不行(其实我们建议通过Param注解强制参数的键为参数名usernamepassword),测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 测试参数化查询:
* 1.${}本质是字符串拼接,存在SQL注入风险
* 2.#{}本质是预编译SQL和参数化占位符
*/
@Test
public void test2() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User tom = mapper.checkUser("tom", "123456");
System.out.println(tom);
}

image-20231124164330133

对于checkUserByMap方法来说,不需要MyBatis帮我们把参数对封装为Map结构供参数解析使用,直接使用传入的Map对象即可,因此也只能使用Map对象的键作为参数,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 测试参数化查询:
* 1.${}本质是字符串拼接,存在SQL注入风险
* 2.#{}本质是预编译SQL和参数化占位符
*/
@Test
public void test2() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("username", "tom");
map.put("password", "123456");
User tom = mapper.checkUserByMap(map);
System.out.println(tom);
}

image-20231124164754545

对于insertUser方法来说,方法参数是实体类对象的属性,注意这里的属性并不是指实体类的某个字段,而是Getter方法表示的属性(首字母小写),例如下面User类具有属性a

image-20231124165856572

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 测试参数化查询:
* 1.${}本质是字符串拼接,存在SQL注入风险
* 2.#{}本质是预编译SQL和参数化占位符
*/
@Test
public void test2() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
int result = mapper.insertUser(new User(null, "admin", "123456", 22, "男", "admin@163.com"));
System.out.println("result = " + result);

}
}

image-20231124165946407

对于checkUserByParam方法来说,方法参数均通过Param注解强制标注其参数名称,这也是我们以后推荐的方式,有利于理解和维护,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 测试参数化查询:
* 1.${}本质是字符串拼接,存在SQL注入风险
* 2.#{}本质是预编译SQL和参数化占位符
*/
@Test
public void test2() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User tom = mapper.checkUserByParam("tom", "123456");
System.out.println(tom);
}
}

image-20231124170224987

4.最佳实践

  • 如果方法中包含若干个简单参数,则使用@Param注解写明参数名称
  • 如果方法中包含Map类型参数,则按照Map的键名称获取参数(通过get方法)
  • 如果方法中包含实体类类型参数,则按照实体类的属性名称获取参数(通过Getter方法)

三、MyBatis的返回值封装

1.测试方法

我们先来看一下SelectMapper接口,其中包含了返回值封装的若干个测试方法,我们下面一一查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface SelectMapper {
// 通过Java实体对象接收单条数据
User getUserById(@Param("id") Integer id);

// 通过Java实体对象接收多条数据
List<User> getAllUsers();

// 接收单个数据
int getCount();

// 通过Map对象接收单条数据
Map<String, Object> getUserByIdToMap(@Param("id") Integer id);

// 通过Map对象接收多条数据
List<Map<String, Object>> getAllUsersToMap();
}

2.语句编写

重点内容是对应的SelectMapper.xml如何书写,我们先给出答案,后面我们会逐个解释并测试:

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huling.mapper.SelectMapper">
<select id="getUserById" resultType="user">
select * from t_user where id = #{id}
</select>

<!--实体类属性与SQL语句字段一致,需要使用实体类的Setter方法-->
<select id="getAllUsers" resultType="user">
select * from t_user;
</select>

<!--MyBatis内置了很多类型别名-->
<select id="getCount" resultType="_int">
select count(*) from t_user;
</select>

<!--当没有实体类能够对应接收所有字段时例如复杂的多表查询结果,就需要使用Map集合接收结果-->
<select id="getUserByIdToMap" resultType="map">
select * from t_user where id = #{id}
</select>

<select id="getAllUsersToMap" resultType="map">
select * from t_user;
</select>
</mapper>

3.答案解析

对于getUserById方法来说,需要返回单个User对象,这里直接使用resultType是因为数据库的表字段与实体类的字段名一致,不需要我们额外的映射处理工作,MyBatis会直接使用实体类的Setter方法完成返回值的封装,测试代码如下:

1
2
3
4
5
6
7
8
9
10
/**
* 测试MyBatis的各种查询功能,分为接收一条数据或多条数据
*/
@Test
public void test3() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SelectMapper mapper = session.getMapper(SelectMapper.class);
System.out.println(mapper.getUserById(2));
}
}

image-20231124172129335

对于getAllUsers方法来说,需要返回多个User对象,因此使用List类型,其他跟上面的方法没什么不同,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 测试MyBatis的各种查询功能,分为接收一条数据或多条数据
*/
@Test
public void test3() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SelectMapper mapper = session.getMapper(SelectMapper.class);
List<User> users = mapper.getAllUsers();
users.forEach(System.out::println);
}
}

image-20231124172451066

对于getCount方法来说,返回Java内建的基本数据类型int,我们在前面XML配置一节中说到MyBatis默认已经配置好了很多Java内建类型的别名,例如对于基本数据类型int的别名就是_int、Map的别名就是map等等,测试代码如下:

1
2
3
4
5
6
7
8
9
10
/**
* 测试MyBatis的各种查询功能,分为接收一条数据或多条数据
*/
@Test
public void test3() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SelectMapper mapper = session.getMapper(SelectMapper.class);
System.out.println(mapper.getCount());
}
}

image-20231124172748762

对于getUserByIdToMap方法来说,查询的每一行记录会被封装为一个Map对象,其中记录的字段名为Map的键,记录的字段值为Map的值,这在日常开发场景下也是经常使用的,因为对于没有实体类与查询结果匹配的方法,采用Map不妨是一种不错的手段,测试代码如下:

1
2
3
4
5
6
7
8
9
10
/**
* 测试MyBatis的各种查询功能,分为接收一条数据或多条数据
*/
@Test
public void test3() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SelectMapper mapper = session.getMapper(SelectMapper.class);
System.out.println(mapper.getUserByIdToMap(4));
}
}

image-20231124173202666

对于getAllUsersToMap方法来说,跟上面的getUserByIdToMap类似,只不过是返回多行记录,因此使用List类型,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 测试MyBatis的各种查询功能,分为接收一条数据或多条数据
*/
@Test
public void test3() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SelectMapper mapper = session.getMapper(SelectMapper.class);
List<Map<String, Object>> mapList = mapper.getAllUsersToMap();
mapList.forEach(System.out::println);
}
}

image-20231124173425751

4.最佳实践

  • 如果返回值是简单类型,需要根据MyBatis的默认别名填写在resultType标签中
  • 如果返回值是实体类类型,根据数据库表记录字段名与实体类属性名是否一致、是否存在多对一或者一对多关系等决定是否采用resultMap
  • 如果返回值是Map类型,直接使用SQL查询语句的字段名作为键,字段值作为值形成Map对象返回,对于复杂查询例如多表联查时经常会使用到

四、MyBatis的特殊注意

1.测试方法

我们先来看一下SQLMapper接口,其中包含了需要特殊注意的若干个测试方法,我们下面一一查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface SQLMapper {
// 模糊查询:搜素场景
List<User> getUsersLike(@Param("username") String username);

// 批量删除:复选删除等场景
int deleteMore(@Param("ids") String ids);

// 动态设置表名:表的水平切分场景
List<User> getUsersByTableName(@Param("tableName") String tableName);

// 获取自增主键:多表关联等场景
void insertUser(User user);
}

2.语句编写

重点内容是对应的SQLMapper.xml如何书写,我们先给出答案,后面我们会逐个解释并测试:

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huling.mapper.SQLMapper">

<select id="getUsersLike" resultType="User">
<!--select * from t_user where username like '%${username}%'-->
<!--select * from t_user where username like concat('%',#{username},'%')-->
select * from t_user where username like "%"#{username}"%"
</select>

<delete id="deleteMore">
<!--delete from t_user where id in (${ids})-->
delete from t_user where id in (#{ids})
</delete>

<select id="getUsersByTableName" resultType="User">
select * from ${tableName}
</select>

<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user values (null,#{username},#{password},#{age},#{sex},#{email})
</insert>
</mapper>

3.答案解析

对于getUsersLike方法来说,如果我们想要使用模糊查询,此时不能使用'%#{username}%'的参数表示方式,只能使用'%${username}%'concat('%',#{username},'%)或者"%"#{username}"%"这几种方式,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 测试特殊SQL的执行:模糊查询、批量删除、动态设置表名
*/
@Test
public void test4() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SQLMapper mapper = session.getMapper(SQLMapper.class);
List<User> users = mapper.getUsersLike("a");
System.out.println(users);
}
}

image-20231124174625433

对于deleteMore方法来说,方法参数是用户ID的字符串例如”1,2,3”,我们不能使用#{},因为这会多出单引号,因此我们只能使用${},测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 测试特殊SQL的执行:模糊查询、批量删除、动态设置表名
*/
@Test
public void test4() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SQLMapper mapper = session.getMapper(SQLMapper.class);
int result = mapper.deleteMore("6,7,8");
System.out.println("result = " + result);
}
}

image-20231124175215290

#{ids}改为${ids}后,发现批量删除成功:

image-20231124175436979

对于getUsersByTableName方法来说,表名同样不能加单引号,因此不能使用#{},测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 测试特殊SQL的执行:模糊查询、批量删除、动态设置表名
*/
@Test
public void test4() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SQLMapper mapper = session.getMapper(SQLMapper.class);
List<User> tUser = mapper.getUsersByTableName("t_user");
System.out.println(tUser);
}
}

image-20231124175710340

对于insertUser方法来说,如果我们想获取插入数据的自增主键值,我们需要设置useGeneratedKeys标签为true,并且设置keyProperty属性为传入的实体类对象参数的主键id,这样插入成功后可以通过查看user对象的id属性获得自增主键值,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 测试特殊SQL的执行:模糊查询、批量删除、动态设置表名
*/
@Test
public void test4() {
try(SqlSession session = SqlSessionUtil.getSqlSession()) {
SQLMapper mapper = session.getMapper(SQLMapper.class);
User user = new User(null, "huling", "123456", 22, "男", "huling@163.com");
mapper.insertUser(user);
System.out.println(user);
}
}

image-20231124180127987

实际上,设置useGeneratedKeys标签为true后,这会令MyBatis使用JDBC的getGeneratedKeys方法来取出插入成功后由数据库内部生成的主键值,上图的打印顺序也说明了这个问题。