MyBatis映射文件

Mybatis映射文件

映射器在Mybatis中是比较核心的部分,也是在使用Mybatis开发过程中使用最多的工具。Mybatis通过映射器将Java代码和SQL语句进行分离,在Java代码中不必包含繁复的SQL语句,将所有的SQL语句全部分离到映射器中只需要很少的配置即可完成对数据库的操作,另外这么做也使得项目更加易于维护和扩展。

映射器主要元素

SQL映射文件只有几个顶级元素,下面先认识这几个元素。

元素名称 描述 备注
select 映射查询语句 可以自定义参数和返回结果
insert 映射插入语句 返回值是一个整数,即影响的条数
update 映射更新语句 返回值是一个整数,即影响的条数
delete 映射删除语句 返回值是一个整数,即影响的条数
sql 可以被其他语句引用的可重用的语句块
resultMap 描述从数据库结果集中加载对象
cache 配置命名空间的缓存
cache-ref 引用其他命名空间的缓存

select元素

查询在日常开发中出现频率最高的数据库操作,select元素帮助开发中从数据库中读取数据,并映射进实体类对象,在执行select语句前,开发中需要定义传入参数(如果sql语句中需要),参数可以是一个简单的类型,如:int、float、String等,也可以是一个JavaBean、Map等。执行SQL后Mybatis会自动映射帮助开发者将结果集绑定到JavaBean中。

select标签常用属性如下表:

属性 描述
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。
resultType 期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。
resultMap 对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个。
useCache 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。

select简单查询

