MyBatis

MyBatis

架构

依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.2</version>
</dependency>

用法

1
2
3
4
5
6
7
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
} finally {
session.close();
}

启动

确保整个应用程序只有一个 SqlSessionFactory 实例:

1
2
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

编写 MyBatis 数据库配置文件 mybatis-config.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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>
<typeAliases>
<typeAlias type="com.zk.User" alias="user" />
</typeAliases>
<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://localhost:3306/crawl"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>
</configuration>

编写 MyBatis - SQL 操作映射文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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="com.zk.UserMapper">
<select id="getUser" parameterType="long" resultType="user">
select id, name, password from users where id = #{id}
</select>
<insert id="insertUser" parameterType="user">
insert into users(name, password) values (#{name}, #{password})
</insert>
<delete id="deleteUser" parameterType="long">
delete from users where id = #{id};
</delete>
</mapper>

执行插入操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

User user = new User();
user.setName("Jack");
user.setPassword("123456");

userMapper.insertUser(user);

sqlSession.commit();
sqlSession.close();

注: 默认必须调用 sqlSession.commit() 之后才会提交操作

日志

MyBatis provides logging information through the use of an internal log factory. The internal log factory will delegate logging information to one of the following log implementations:

  • SLF4J
  • Apache Commons Logging
  • Log4j 2
  • Log4j
  • JDK logging

The logging solution chosen is based on runtime introspection by the internal MyBatis log factory.


添加依赖:

1
2
3
4
5
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

放置 log4j.properties:

1
2
3
4
5
6
7
8
# Global logging configuration
log4j.rootLogger=DEBUG,stdout
# MyBatis logging configuration
log4j.logger.org.mybatis=DEBUG
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n

下面的方法也可以:

(1) 查看全部日志:

1
log4j.rootLogger = DEBUG, stdout

(2) 查看某个 Mapper 方法里面的日志:

1
2
3
4
5
6
# root 调整为 INFO
log4j.rootLogger = INFO, stdout

# 这个 Logger 调整为 DEBUG
log4j.logger.com.buptnsrc.appcrawl.mapper.WebpageApkMapper.selectRankingValidApkCount = DEBUG, S
log4j.additivity.com.buptnsrc.appcrawl.mapper.WebpageApkMapper.selectRankingValidApkCount = false

显示指明 configuration 打印日志所依赖的 Jar 包:

1
2
3
4
5
6
7
<configuration>
<settings>
...
<setting name="logImpl" value="LOG4J"/>
...
</settings>
</configuration>

传递多个参数 - Map

parameterType 的值设置为 map 即可接受多个参数:

1
2
3
<select id="getUseWithMapParam" parameterType="map" resultType="user">
select id, name, password from users where name = #{name} and password = #{password}
</select>
1
2
3
public interface UserMapper {
User getUseWithMapParam(Map<String, String> params);
}
1
2
3
4
Map<String, String> params = new HashMap<String, String>();
params.put("name", "Zhaokun");
params.put("password", "123456");
User user = userMapper.getUseWithMapParam(params);

传递多个参数 - @Param

1
2
3
public interface UserMapper {
User getUseWithMapParam(@Param("name") String name, @Param("password") String password);
}
1
2
3
<select id="getUseWithMapParam" parameterType="map" resultType="user">
select id, name, password from users where name = #{name} and password = #{password}
</select>

插入的时候自动回填主键

1
2
3
4
<insert id="insertUserWithAutoGenerateId" parameterType="user"
useGeneratedKeys="true" keyProperty="id">
insert into users(name, password) values (#{name}, #{password})
</insert>
1
2
3
public interface UserMapper {
void insertUserWithAutoGenerateId(User user);
}
1
2
3
User user = createUser();
userMapper.insertUserWithAutoGenerateId(user);
System.out.println(user.getId());

传递多个参数 - @Param

不用声明 parameterType:

1
2
3
<select id="getUseWithAnnotationParam" resultType="user">
select id, name, password from users where name = #{name} and password = #{password}
</select>
1
2
3
public interface UserMapper {
User getUseWithAnnotationParam(@Param("name") String name, @Param("password") String password);
}

参数设置精度

1
#{price, javaType=double, jdbcType=NUMERIC, numericScale=2}

association 一对一级联

学生拥有学生卡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Student {

private long id;
private String name;
private StudentCard studentCard;

}

public class StudentCard {

private long cardId;
private String cardName;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<resultMap id="studentAndCardMap" type="com.zk.Student">
<id property="id" column="student.id" />
<result property="name" column="user_name" />
<association property="studentCard" javaType="com.zk.StudentCard">
<id property="cardId" column="card_id" />
<result property="cardName" column="card_name" />
</association>
</resultMap>

<select id="getStudent" parameterType="long" resultMap="studentAndCardMap">
select student.id as user_id,
student.name as user_name,
student_card.id as card_id,
student_card.name as card_name
FROM student left outer join student_card
ON student.id = student_card.user_id
WHERE student.id = #{id}
</select>

collection 一对多级联

学生拥有多门课程:

If 元素

某个元素不为 null 且不为空字符串就查询:

1
2
3
4
5
6
7
8
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.String" >
select user_name, user_password
from asec_phishingapp_review_task
where 1 = 1
<if test="userId != null and userId != ''">
and user_id like concat('%', #{userId}, '%')
</if>
</select>

注意事项

(1) 分清 #$ 的区别

不要误把 # 写成了 $:

1
2
3
4
5
<select id="selectBy" resultMap="BaseResultMap">
select <include refid="Base_Column_List" />
from asec_phishingapp_review_task
where package_name = ${packageName} AND cer_hash = ${cerHash}
</select>

对于同样的一句 SQL 语句,如:

1
select * from user where name = #{name};
  • #{name} 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符 ?:
1
select * from user where name = ?;
  • ${name} 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换:
1
select * from user where name = "Tom";

#{} 绑定参数,能够防止 SQL 注入,同时谨慎使用 ${}${} 相当于使用字符串拼接 SQL。

(2) 分清 resultMapresultType:

这两个不能同时存在

  • resultMap 自定义映射关系来将表的列名转换为类的字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resultMap id="BaseResultMap" type="com.zk.User" >
<result column="user_name" property="userName" jdbcType="VARCHAR" />
<result column="user_password" property="userPassword" jdbcType="VARCHAR" />
</resultMap>

<sql id="Base_Column_List" >
user_name, user_password
</sql>

<select id="selectBy" resultMap="BaseResultMap">
select <include refid="Base_Column_List" />
from users
where user_name = #{userName} AND user_password = #{userPassword}
</select>
  • resultType 需要通过 AS 来将表的列名映射为类的字段:
1
2
3
4
5
6
7
8
9
<sql id="Base_Column_List" >
user_name AS userName, user_password AS userPassword
</sql>

<select id="selectBy" resultType="com.zk.User">
select <include refid="Base_Column_List" />
from users
where userName = #{userName} AND userPassword = #{userPassword}
</select>

如果不加 AS 的话,那么就会直接将表的列名映射为类的字段

批量更新

在数据库中使用批量更新有助于提高性能:

1) 配置批量更新:

1
2
3
4
5
<configuration>
<settings>
<setting name="defaultExecutorType" value="BATCH"/>
</settings>
</configuration>

2) Java 代码开启:

1
sqlSessionFactory.openSession(ExecutorType.BATCH);

MyBatis Generator

1
mvn mybatis-generator:generate

一篇教程足以

(1) 如何防止覆盖已经生成的文件:

注意这个是要放到 pom.xml 文件中的,不是 generatorConfig.xml 文件中:

1
2
3
<properties>
<mybatis.generator.overwrite>false</mybatis.generator.overwrite>
</properties>

(2) 在源码编译之前生成 Java 接口和 XML 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>

(3) 添加mysql-connector 依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.43</version>
</dependency>
</dependencies>
</plugin>

(4) 指定配置文件参数:

1
2
3
4
<properties>
# 这个就是默认路径
<mybatis.generator.configurationFile>${basedir}/src/main/resources/generatorConfig.xml</mybatis.generator.configurationFile>
</properties>

只需要在 properties 里加上这句话就可以了,插件运行的时候,能够自动找到这句话,进行相应的配置。

(5) 配置文件参数详解:

MyBatis GeneratorXML Configuration File Reference


5.1 怎样连接数据库:

1
2
3
4
5
6
7
8
9
10
<generatorConfiguration>
<properties resource="database.properties"></properties>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<jdbcConnection driverClass="${driver}"
connectionURL="${url}"
userId="${username}"
password="${password}">
</jdbcConnection>
</context>
</generatorConfiguration>

下面是 database.properties 的一个示例 (对应的是 mysql-connector-5.x 版本):

1
2
3
4
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test_db
username=root
password=root
  • id: 这个 Context 的唯一的标识符

关于 Context进一步解释


5.2 javaTypeResolver

javaTypeResolver 用来计算从数据库列如何转变为 Java 类型forceBigDecimals 的默认值就是 false,即默认情况下数据库的 DECIMALNUMERIC 这两种数据库类型会转变为 JavaLong, Integer, Short, 等相应类型。如果这不是你想要的效果,那么请改为 true:

1
2
3
4
5
6
7
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
</context>
</generatorConfiguration>

5.3 javaModelGenerator:

用来定义 Java 模型生成器的属性,即生成一些 JavaBean:

1
2
3
4
<javaModelGenerator targetPackage="com.buptnsrc.appcrawl.entity" targetProject="src/main/java">
<property name="enableSubPackages" value="true"></property>
<property name="trimStrings" value="true"></property>
</javaModelGenerator>
  • targetPackage: 生成的 JavaBean 类放在哪个包下面,MyBatis 会自动创建包
  • targetProject: 指定从项目根路径到包顶级的路径,MyBatis 不会自动创建文件夹
  • enableSubPackages: 如果 trueMBG 会根据 catalogschema 来生成子包。如果 false 就会直接用 targetPackage 属性。默认为 false
  • trimStrings: 是否对数据库查询结果进行 trim 操作,如果设置为 true 就会生成类似这样:
1
2
3
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
}

setter 方法。默认值为 false


5.4 sqlMapGenerator

生成 Mapper.xml 文件:

1
2
3
<sqlMapGenerator targetPackage="com.zk.mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"></property>
</sqlMapGenerator>

5.5 javaClientGenerator

生成 Mapper.java 文件:

1
2
3
4
<javaClientGenerator targetPackage="com.zk.mapper" targetProject="src/main/java"
type="XMLMAPPER">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>

5.6 table:

1
2
3
<table tableName="model_monitor">
<property name="useActualColumnNames" value="false"/>
</table>
  • useActualColumnNames: If true, then MBG will use 列名 as returned from the database metadata as the properties of the generated domain objects. If false (默认 false), MBG will attempt to 驼峰 the returned names. In either event, the name can be specified 明确地 by the <columnOverride> element in which case this property will be ignored for the specified column. For example, suppose a table contains a START_DATE . If the value of this property is “true”, then MBG will generate the property name as START_DATE - meaning that the getters and setters for the value will be getSTART_DATE() and setSTART_DATE(). If the value of this property is false, then MBG will generate the 属性名 as startDate - meaning that the getters and setters for the value will be getStartDate() and setStartDate().

(6) 元素定义顺序问题

generatorConfig.xml<context> 下面的子元素的顺序可不是任意的,这点很让人疑惑,它必须遵从一定的顺序,而不是像常见的那样任意顺序均可:

如果顺序有误,会提示:

1
2
3
4
5
6
[ERROR] Failed to execute goal org.mybatis.generator:mybatis-generator
-maven-plugin:1.3.5:generate (default-cli) on project mybatis: XML Parser
Error on line 35: The content of element type "context" must match
"(property*,plugin*,commentGenerator?,(connectionFactory|jdbcConnection),
javaTypeResolver?,javaModelGenerator,sqlMapGenerator?,javaClientGenerator?,
table+)". -> [Help 1]