下面,先试用select标签进行简单的查询,首先创建数据库:

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
72
73
74
-- ----------------------------
-- Table structure for course
-- ----------------------------
DROP TABLE IF EXISTS `course`;
CREATE TABLE `course` (
`course_id` int(11) NOT NULL AUTO_INCREMENT,
`course_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`course_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of course
-- ----------------------------
INSERT INTO `course` VALUES (1, '语文');
INSERT INTO `course` VALUES (2, '数学');
INSERT INTO `course` VALUES (3, '英语');

-- ----------------------------
-- Table structure for socre
-- ----------------------------
DROP TABLE IF EXISTS `socre`;
CREATE TABLE `socre` (
`score_id` int(11) NOT NULL AUTO_INCREMENT,
`student_id` int(11) DEFAULT NULL,
`course_id` int(11) DEFAULT NULL,
`score` double DEFAULT NULL,
PRIMARY KEY (`score_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of socre
-- ----------------------------
INSERT INTO `socre` VALUES (1, 1, 1, 80);
INSERT INTO `socre` VALUES (2, 1, 2, 85);
INSERT INTO `socre` VALUES (3, 1, 3, 90);
INSERT INTO `socre` VALUES (4, 2, 1, 92);
INSERT INTO `socre` VALUES (5, 2, 2, 75);
INSERT INTO `socre` VALUES (6, 2, 3, 80);

-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`student_id` int(11) NOT NULL AUTO_INCREMENT,
`student_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`student_age` int(20) DEFAULT NULL,
`student_gender` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`student_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, '李雷', 18, '男');
INSERT INTO `student` VALUES (2, '韩梅梅', 18, '女');

-- ----------------------------
-- Table structure for student_detail
-- ----------------------------
DROP TABLE IF EXISTS `student_detail`;
CREATE TABLE `student_detail` (
`student_detail_id` int(11) NOT NULL AUTO_INCREMENT,
`student_id` int(11) DEFAULT NULL,
`telephone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`mail` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`student_detail_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student_detail
-- ----------------------------
INSERT INTO `student_detail` VALUES (1, 1, '17786661234', 'lilei@163.com');
INSERT INTO `student_detail` VALUES (2, 2, '18627465535', 'hanmeimei@163.com');

在创建好数据库后,做一个简单查询,根据ID查询学生基本信息。步骤如下:

  1. 新建Student实体类
1
2
3
4
5
6
7
8
9
package cn.bytecollege.entity;
import lombok.Data;
@Data
public class Student {
private int studentId;
private String studentName;
private int studentAge;
private String studentGender;
}
  1. 新建接口StudentMapper,并在接口中定义方法findStudentById(int id)。
1
2
3
4
5
6
package cn.bytecollege.mapper;
import cn.bytecollege.entity.Student;
public interface StudentMapper {
//根据ID查询学生基本信息
Student findStudentById(int id);
}
  1. 新建映射器StudentMapper.xml,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--1.namespace和接口的全限定名一致-->
<!--2.接口方法名和xml中sql语句ID一致-->
<!--3.sql语句resultType返回值类型和方法返回值类型一致-->
<!--4.输入参数类型与方法中一致-->
<mapper namespace="cn.bytecollege.mapper.StudentMapper">
<select id="findStudentById" resultType="cn.bytecollege.entity.Student" parameterType="int">
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT WHERE STUDENT_ID = #{studentId}
</select>
</mapper>

在新建的映射器中需要遵守如下约定:

  1. 映射器mapper标签的namespace属性值必须和接口的全限定名一致,只有这样Mybatis才会将接口和映射器互相绑定
  2. select元素的属性是只该条查询语句的唯一标识,必须和方法名称一致,并且在同一命名空间下不能有相同ID的元素(这里的元素包括insert、delete、update、select)
  3. select元素的resultType属性值要和接口中方法定义的返回值类型保持一致
  4. select元素的parameterType属性值要和接口中方法定义的参数类型保持一致。

下面将就映射器中的各个标签及属性讲解:

  1. mapper标签:该标签是映射器的根标签,select、insert、delete、update等元素必须放置在该标签内。标签的namespace(命名空间)的作用有两个:一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了上面见到的接口绑定。

  2. select标签:该用于做查询操作

    1. id属性是该标签在所在命名空间中的唯一标识
    2. parameterType是指查询语句中如果有传入参数,则使用该属性指定参数的java属性类型,上例中的查询语句是根据studentId查询学生对象,studentId是整型,因此需要指定该属性的值为整型,在Mybatis中为常用的类型指定了别名,因此此处写int和Integer并没有什么区别
    3. resultType是指查询语句查询成功后需要将结果集中的数据映射到哪个类型的对象中去,上例中将结果集中的数据映射到Student对象中,需要注意的是如果在全局配置文件中指定了类型别名,此处可以写类的别名,如果没有指定别名,则需要配置类型的全限定名。

最后,新建测试类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.bytecollege.test;

import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();

int id = 1;
StudentMapper mapper = session.getMapper(StudentMapper.class);
Student student = mapper.findStudentById(id);
}
}

MyBatisUtils:工具类

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 cn.bytecollege.utils;

import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;

import static org.apache.ibatis.io.Resources.getResourceAsStream;

public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
//读取Mybatis配置文件
String resources = "mybatis-config.xml";
try {
InputStream is = getResourceAsStream(resources);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession getSqlSesstion(){
//获取SqlSession
return sqlSessionFactory.openSession();
}
}

查询结果如下:

resultMap

在数据库设计的过程中对于数据库的命名方式通常采用经典命名法,即如果字段需要多个单词来表明字段含义,多个单词之间用下划线连接,例如学生性别student_age。但是在Java中实例变量的命名方式通常采用camel命名法,对应学生性别字段的实例变量名称为studentAge,这样就存在一个问题,如何将数据库的字段和实体类的实例变量一一对应,再或者数据库中的字段名和实体类属性名称完全不同,例如学生性别在数据库中使用student_gender,而实体类中对应的实例变量名称是gender,这样该如何将查询结果和实体类对象进行绑定呢?这时resultMap就可以解决这个问题,resultMap元素的主要作用就是配置数据库表中的字段和实体类实例变量的映射。

需要注意的是resultMap并不是必须的,如果数据库的字段采用经典命名法,实体类属性使用camel命名法时,通过Mybatis全局设置的Settings属性配置,Mybatis即可自动将查询结果映射到实体类对象的实例变量中。

但是如果要进行关联查询,那么就必须使用resultMap元素了(关于关联查询会在下一小节讲解)。

首先了解一下resultMap所包含的子标签及常用属性

子标签

  • constructor - 用于在实例化类时,注入结果到构造方法中

    • idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • arg - 将被注入到构造方法的一个普通结果
  • id – 一个 ID 结果,一般用于配置实体类中对应数据库表主键字段的属性。

  • result – 用于配置除了主键对应字段的实例变量

  • association – 一个复杂类型的关联;许多结果将包装成这种类型

    • 嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的引用
  • collection – 一个复杂类型的集合

    • 嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的引用
  • discriminator – 使用结果值来决定使用哪个 resultMap

    • case – 基于某些值的结果映射
      • 嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射

resultMap属性

id 当前命名空间中的一个唯一标识,用于标识一个结果映射。
type 类的完全限定名, 或者一个类型别名

下面,以Student表为例进行示例:

在StudentMapper.xml中配置如下:

1
2
3
4
5
6
<resultMap id="StudentResultMap" type="cn.bytecollege.entity.Student">
<id property="studentId" column="student_id"></id>
<result property="studentName" column="student_name"></result>
<result property="studentAge" column="student_age"></result>
<result property="studentGender" column="student_gender"></result>
</resultMap>

在上面的配置中可以看出id标签和result标签都具有相同的属性property和column,其中column属性指数据库表中的字段名称,property是指将column中配置的字段的值映射到哪个属性。

在配置好resultMap后,还需要在select标签中配置resultMap属性,需要特别注意的是,resultType和resultMap不能同时出现。

1
2
3
4
<select id="findStudentById" resultMap="StudentResultMap" parameterType="int">
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT WHERE STUDENT_ID = #{studentId}
</select>

关联查询

在进行关联查询时,通常要辨别数据的对应关系,如:一对一,一对多,多对多,如果使用JDBC查询,将数据查询出来后如何封装数据是一件很复杂的工作,但是使用Mybatis就可以很好的解决这个问题。

在初始化的数据库中可以看出学生表和学生信息表存在一对一的关系,学生和成绩则是一对多的关系,就这两个操作来学习Mybatis的关联查询

一对一

学生基本信息和详细信息存在一对一的关系,在实体类中,可以在Student类中封装StudentDetail对象用于表示这种关系,那么在映射器中应该如何表示这种关系呢,这就需要使用resultMap的子标签association

首先在Student类中封装StudentDetail类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cn.bytecollege.vo;
import cn.bytecollege.entity.StudentDetail;
import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class StudentDetailVO {
private int studentId;
private String studentName;
private int studentAge;
private String studentGender;
private StudentDetail studentDetail;
}

在StudentMapper接口中定义方法:

1
2
3
4
public interface StudentMapper {
//根据ID查询学生基本信息及详情
StudentDetailVO findStudentDetailById(int id);
}

接着在StudentMapper.xml中配置resultMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<resultMap id="StudentDetailResultMap" type="cn.bytecollege.vo.StudentDetailVO">
<id property="studentId" column="student_id"></id>
<result property="studentName" column="student_name"></result>
<result property="studentAge" column="student_age"></result>
<result property="studentGender" column="student_gender"></result>
<association property="studentDetail" javaType="cn.bytecollege.entity.StudentDetail">
<result property="mail" column="mail"></result>
<result property="telephone" column="telephone"></result>
</association>
</resultMap>
<!--查询语句如下-->
<select id="findStudentDetailById" resultMap="StudentDetailResultMap" parameterType="int">
SELECT STUDENT.STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER,MAIL,TELEPHONE
FROM STUDENT INNER JOIN STUDENT_DETAIL ON STUDENT.STUDENT_ID = STUDENT_DETAIL.STUDENT_ID
WHERE STUDENT.STUDENT_ID = #{studentId}
</select>

上述代码中association标签的property是指在Student中封装的StudentDetail类的变量名,javaType则是在变量对应的Java类型。

在association中的result则配置了StudentDetail中的实例变量和对应的数据库的字段名称。

新建测试类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.bytecollege.test;
import cn.bytecollege.mapper.StudentMapper;
import cn.bytecollege.vo.StudentDetailVO;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();

int id = 1;
StudentMapper mapper = session.getMapper(StudentMapper.class);
StudentDetailVO studentDetailVO = mapper.findStudentDetailById(id);
}
}

查询结果如下:

一对多

在上一小节的示例中学习了一对一的关联查询,在本小节内,将详细学习一对多的关联查询。

在初始化的数据库中可以看出学生和成绩是一对多的关系,例如要查询id为1的学生的所有成绩,这就需要用到resultMap的子标签collection。

首先在Student类中封装Score类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cn.bytecollege.vo;

import cn.bytecollege.entity.Score;
import lombok.Data;

import java.util.List;
@Data
public class StudentScoreVO {
private int studentId;
private String studentName;
private int studentAge;
private String studentGender;
private List<Score> list;
}

在StudentMapper接口中定义方法:

1
2
3
4
public interface StudentMapper {
//根据ID查询学生成绩
StudentScoreVO findStudentScore(int id);
}

在映射器中配置resultMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<resultMap id="StudentScoreResultMap" type="cn.bytecollege.vo.StudentScoreVO">
<id property="studentId" column="student_id"></id>
<result property="studentName" column="student_name"></result>
<result property="studentAge" column="student_age"></result>
<result property="studentGender" column="student_age"></result>
<collection property="list" ofType="cn.bytecollege.entity.Score">
<result column="course_id" property="courseId"></result>
<result column="score" property="score"></result>
</collection>
</resultMap>
<!--查询语句如下-->
<select id="findStudentScore" resultMap="StudentScoreResultMap" parameterType="int">
SELECT STUDENT.STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER,COURSE_ID,SCORE
FROM STUDENT
LEFT JOIN SCORE S ON STUDENT.STUDENT_ID = S.STUDENT_ID
WHERE
STUDENT.STUDENT_ID = #{studentId}
</select>

在上面的配置中collection标签的property属性是指封装进实体类集合的名称,ofType则是集合中存放的是何种类型的数据。

新建测试类:

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
package cn.bytecollege.test;
import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import cn.bytecollege.vo.StudentScoreVO;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;
public class Test {

public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();

int id = 1;
StudentMapper mapper = session.getMapper(StudentMapper.class);
StudentScoreVO studentScoreVO = mapper.findStudentScore(id);
logger.info(studentScoreVO);
}
}

运行结果如下:

复杂的关联查询

在上一小节的示例中可以看出,虽然查询出了学生的分数和课程id,但是并没有查询出课程id对应的课程名称。如果需要查询课程名称,用Mybatis很容易做到,简单分析即可明确成绩和课程是一对一的关系,那么只需要在Score类中封装Course类即可。

实体类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
package cn.bytecollege.vo;
import cn.bytecollege.entity.Score;
import lombok.Data;
import java.util.List;
@Data
public class StudentScoreVO {
private int studentId;
private String studentName;
private int studentAge;
private String studentGender;
private List<Score> list;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.bytecollege.entity;
import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class Score {
private int scoreId;
private int studentId;
private int courseId;
private double score;
private Course course;
}

StudentMapper.xml映射器中将上例中的resultMap稍作修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<resultMap id="StudentScoreResultMap" type="cn.bytecollege.vo.StudentScoreVO">
<id property="studentId" column="student_id"></id>
<result property="studentName" column="student_name"></result>
<result property="studentAge" column="student_age"></result>
<result property="studentGender" column="student_age"></result>
<collection property="list" ofType="cn.bytecollege.entity.Score">
<result column="course_id" property="courseId"></result>
<result column="score" property="score"></result>
<association property="course" javaType="cn.bytecollege.entity.Course">
<result property="courseName" column="COURSE_NAME"></result>
</association>
</collection>
</resultMap>
<!--修改SQL语句-->
<select id="findStudentScore" resultMap="StudentScoreResultMap" parameterType="int">
SELECT STUDENT.STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER,S.COURSE_ID,SCORE,COURSE_NAME
FROM STUDENT
LEFT JOIN SCORE S ON STUDENT.STUDENT_ID = S.STUDENT_ID
INNER JOIN COURSE C ON S.COURSE_ID = C.COURSE_ID
WHERE
STUDENT.STUDENT_ID = #{studentId}
</select>

从上面的代码中可以看出collection标签和association标签进行了嵌套,这是因为学生和成绩是一对多的关系,而成绩和课程是一对一的关系,在对实体类封装过后,对resultMap也需要进行对应的修改。

接口和测试类不需要改动,再次运行测试类,结果如下:

懒加载

如果对联合查询不熟悉,Mybatis还提供了一种查询方式,通俗来讲就是将SQL语句拆分,以3.2.3.1中查询学生详情为例,查询学生基本信息后,根据基本信息中的学生ID,再查询学生详情。如下代码所示,将一个操作拆分为了2个。

1
2
3
4
5
6
<select id="findStudent" resultMap="StudentDetailResultMap" parameterType="int">
SELECT * FROM STUDENT WHERE STUDENT_ID = #{studentId}
</select>
<select id="findStudentDetail" resultType="cn.bytecollege.entity.StudentDetail" parameterType="int">
SELECT * FROM STUDENT_DETAIL WHERE STUDENT_ID = #{studentId}
</select>

编写完查询语句后,需要对ResultMap进行一些修改。

1
2
3
4
5
6
7
8
9
10
<resultMap id="StudentDetailResultMap" type="cn.bytecollege.vo.StudentDetailVO">
<id property="studentId" column="student_id"></id>
<result property="studentName" column="student_name"></result>
<result property="studentAge" column="student_age"></result>
<result property="studentGender" column="student_age"></result>
<association property="studentDetail" column="student_id" javaType="cn.bytecollege.entity.StudentDetail" select="findStudentDetail">
<result property="mail" column="mail"></result>
<result property="telephone" column="telephone"></result>
</association>
</resultMap>

此时需要在association元素上添加column属性和select属性,其中select属性指的是查询的ID,也就是说在查询关联对象时使用哪个sql语句进行查询。column属性则是指进行查询时所需要的字段。

也就是说在查询完学生详情后,需要将获取到的student_id传给查询关联对象的SQL语句。

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

StudentDetailVO vo = mapper.findStudent(1);
}

运行测试类后结果如下:

从结果中可以看出Mybatis分别执行了两条SQL语句。

这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为“N+1 查询问题”。 概括地讲,N+1 查询问题是这样子的:

  • 你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。
  • 对列表返回的每条记录,你再次执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。

这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来。

如果要是用延迟加载,首先需要在Mybatis全局配置中进行配置,配置如下:

1
2
3
4
5
6
<settings>
<!--开启延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--级联对象懒加载-->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

在上述配置中,lazyLoadingEnabled延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认值为false aggressiveLazyLoading开启时,任一方法的调用都会加载该对象的所有延迟加载属性,在Mybatis3.4.1之前默认值为true。当值为false时,此时在不是用级联对象时,对应的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
package cn.bytecollege.test;
import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import cn.bytecollege.vo.StudentDetailVO;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

StudentDetailVO vo = mapper.findStudent(1);
logger.info(vo.getStudentName());
}
}

此时,没有使用StudentDetail中的数据,结果如下:

从结果中可以看出,Mybatis并没有查询学生详情,此时打印学生详情中的信息,修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

StudentDetailVO vo = mapper.findStudent(1);
logger.info(vo.getStudentDetail().getTelephone());
}
}

从执行结果中可以看出,此时使用了学生详情中的数据,Mybatis也执行了对应的SQL语句。

如果将aggressiveLazyLoading的属性设置为true,则不管有没有使用级联对象中的数据,对应的所有SQL语句对应都会执行,将aggressiveLazyLoading值修改为true,在不打印StudentDetail对象数据的情况下,再次执行测试类。

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

StudentDetailVO vo = mapper.findStudent(1);
logger.info(vo.getStudentName());
}

结果如下:

从结果可以看出,不管是否使用级联对象中的数据,此时也执行了所有的SQL语句。

缓存

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 其特点是将数据保存在计算内存中,在读取的时候无需从硬盘读入。因此可以快速读取和使用,可以极大的改善和提高系统的性能。

一级缓存

Mybatis在默认情况下只开启一级缓存,一级缓存只是相对于同一个SqlSession对象而言,其具体表现形式是,在参数和SQL完全一样的情况下,使用同一个SqlSession对象调用同一个Mapper的方法,通常只执行1次SQL查询,因为使用SqlSession第一次查询,Mybatis会将查询结果放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没超时的情况下,SqlSession只读取缓存的数据,而不会再次发送SQL到数据库。

但是,如果使用了不同SqlSession对象,因为不同的SqlSession对象是相互隔离的,所有用相同的Mapper、参数和方法,它还是会再次发送SQL的数据库执行。

下面,以3.2.1小节中简单查询来测试一级缓存:

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
package cn.bytecollege.test;
import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import cn.bytecollege.vo.StudentScoreVO;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();

int id = 1;
StudentMapper mapper = session.getMapper(StudentMapper.class);
//第一次查询
logger.info("----------------------------------第一次查询----------------------------------");
Student student1 = mapper.findStudentById(id);
logger.info("----------------------------------第二次查询----------------------------------");
Student student2 = mapper.findStudentById(id);
}
}

运行结果如下:

从查询结果可以看出第一次查询的时候Mybatis向数据库发送并执行了SQL语句,第二次使用相同的SQL语句和参数再次查询时,并没有向数据库发送SQL语句。

再次测试使用不同的SqlSession对象查询相同的数据:

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 cn.bytecollege.test;
import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import cn.bytecollege.vo.StudentScoreVO;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
int id = 1;
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);
//第一次查询
logger.info("----------------------------------第一次查询----------------------------------");
Student student1 = mapper1.findStudentById(id);
logger.info("----------------------------------第二次查询----------------------------------");
Student student2 = mapper2.findStudentById(id);
}
}

运行结果如下

从查询结果可以看出,当使用不同的SqlSession对象进行查询时,都会向数据库发送并执行SQL语句,因此Mybatis一级缓存是基于SqlSession对象的。

二级缓存

二级缓存默认是不开启的,开启二级缓存需要进行配置,实现二级缓存的时候,Mybatis要求返回的实体类对象必须是可序列化的,配置的方法很简单,只需要在映射器XML文件中进行如下配置:

1
<cache/>

在全局配置文件开启二级缓存

1
<setting name="cacheEnabled" value="true"/>

在这个语句中Mybatis进行了如下了默认配置:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

需要注意的是,如果要测试二级缓存,需要在第一次查询后提交事务,否则会导致缓存失效:

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
package cn.bytecollege.test;
import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import cn.bytecollege.vo.StudentScoreVO;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
int id = 1;
StudentMapper mapper1 = session1.getMapper(StudentMapper.class);
StudentMapper mapper2 = session2.getMapper(StudentMapper.class);

//第一次查询
logger.info("----------------------------------第一次查询----------------------------------");
Student student1 = mapper1. findStudentById(id);
//提交事务
session1.commit();
logger.info("----------------------------------第二次查询----------------------------------");
Student student2 = mapper2.findStudentById(id);
}
}

在StudentMapper.xml映射器中配置开启二级缓存后,再次查询执行结果如下

insert元素

insert语句用于在数据库中新增数据,Mybatis会在执行插入后返回一个整数,表示插入操作后影响的记录数。insert元素的配置如下表,虽然配置很多,但是常用的只有几个。

属性 描述
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
statementType 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
useGeneratedKeys (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。
keyProperty (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn (仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。

下面,使用insert元素对Student表进行新增操作:

首先在StudentMapper接口中定义方法,代码如下:

1
2
3
public interface StudentMapper {
int addStudent(Student student);
}

接下来,在映射器xml中配置insert元素,代码如下:

1
2
3
4
5
<insert id="addStudent" parameterType="cn.bytecollege.entity.Student">
INSERT INTO STUDENT (STUDENT_NAME, STUDENT_AGE, STUDENT_GENDER)
VALUES
(#{studentName},#{studentAge},#{studentGender})
</insert>

定义测试类进行测试:

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 cn.bytecollege.test;
import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

Student student = new Student();
student.setStudentGender("男");
student.setStudentAge(18);
student.setStudentName("Jim");
logger.info("-----------------------------新增操作-----------------------------");
mapper.addStudent(student);
session.commit();
}
}

运行结果如下:

主键回填

在开发时有时需要在新增数据后获取自增的主键。例如在网站中注册了用户名密码以后,在适当的时候还需要用户补全详细信息,此时就需要在用户详情表先占一个位置,此时就需要将新注册用户的ID,先插入详情表。

在Mybatis当中,如果需要主键回填,则只需要简单的配置即可,当新增成功后,Mybatis会将获取到的主键保存进配置好的属性中

首先需要在insert元素上添加useGeneratedKeys,并赋值为true。并设置keyProperty表明获取到主键后封装进哪个属性代码如下:

1
2
3
4
5
<insert id="addStudent" parameterType="cn.bytecollege.entity.Student" keyProperty="studentId" useGeneratedKeys="true">
INSERT INTO STUDENT (STUDENT_NAME, STUDENT_AGE, STUDENT_GENDER)
VALUES
(#{studentName},#{studentAge},#{studentGender})
</insert>

当新增完成后Mybatis会将获得的主键值封装进传入参数对象的对应属性中。

再次新增数据并打印获取的ID

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 cn.bytecollege.test;
import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

Student student = new Student();
student.setStudentGender("男");
student.setStudentAge(18);
student.setStudentName("Bob");
mapper.addStudent(student);
session.commit();
logger.info("studentId====="+student.getStudentId());
}
}

运行结果如下:

selectKey元素

对于不支持自动生成主键的数据库或者需要自定义主键时就可以使用selectKey元素。首先查看一下该元素的主要属性:

keyProperty selectKey 语句结果应该被设置到的目标属性。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn 返回结果集中生成列属性的列名。如果生成列不止一个,可以用逗号分隔多个属性名称。
resultType 结果的类型。通常 MyBatis 可以推断出来,但是为了更加准确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果生成列不止一个,则可以使用包含期望属性的 Object 或 Map。
order 可以设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它首先会生成主键,设置 keyProperty 再执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句 - 这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用。
statementType 和前面一样,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 类型的映射语句,分别代表 Statement, PreparedStatement 和 CallableStatement 类型。

例如下面的示例:

1
2
3
4
5
6
7
8
<insert id="addStudent" parameterType="cn.bytecollege.entity.Student">
<selectKey keyProperty="studentId" resultType="String" order = "BEFORE">
SELECT UUID();
</select>
INSERT INTO STUDENT (STUDENT_ID,STUDENT_NAME, STUDENT_AGE, STUDENT_GENDER)
VALUES
(#{studentId},#{studentName},#{studentAge},#{studentGender})
</insert>

update元素

update元素主要用于更新数据,其主要属性和insert一致,此处不做赘述。下面通过示例修改Student表中信息。

首先在StudentMapper接口中定义方法:

1
2
3
public interface StudentMapper {
int updateStudent(Student student);
}

接下来,在StudentMapper.xml映射器中配置update元素,代码如下:

1
2
3
4
5
6
<update id="updateStudent" parameterType="cn.bytecollege.entity.Student">
UPDATE STUDENT SET STUDENT_NAME = #{studentName},
STUDENT_AGE = #{studentAge},
STUDENT_GENDER = #{studentGender}
WHERE STUDENT_ID = #{studentId}
</update>

新建测试类代码如下:

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 cn.bytecollege.test;
import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

Student student = new Student();
student.setStudentGender("男");
student.setStudentAge(20);
student.setStudentName("John");
student.setStudentId(6);
mapper.updateStudent(student);
session.commit();
}
}

运行结果如下图:

delete元素

delete元素是用于删除数据,其主要属性和insert一致,下面通过删除学生示例学习delete元素的使用

首先,StudentMapper.xml接口中定义方法

1
2
3
public interface StudentMapper {
int deleteStudentById(int id);
}

接着在StudentMapper.xml映射器中添加delete元素,代码如下:

1
2
3
<delete id="deleteStudentById" parameterType="int">
DELETE FROM STUDENT WHERE STUDENT_ID = #{studentId}
</delete>

新建测试类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.bytecollege.test;
import cn.bytecollege.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

mapper.deleteStudentById(6);
session.commit();
}
}

运行结果如下:

sql元素

这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 例如:

1
2
3
4
5
<sql id="StudentColumns">STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER</sql>
<select id="findStudentById" resultMap="StudentResultMap" parameterType="int">
SELECT <include refid="StudentColumns"></include>
FROM STUDENT WHERE STUDENT_ID = #{studentId}
</select>

在上述代码中使用sql元素定义了SQL代码片段,如果需要使用该片段可以使用include标签引入,需要注意的是sql元素需要一个唯一id,以便include元素的refId的属性引用。

参数

Mybatis中的参数可以分为两类,即select、insert、delete、update等元素输入参数和输出参数,也就是parameterType和resultType对应的值。

输入参数

在前面的章节示例中可以看出,如果SQL语句的参数是基本类型的值,只需要在对应的parameterType配置基本类型的名称或者基本类型对应的包装类名称(也可以是包装类的全限定名)。对于复杂的类型,可以封装进对象传递给SQL语句,在SQL语句中使用“#{}”,这里的“#{}”可以理解为JDBC中的占位符“?”,并且使用“#{}”会预编译SQL语句,也一定程度上避免了SQL注入。

当使用“#{}”时,假如传入的参数是对象,则会调用对象中在“#{}”配置属性所对应的get方法,如果不存在该属性,则抛出异常。除此以外,还可以传入一个集合作为参数。例如新增学生信息,可以将学生信息封装进HashMap。下面通过示例来学习使用HashMap作为输入参数。

首先,在StudentMapper.xml映射器中添加insert元素,parameterType修改为HashMap:

1
2
3
4
5
<insert id="addStudent" parameterType="HashMap" useGeneratedKeys="true" keyProperty="studentId">
INSERT INTO STUDENT (STUDENT_NAME, STUDENT_AGE, STUDENT_GENDER)
VALUES
(#{studentName},#{studentAge},#{studentGender})
</insert>

在StudentMapper接口中定义方法

1
2
3
public interface StudentMapper {
int addStudent(Map map);
}

定义测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

Map<String,Object> map = new HashMap<>();
map.put("studentName","Jack");
map.put("studentAge",18);
map.put("gender","男");

mapper.addStudent(map);
session.commit();
}
}

运行结果如下:

当传入参数是Map时,Mybatis会调用get方法获取#{}中配置的key所对应的value。但是通常并不建议使用Map作为输入参数,因为这样会降低代码的可读性,并且由于不同的书写习惯,可能导致key的命名不一致,进而获取不到对应的value。

3.9.2 字符串替换

默认情况下,使用 #{} 参数语法时,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在 SQL 语句中直接插入一个不转义的字符串。 比如 ORDER BY 子句,这时候你可以:

1
ORDER BY ${columnName}

这样,MyBatis 就不会修改或转义该字符串了。除非必要情况下,并不推荐使用这种方式,因为这将会导致被SQL注入。

#{}和${}的区别

  1. #{}是预编译处理,${}是字符串替换。
  2. Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 的 set 方法来赋值;
  3. Mybatis 在处理${}时,就是把${}替换成变量的值。
  4. 使用#{}可以有效的防止 SQL 注入,提高系统安全性。

输出参数

输出参数通常使用ResultMap或者ResultType,ResultMap在前面的内容中已经介绍过了。此处主要介绍resultType,在前面的实例中可以看出insert,update,delete等元素通常不需要配置该参数,因为这些元素固定返回一个整型值,resultType通常在select元素上使用,其值可以是基本类型也可以是引用类型。和parameterType一样,resultType的值也可以是一个Map,但是并不建议这么做。下面通过示例学习。

首先,在StudentMapper.xml映射器中配置select元素

1
2
3
4
<select id="findStudentById" resultType="HashMap" parameterType="int">
SELECT STUDENT_ID,STUDENT_NAME,STUDENT_AGE,STUDENT_GENDER
FROM STUDENT WHERE STUDENT_ID = #{studentId}
</select>

在StudentMapper接口中定义方法

1
2
3
4
public interface StudentMapper {
//根据ID查询学生基本信息
HashMap findStudentById(int id);
}

新建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger(Test.class);
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
StudentMapper mapper = session.getMapper(StudentMapper.class);

HashMap map = mapper.findStudentById(1);
map.forEach((k,v)->logger.info(k+"===="+v));
}
}

运行结果如下:

从结果中可以看出,Mybatis会将字段的名称作为key,字段的值作为value放入map中。