网友的回答中最后也指出: Maybe you want to know how to make the element out of order, question How to define DTD without strict element order could be useful.

(7) Cannot obtain primary key information from the database, generated objects may be incomplete

使用 mysql-connector-6.0.6mybatis-generator-core-1.3.5 这两个相结合,在编译完成之后所生成的 Mapper 中,只有两个方法:

1
2
3
4
public interface StudentMapper {
int insert(Student record);
List<Student> selectAll();
}

为什么只有两个方法,为什么缺失了其它一些常见的方法,用过 MyBatis Generator 的同学,肯定知道它不止自动生成这两个方法。再查看 mvn compileConsole 信息来看,发现有一条 Warning:

1
[WARNING] Cannot obtain primary key information from the database, generated objects may be incomplete

带着这句话来 Google 搜索,相关网站聊聊无几,但是却找到了这句话的源文件:

于是将 mybatis-generator 整个项目 clone 到本地,想调试一下看究竟哪里出现了问题。

如下图所示,原来在调用 databaseMetaData.getPrimaryKeys 的时候,源代码只是简单的给出了警告信息 warnings.add(getString("Warning.15")) ,但是异常信息我们却没有任何方式知道,因此修改源代码添加了 warnings.add(e.toString()) 这句代码以便打印出异常信息:

接着使用 mvn package 重新打包生成 mybatis-generator-core-1.3.6-SNAPSHOT.jar。因为生成 Mapper 的过程需要用到 mysql-connector.jar ,因此也需要拷贝 mysql-connector-java-6.0.6.jar 过来,才能在命令行运行程序:

1
java -cp mybatis-generator-core-1.3.6-SNAPSHOT.jar:mysql-connector-java-6.0.6.jar org.mybatis.generator.api.ShellRunner -configfile generatorConfig.xml -overwrite

随后我们在刚才的 Warning 下面看到了这个异常信息:

1
java.sql.SQLSyntaxErrorException: Unknown table 'student' in information_schema

结合源代码,我们知道原来是在调用方法 databaseMetaData.getPrimaryKeys 这句话的时候引发了 SQL 语句错误,导致出现异常并给出了如下警告信息。但为什么会出现语句错误问题呢?

java.sql.DatabaseMetaData 只是一个接口,这句话的实现类是由各个数据库厂商实现的,那么此时我们应该在 mysql-connector 中寻找究竟是哪个类实现了这个接口,使用如下命令重新尝试运行程序:

1
java -verbose:class -cp mybatis-generator-core-1.3.6-SNAPSHOT.jar:mysql-connector-java-6.0.6.jar org.mybatis.generator.api.ShellRunner -configfile generatorConfig.xml -overwrite | grep MetaData

上述命令帮助我们找到了真正的 DatabaseMetaData 的实现类 com.mysql.cj.jdbc.DatabaseMetaData:

接下来我们需要调试 mysql-connector 的实现了,具体需要:

1
com.mysql.cj.build.jdk=/usr/lib/jvm/oracle_jdk8/jdk1.8.0_144
  • 修改 build.xml 文件中的 javassist 库的版本信息,提升一下版本 (需要下载 javassist.jar 并将其放到 lib 文件夹下面):

如果不修改直接使用版本 3.15.0 编译的话,会报如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[java] Exception in thread "main" java.lang.RuntimeException: java.io.IOException: invalid constant type: 18
[java] at javassist.CtClassType.getClassFile2(CtClassType.java:204)
[java] at javassist.CtClassType.makeFieldCache(CtClassType.java:835)
[java] at javassist.CtClassType.getMembers(CtClassType.java:826)
[java] at javassist.CtClassType.getDeclaredMethod(CtClassType.java:1205)
[java] at instrumentation.CommonChecks.main(CommonChecks.java:68)
[java] Caused by: java.io.IOException: invalid constant type: 18
[java] at javassist.bytecode.ConstPool.readOne(ConstPool.java:1113)
[java] at javassist.bytecode.ConstPool.read(ConstPool.java:1056)
[java] at javassist.bytecode.ConstPool.<init>(ConstPool.java:150)
[java] at javassist.bytecode.ClassFile.read(ClassFile.java:765)
[java] at javassist.bytecode.ClassFile.<init>(ClassFile.java:109)
[java] at javassist.CtClassType.getClassFile2(CtClassType.java:191)
[java] ... 4 more

参考网友的回答,这说明这个库中的 Java Parser 还识别不了 Java 8 中的 constant type,所以推荐换用版本更高的 javassist

  • 修改源代码中的 DatabaseMetaData.java 文件,找到方法 getPrimaryKeys 的实现,添加一行打印信息:

  • 使用 ant dist 进行编译

重新运行命令,尝试生成 Mapper,于是我们会得到如下所示的一条 SQL 语句:

1
SHOW KEYS FROM `student` FROM `information_schema`

就是在执行这条 SQL 语句的时候,发生了语法错误。通过从 mysql-connector-6.0.6 降到 5.x 版本能够解决这个问题,相应地打印出的 SQL 语句就变成了:

1
SHOW KEYS FROM `student` FROM `test_db`

(8) 系统缓存问题

没有配置的默认情况下,MyBatis 只开启一级缓存:

1
2
3
4
SqlSession session = sqlSessionFactory.openSession();
StudentMapper studentMapper = session.getMapper(StudentMapper.class);
studentMapper.getStudent(1);
studentMapper.getStudent(1);

我们在同一个 session 上调用了两次查询,而第二次查询是从缓存中查出的。

(9) 原理简介

StudentMapper 接口运行时究竟是什么类型:

而且,它就是在运行时采用的 JDK 的动态代理技术实现的:

(10) 完整版 generatorConfig.xmlmybatis-config.xml 示例

1) generatorConfig.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
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="database.properties" ></properties>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<jdbcConnection driverClass="${driver}"
connectionURL="${url}"
userId="${username}"
password="${password}">
</jdbcConnection>

<javaModelGenerator targetPackage="com.zk" targetProject="src/main/java">
<property name="enableSubPackages" value="true"></property>
<property name="trimStrings" value="true"></property>
</javaModelGenerator>

<sqlMapGenerator targetPackage="com.zk.mapper" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"></property>
</sqlMapGenerator>

<javaClientGenerator targetPackage="com.zk.mapper" targetProject="src/main/java"
type="XMLMAPPER">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>

<table tableName="student">
<property name="useActualColumnNames" value="false"/>
</table>

<table tableName="tasks">
<property name="useActualColumnNames" value="false"/>
</table>
</context>

</generatorConfiguration>

2 mybatis-config.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 configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="database.properties">
</properties>
<typeAliases>
<typeAlias type="com.zk.Student" alias="student" />
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/zk/mapper/StudentMapper.xml"/>
</mappers>
</configuration>

一个类包含其他类、List 对象

1
2
3
4
5
6
7
8
public class Blog implements Serializable {

private int id;
private String title;
private Author author; // Author 对象
private List<Post> posts; // List 对象

}

mybatis-config.xml 配置文件的结构:

1
2
3
4
5
6
<configuration>
<typeAliases>
<!-- 配置别名信息 -->
<typeAlias type="com.zk.Blog" alias="Blog" />
</typeAliases>
</configuration>

BlogMapper.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
34
35
36
<mapper>
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
<!-- 连接 Blog, Author, Post 这三个表 -->
</select>

<!-- Blog 映射规则 -->
<resultMap id="detailedBlogResultMap" type="Blog">

<!-- 构造函数映射 -->
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>

<!-- Blog 类的 title 字段,查询语句的 blog_title 列 -->
<result property="title" column="blog_title" />

<!-- Blog 类的 author 对象字段 -->
<association property="author" resultMap="authorResult" />

<!-- Blog 类的 List<Post> 集合映射 -->
<collection property="posts" ofType="Post">
<id property="id" column="post_id" />
<result property="content" column="post_content" />
</collection>

</resultMap>

<!-- Author 映射规则 -->
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
</resultMap>

</mapper>

解析器

1
"SELECT * FROM ${tableName:users} ORDER BY ${orderColumn:id}"

解析为:

1
SELECT * FROM members ORDER BY member_id

GenericTokenParser 负责扫描文本,当遇到需要解析的指定字符串后,会调用 TokenHandler 接口进行处理:




异常都是没有抛出的,继承的是 RuntimeException:

运行时异常 represent problems that are the result of a 编程问题, and as such, the API client code 没有义务 be expected to 恢复 from them or to handle them in any way. Such problems include arithmetic exceptions, such as dividing by zero; pointer exceptions, such as trying to access an object through a null reference; and indexing exceptions, such as attempting to access an array element through an index that is too large or too small.

Runtime exceptions can occur anywhere in a program, and in a typical one they can be very numerous. Having to add runtime exceptions in every method declaration would reduce a program’s clarity. Thus, the compiler does not require that you catch or specify runtime exceptions (although you can).

<include>

This element can be used to define a 可重用 fragment of SQL code that can be included in other statements. It can be statically (during load phase) parametrized. Different property values can vary in include instances. For example:

1
2
3
4
5
6
7
8
9
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>

反射工具箱

gettersetterMethod 方法用 Invoker 统一封装。

DataSource

1. PooledConnection 代理 close 方法,进行假关闭

1
2
3
4
5
6
7
if (method.getName().equals("close")) {
// 当调用 close 方法的时候,归还 `Connection`
dataSource.pushConnection(this);
return null;
} else {
return method.invoke(realConnection, args);
}

2. PooledDataSourcepushConnection 归还 Connection 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
synchronized (state) {
// 从活跃连接中移除这个 Connection
state.activeConnections.remove(conn);
if (conn.isValid()) {
// 空闲连接还没有达到最大
if (state.idleConnections.size() < poolMaximumIdleConnections) {
// ...
// 复用原来的 realConnection,创建一个新的 PooledConnection
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
// 将新创建的 newConn 放到空闲连接池中
state.idleConnections.add(newConn);
// 唤醒阻塞在 state 上的线程
state.notifyAll();
} else {
// ...
// 直接关闭这个连接
conn.getRealConnection().close();
}
} else {
// 坏连接数量++
state.badConnectionCount++;
}
}

3. PooledDataSourcepopConnection 获取 Connection 方法

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
while (conn == null) {

synchronized (state) {
if (!state.idleConnections.isEmpty()) {
// 有空闲连接
conn = state.idleConnections.remove(0);
} else {
// 可以创建新的连接
if (state.activeConnections.size() < poolMaximumActiveConnections) {
conn = new PooledConnection(dataSource.getConnection(), this);
} else {
// 最旧的那个连接
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
// 连接已经超时
if (longestCheckoutTime > poolMaximumCheckoutTime) {
// 移除这个连接
state.activeConnections.remove(oldestActiveConnection);
// 创建新的连接
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
} else {
// 等待一段时间
state.wait(poolTimeToWait);
}
}
}

// ==============

if (conn != null) {
// 连接有效
if (conn.isValid()) {
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
// 添加到活跃连接中
state.activeConnections.add(conn);
}
}

}

}

4. 如何统计超时

没有设置什么定时任务,隔一段时间扫描一次,而是当获取新的 Connection 的时候,才会尝试移除超时的 Connection

5. 统计什么

  • 坏连接数量
  • 请求 getConnection 的数量
  • 平均请求 getConnection 时长
  • 平均超时时间
  • 等待次数
  • 平均等待时间

6. 如何判断连接是否有效

  • valid 值 + ping 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
boolean result = true;

try {
result = !conn.getRealConnection().isClosed();
} catch (SQLException e) {
result = false;
}

if (result) {
// 如果超过 poolPingConnectionsNotUsedFor 未使用
if (conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
// 发送测试 SQL 语句
try {
Connection realConn = conn.getRealConnection();
Statement statement = realConn.createStatement();
ResultSet rs = statement.executeQuery(poolPingQuery);
rs.close();
statement.close();
} catch (Exception e) {
result = false;
}
}
}

7. finalize() 方法

当这个类 GC 的时候,就关闭所有连接:

1
2
3
4
protected void finalize() throws Throwable {
forceCloseAll();
super.finalize();
}

binding

1
2
3
4
5
6
public interface BlogMapper {

// 此方法是如何映射到 Mapper.xml 文件里面的 id="selectBlog" 的 SQL 语句的 ?
public Blog selectBlog(int index);

}

1. MapperRegistry

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
public class MapperRegistry {

// Mapper 接口与对应的 MapperProxyFactory 之间的关系
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers =
new HashMap<Class<?>, MapperProxyFactory<?>>();

public <T> void addMapper(Class<T> type) {
// 接口类型
if (type.isInterface()) {
// 确保不存在这个类型,否则抛出异常
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 添加进去
knownMappers.put(type, new MapperProxyFactory<T>(type));
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

}

2. 当我们调用 sqlSession.getMapper(BlogMapper.class) 的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MapperRegistry {

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}

try {
// 创建一个代理对象
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

}

MapperProxyFactory 负责创建代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MapperProxyFactory<T> {

private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

// 创建一个 MapperProxy InvokeHandler
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}

}

MapperProxy 的实现:

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
public class MapperProxy<T> implements InvocationHandler, Serializable {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// Object 对象上的方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
// java 7 以上版本对动态语言的支持, default 关键字
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}

// 从缓存中取出或者创建新的 Method 对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 执行
return mapperMethod.execute(sqlSession, args);
}

/**
* Backport of java.lang.reflect.Method#isDefault()
*/
private boolean isDefaultMethod(Method method) {
return (method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
&& method.getDeclaringClass().isInterface();
}
}

MapperMethod 是连接 Mapper 和 映射配置文件中定义的 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
public class MapperMethod {

private final SqlCommand command;
private final MethodSignature method;

public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}

case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
// ...
}
return result;
}

}

缓存

启用二级缓存

1
2
3
4
<settings>
<!-- 缓存总开关 -->
<setting name="cacheEnabled" value="true"/>
</settings>

二级缓存

<select> 节点中的 useCache 属性,将其设置为 true 将会使结果缓存到二级缓存中:

useCache

查询二级缓存的算法流程:

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
/**
* CachingExecutor.java
*/
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {

// 获取查询语句所在命名空间对应的二级缓存
Cache cache = ms.getCache();

// 是否开启了二级缓存
if (cache != null) {

// 根据 <select> 配置是否需要清空二级缓存
flushCacheIfRequired(ms);

// useCache 是否为 true
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);

// 查询二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {

// 调用封装的 Executor ,其会先查询一级缓存
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

// 将查询结果保存到 TransactionalCache.entiresToAddOnCommit 集合中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}

// 调用封装的 Executor ,其会先查询一级缓存
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

commit 的时候,才会将 TransactionalCache.entriesToAddOnCommit 集合中缓存的数据写入到二级缓存,其目的是为了避免出现脏读:

1
2
3
4
5
6
7
8
/**
* CachingExecutor.java
*/
public void commit(boolean required) throws SQLException {
delegate.commit(required);
// 这个时候才会写入
tcm.commit();
}

dirty_read_transaction

清空二级缓存

flushCacheIfRequired 会决定是否调用 clear 方法,TransactionalCache.clear() 方法会设置 clearOnCommittrueTransactionalCache.commit() 方法会根据 clearOnCommit 来决定是否清空二级缓存。

缓存总览

MyBatis 缓存总览:

mybatis_caching

一级缓存的生命周期与 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
30
31
32
33
34
35
36
37
38
/**
* BaseExecutor.java
*/
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);

// 创建 CacheKey
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

// 执行下面版本的查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 开启 flushCache 清空缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}

List<E> list;
try {
queryStack++;

// 查询一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}

// ...

return list;
}

清空一级缓存

回滚、更新、提交

Mapper 方法重载问题

第一种情况:

Mapper.xml定义且只定义一个 <select> 节点:

select

Mapper.java 这边定义多个方法:

select_in_mapper

这种是不会报错的,这样也是 OK 的!

第二种情况:

Mapper.xml 中定义多个 id 相同的 <select> 节点,想通过不同的 parameterType 来表征这两个 select 节点的不同:

multiple_select_id_in_mapper_xml

Mapper.java 这边对应的定义两个方法:

multiple_methods_in_mapper_java

这种是一定会报错的 (以下是部分异常):

1
2
3
4
5
6
7
8
9
10
Caused by: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.buptnsrc.appcrawl.mapper.AsecAppMetaMapper.selectRecentlyRecords
at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:859)
at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:831)
at org.apache.ibatis.session.Configuration.addMappedStatement(Configuration.java:655)
at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:302)
at org.apache.ibatis.builder.xml.XMLStatementBuilder.parseStatementNode(XMLStatementBuilder.java:109)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.buildStatementFromContext(XMLMapperBuilder.java:135)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.buildStatementFromContext(XMLMapperBuilder.java:128)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:118)
... 14 more

以下是关键的几段代码,我们可以看出 MyBatis 通过使用 namespace.id 来唯一定位一个 MappedStatement,而不是通过 namespace.id.parameterType 来定位一个 MappedStatement,因此会抛出异常。

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
/**
* XMLStatementBuilder.java
*/
public void parseStatementNode() {
// id 就是 selectRecentlyRecords
String id = context.getStringAttribute("id");
// ...
builderAssistant.addMappedStatement(id /**, 省略若干参数 **/ );
}

/**
* MapperBuilderAssistant.java
*/
public MappedStatement addMappedStatement(String id /**, 省略若干参数 **/ ) {
id = applyCurrentNamespace(id, false);

MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, /**, 省略若干参数 **/);

MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}

/**
* MapperBuilderAssistant.java
*/
public String applyCurrentNamespace(String base, boolean isReference) {
// currentNamespace: com.buptnsrc.appcrawl.mapper.AsecAppMetaMapper
// base: selectRecentlyRecords
return currentNamespace + "." + base;
}

/**
* Configuration.java
*/
protected final Map<String, MappedStatement> mappedStatements
= new StrictMap<MappedStatement>("Mapped Statements collection");

/**
* Configuration.java
*/
public void addMappedStatement(MappedStatement ms) {
// id: 不能重复
mappedStatements.put(ms.getId(), ms);
}

/**
* Configuration.StrictMap
*/
protected static class StrictMap<V> extends HashMap<String, V> {

public V put(String key, V value) {
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains value for " + key);
}

// ...
}

}

第三种情况:

Mapper.xml不定义 <select> 节点,转而在 Mapper.java 中的方法中通过 Select 注解来声明 SQL 语句:

mapper_select_annotation

这种也是不行的,报如下异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
Caused by: java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.buptnsrc.appcrawl.mapper.AsecAppMetaMapper.selectRecentlyRecords
at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:859)
at org.apache.ibatis.session.Configuration$StrictMap.put(Configuration.java:831)
at org.apache.ibatis.session.Configuration.addMappedStatement(Configuration.java:655)
at org.apache.ibatis.builder.MapperBuilderAssistant.addMappedStatement(MapperBuilderAssistant.java:302)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement(MapperAnnotationBuilder.java:351)
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:134)
at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72)
at org.apache.ibatis.binding.MapperRegistry.addMappers(MapperRegistry.java:97)
at org.apache.ibatis.binding.MapperRegistry.addMappers(MapperRegistry.java:105)
at org.apache.ibatis.session.Configuration.addMappers(Configuration.java:724)
at org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement(XMLConfigBuilder.java:360)
at org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration(XMLConfigBuilder.java:118)

其原因看如下实现代码即可知:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* MapperAnnotationBuilder.java
*/
void parseStatement(Method method) {
// ...
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
// 类的全限定名 + '.' + 方法名 来唯一确定一个 MappedStatement
final String mappedStatementId = type.getName() + "." + method.getName();
// 添加到 Map 中
assistant.addMappedStatement(mappedStatementId);
}
}

其使用 MapperAnnotationBuilder 进行注解解析的时候,确定 ID 的时候用的是:

1
type.getName() + "." + method.getName()

LIKE 语句写法

The bind element lets you create a variable (变量) out of an OGNL expression and bind it to the context.

LIKE_example

字符串连接:

1
2
3
4
5
6
7
8
<select id="xxx" ...>
select id,name,... from country
<where>
<if test="name != null and name != ''">
name like concat('%', #{name}, '%')
</if>
</where>
</select>

SQL Builder

MyBatis 提供了 SQL Builder 类来帮助开发者使用 Java 代码来构建 SQL 语句

推荐文章