0%

Mybatis

1、简介

1.1、什么是Mybatis

  • MyBatis 是一款优秀的持久层框架。
  • 它支持自定义 SQL、存储过程以及高级映射。
  • MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
  • MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
  • MyBatis 本是apache的一个开源项目 iBatis。
  • 2010年这个项目由 apache software foundation 迁移到了 google code,并且改名为MyBatis。
  • 2013年11月迁移到 Github。

如何获取 Mybatis?

1.2、持久化

数据持久化

  • 持久化就是将程序的数据在持久状态和瞬时状态转化的过程。
  • 内存断电即失

为什么需要持久化?

  • 有一些对象不能让他丢掉。
  • 内存太贵了。

1.3、持久层

Dao 层、Service 层、Controller 层……

  • 完成持久化工作的代码块
  • 层界限十分明显。

1.4、为什么需要Mybatis?

  • 传统 JDBC 代码过于复杂冗余,而Mybatis 简洁了很多。
  • 方便简洁自动化,框架在背后帮我们做了很多事情。

2、第一个 Mybatis 程序

添加 maven 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<!--mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<!--test-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

</dependencies>

mybatis 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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>
<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/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!--注册mapper文件-->
<mappers>
<mapper resource="com/yqx/mapper/UserMapper.xml"/>
</mappers>
</configuration>

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.yqx.pojo;

public class User {
private int id;
private String name;
private int age;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}

public User() {
}

public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
}

mapper.xml

1
2
3
4
5
6
7
8
9
<?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.yqx.mapper.UserMapper">
<select id="findUserById" resultType="com.yqx.pojo.User">
select * from mybatis.user where id = #{id}
</select>
</mapper>

mapper.xml 放在 java 类中。不会被 maven 读取,需要在 pom.xml 中显式定义路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>

mapper 类

1
2
3
4
5
6
7
package com.yqx.mapper;

import com.yqx.pojo.User;

public interface UserMapper {
User findUserById(int id);
}

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
package com.yqx.utils;

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 MyBatisUtil {
private static SqlSessionFactory sqlSessionFactory;

static {
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}

public static SqlSession getSession(){
return sqlSessionFactory.openSession();
}
}

Test 测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.yqx.mapper;

import com.yqx.utils.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

public class MapperTest {
@Test
public void test(){
SqlSession sqlSession = MyBatisUtil.getSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println(mapper.findUserById(5));
}
}

3、CRUD

3.1、namespace

namespace 中的类名要和 Dao/Mapper 接口名一致!

3.2、sql语句

  • id:对应 namespace 中的方法名。

  • resultType:sql 语句执行的返回值。

  • parameterType:参数类型。

mapper

增删改会返回修改的记录数。

1
2
3
4
5
6
7
public interface UserMapper {
User findUserById(int id);
List<User> findAllUsers();
int deleteUserById(int id);
int updateUser(User user);
int addUser(User user);
}

对于 mapper 中的 sql 语句,需要遵守以下规则。

  • 返回值为基本类型时可以不写。
  • 参数列表只有一个时,#{} 中的形参名可以随便写。
  • 传入实体类时,形参名要和实体类中的属性名一致。

select

1
2
3
4
5
6
7
<select id="findUserById" resultType="com.yqx.pojo.User">
select * from mybatis.user where id = #{id}
</select>

<select id="findAllUsers" resultType="com.yqx.pojo.User">
select * from mybatis.user
</select>

delete

1
2
3
<delete id="deleteUserById">
delete from mybatis.user where id = #{id}
</delete>

update

1
2
3
<update id="updateUser">
update mybatis.user set name = #{name}, age = #{age} where id = #{id}
</update>

insert

1
2
3
<insert id="addUser">
insert into mybatis.user (name, age) values(#{name}, #{age})
</insert>

注:默认情况下,使用 #{} 参数语法时,MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。 这样做更安全,更迅速,通常也是首选做法。

3.3、万能Map

普通的 update 方法需要传入一个对象,即便只需要改一个字段。而 map 方法可以指定传入的内容,从而达成更改指定的字段。

mapper 接口添加方法

1
int updateUserName(Map map);

mapper.xml

1
2
3
<update id="updateUserName">
update mybatis.user set name = #{user_name} where id = #{user_id}
</update>

test

1
2
3
4
5
6
7
8
9
10
@Test
public void test6(){
SqlSession sqlSession = MyBatisUtil.getSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("user_id", 5);
map.put("user_name", "龙傲天");
System.out.println(mapper.updateUserName(map));
System.out.println(mapper.findAllUsers());
}

3.4、模糊查询

mapper 接口添加方法

1
List<User> findUserLike(String value);

方法一

mapper.xml

一定要用双引号而不是单引号,会报错!

1
2
3
<select id="findUserLike" resultType="com.yqx.pojo.User">
select * from mybatis.user where name like "%"#{value}"%"
</select>

test

1
2
3
4
5
6
@Test
public void test7(){
SqlSession sqlSession = MyBatisUtil.getSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println(mapper.findUserLike("天"));
}

方法二

也可以将 % 放在参数里

mapper.xml

1
2
3
<select id="findUserLike" resultType="com.yqx.pojo.User">
select * from mybatis.user where name like #{value}
</select>

test

1
2
3
4
5
6
@Test
public void test7(){
SqlSession sqlSession = MyBatisUtil.getSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
System.out.println(mapper.findUserLike("%天%"));
}

4、配置

4.1、核心配置属性

  • mybatis-config.xml
  • Mybatis 的配置文件包含了会深深影响 Mybatis 行为的设置和属性信息。
  • 写的时候按照下面的顺序,有顺序之分!!!
1
2
3
4
5
6
7
8
9
10
11
12
13
configuration(配置)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)

4.2、环境配置(environment)

Mybatis 可以配置多套环境,但每个 SqlSessionFactory 实例只能选择一种环境。

Mybatis 默认的事务管理器就是 JDBC,连接池 POOLED

4.3、属性(properties)

我们可以通过 properties 属性来引用配置文件。

这些属性可以在外部进行配置,并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性,也可以在 properties 元素的子元素中设置。

1
2
3
4
<properties resource="db.properties">
<property name="sql_driver" value="com.mysql.jdbc.Driver"/>
<property name="sql_url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
</properties>

db.properties

1
2
username = root
pwd = 123456

若有两者有冲突,会优先使用 properties 文件中的值。

设置完后可以在整个配置文件中替换需要动态配置的属性值。

1
2
3
4
5
6
7
8
9
10
11
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${sql_driver}"/>
<property name="url" value="${sql_url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${pwd}"/>
</dataSource>
</environment>
</environments>

4.3、类型别名(typeAliases)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。

方法一:直接设置别名

1
2
3
<typeAliases>
<typeAlias type="com.yqx.pojo.User" alias="User"/>
</typeAliases>

当这样配置时,User 可以用在任何使用 com.yqx.pojo.User 的地方。

1
2
3
<select id="findAllUsers" resultType="User">
select * from mybatis.user
</select>

方法二:扫描包

1
2
3
<typeAliases>
<package name="com.yqx.pojo"/>
</typeAliases>

默认别名是类名。(大小写无所谓)

1
2
3
<select id="findAllUsers" resultType="uSer">
select * from mybatis.user
</select>

可以添加注解 @Alias 来更改别名。

1
2
3
4
5
6
@Alias("userAlias")
public class User {
private int id;
private String name;
private int age;
}
1
2
3
<select id="findAllUsers" resultType="userAlias">
select * from mybatis.user
</select>

4.4、设置(settings)

设置内容很多,这里只挑重要的记录。

设置名 描述 有效值 默认值
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 true | false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 true | false false
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING 未设置

4.5、映射器(mappers)

既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。

1. 使用相对于类路径的资源引用

1
2
3
4
5
<mappers>
<mapper resource="com/yqx/mapper/AuthorMapper.xml"/>
<mapper resource="com/yqx/mapper/BlogMapper.xml"/>
<mapper resource="com/yqx/mapper/PostMapper.xml"/>
</mappers>

2. 使用映射器接口实现类的完全限定类名)

1
2
3
4
5
<mappers>
<mapper class="com.yqx.mapper.AuthorMapper"/>
<mapper class="com.yqx.mapper.BlogMapper"/>
<mapper class="com.yqx.mapper.PostMapper"/>
</mappers>

3. 将包内的映射器接口实现全部注册为映射器

1
2
3
<mappers>
<package name="com.yqx.mapper"/>
</mappers>

注:第2、3种方法必须遵守下述规范

  • 接口和 mapper 配置文件必须同名!
  • 接口和 mapper 配置文件必须在同一个包下!

5、生命周期

不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。

SqlSessionFactoryBuilder

  • 一旦创建了 SqlSessionFactroy,就不再需要它了。(妥妥的工具人)
  • 局部变量

SqlSessionFactory

  • 可以理解为数据库连接池。
  • 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。
  • 最佳作用域是应用作用域(application)。
  • 建议使用单例模式或者静态单例模式。

SqlSession

  • 每个线程都应该有它自己的 SqlSession 实例。
  • SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
  • 用完之后必须关闭,否则资源会被占用!

6、结果集映射(ResultMap)

用于解决实体类字段和 Sql 字段不匹配的问题。

1
2
3
4
<!--正常sql-->
select * from mybatis.user
<!--完整sql-->
select id, name, age from mybatis.user

可是 User 的属性只有 id,userName 和 userAge,因此再返回结果的时候就会出现属性无法匹配而是用默认值的问题。

可以看到查询出来的 User 没有 userName 和 userAge 的值。

1
2
3
[User{id=5, userName='null', userAge=0}, User{id=7, userName='null', userAge=0}, User{id=34, userName='null', userAge=0}, User{id=4, userName='null', userAge=0}, User{id=35, userName='null', userAge=0}, User{id=33, userName='null', userAge=0}]

Process finished with exit code 0

解决方法有二。

其一:使用指定列名

这样就能和实体类的字段名对应起来了。

1
select id, name userName, age userAge from mybatis.user

其二:ResultMap

1
2
3
4
5
6
7
8
9
10
<!--创建resultMap,只需要映射和sql字段不同属性即可-->
<resultMap id="userMap" type="userAlias">
<result column="name" property="userName"/>
<result column="age" property="userAge"/>
</resultMap>

<!--引用resultMap-->
<select id="findAllUsers" resultType="userAlias" resultMap="userMap">
select * from mybatis.user
</select>
  • resultMap 元素是 MyBatis 中最重要最强大的元素。

  • ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

7、日志

Mybatis 通过使用内置的日志工厂提供日志功能。内置日志工厂将会把日志工作委托给下面的实现之一。

  • SLF4J

  • Apache Commons Logging

  • Log4j 2

  • Log4j 【掌握】

  • JDK logging

  • STDOUT_LOGGING 【掌握】

在 mybatis-config.xml 的 setting 中设置。

可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了 org.apache.ibatis.logging.Log 接口,且构造方法以字符串为参数的类完全限定名。

一定要严格按照上述可选值,大小写错误亦或是多了空格都会报错!

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

7.1、STDOUT_LOGGING

标准日志,个人强推!十分方便,配置完了就可以使用。效果如下。

1
2
3
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

7.2、Log4j

什么是Log4j ?

  • 可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等。
  • 可以控制每一条日志的输出格式。
  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。
  • 可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

先导入依赖。

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

设置日志实现。

1
2
3
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>

创建 log4j.properties,可定制化打印内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file

#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n

#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

之后便能使用了。

也可以创建 Logger 对象,手动打印信息。

1
2
3
4
5
6
7
8
9
10
public class MapperTest{
static Logger logger = Logger.getLogger(MapperTest.class);

@Test
public void test2(){
logger.info("我是提示信息。");
logger.debug("我是测试信息。");
logger.warn("我是警告信息!");
}
}

8、分页

8.1、Limit 分页(建议)

添加接口方法

1
List<User> findUserLimit(Map map);

为该方法设置 sql 语句

1
2
3
<select id="findUserLimit" resultType="user">
select * from mybatis.user limit #{start}, #{offset}
</select>

测试

1
2
3
4
5
6
7
8
9
@Test
public void test1(){
SqlSession sqlSession = MyBatisUtil.getSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Map<String, Integer> map = new HashMap<>();
map.put("start", 1);
map.put("offset", 3);
System.out.println(mapper.findUserLimit(map));
}

8.2、RowBounds 分页

添加接口方法

1
List<User> findUserRowBounds(Map map);

为该方法设置 sql 语句

1
2
3
<select id="findUserRowBounds" resultType="user">
select * from mybatis.user
</select>

测试

1
2
3
4
5
6
7
8
@Test
public void test1(){
SqlSession sqlSession = MyBatisUtil.getSession();
List<User> userList = sqlSession.selectList("com.yqx.mapper.UserMapper.findUserRowBounds",
null,
new RowBounds(1, 3));
System.out.println(userList);
}

9、注解

使用注解可以省去 mapper.xml 这个配置文件,所有 sql 语句都写在注解中即可,但无法胜任复杂的 sql 语句也不能使用 ResultMap 等功能。

9.1、 CRUD

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

import com.yqx.pojo.User;
import org.apache.ibatis.annotations.*;
import java.util.List;

public interface UserMapper {
@Select("select * from mybatis.user")
List<User> findAllUsers();

@Select("select * from user where id = #{id}")
User findUserById(@Param("id") int user_id);

@Delete("delete from user where id = #{id}")
int deleteUserById(@Param("id") int user_id);

@Update("update user set name = #{name}, age = #{age} where id = #{id}")
int updateUser(User user);

@Insert("insert into user(name, age) values(#{name}, #{age})")
int addUser(User user);
}

@Param 可以指定输入参数名字。

9.2、注意

mybatis-config.xml 中,不能使用 resource 来引入 mapper.xml 文件,毕竟没有了嘛。

1
2
3
4
5
<mappers>
<!--下述任选其一-->
<!--<mapper class="com.yqx.mapper.UserMapper"/>-->
<package name="com.yqx.mapper"/>
</mappers>

10、Mybatis 的工作原理

(1)读取 MyBatis 配置文件 mybatis-config.xml。mybatis-config.xml 作为MyBatis 的全局配置文件,配置了MyBatis 的运行环境等信息,其中主要内容是获取数据库连接

(2)加载映射文件Mapper.xml。Mapper.xml 文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在mybatis-config.xml 中加载才能执行。mybatis-config.xml 可以加载多个配置文件,每个配置文件对应数据库中的一张表。

(3)构建会话工厂。通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。

(4)创建SqlSession对象。由会话工厂创建 SqlSession对象,该对象中包含了执行 SQL的所有方法。

(5)MyBatis底层定义了一个Executor 接口来操作数据库,它会根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

(6)在Executor接口的执行方法中,包含一个 MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SOL语句的id、参数等。Mapper.xmI 文件中一个SQL对应一个MappedStatement对象,SOL的id即是MappedStatement 的id。

(7)输入参数映射。在执行方法时,MappedStatement对象会对用户执行 SQL语句的输入参数进行定义(可以定义为Map、List类型、基本类型和POJO类型),Executor执行器会通过MappedStatement对象在执行SQL前,将输入的Java对象映射到SQL语句中。这里对输入参数的映射过程就类似于JDBC 编程中对preparedStatement对象设置参数的过程。

(8)输出结果映射。在数据库中执行完SQL语句后,MappedStatement对象会对SQL执行输出的结果进行定义(可以定义为Map和List类型、基本类型、POJO类型),Executor 执行器会通过MappedStatement对象在执行SQL语句后,将输出结果映射至Java对象中。这种将输出结果映射到Java对象的过程就类似于JDBC编程中对结果的解析处理过程。

11、多对一查询

至此,我们已经学会了数据库的基础查询,但那儿也只是个开始,真实的情况远远可比单表查询复杂得多。

  • 多个学生,对应一个老师
  • 对于学生,关联,多个学生,关联多个学生 【多对一】
  • 对于老师,集合,一个老师,拥有很多学生 【一对多】

先创建数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE `teacher` (
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8

INSERT INTO teacher(`id`, `name`) VALUES (1, 秦老师);
INSERT INTO teacher(`id`, `name`) VALUES (2, 余老师);

CREATE TABLE `student` (
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
`tid` INT(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fktid` (`tid`),
CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8

INSERT INTO `student` (`id`, `name`, `tid`) VALUES (1, 小明, 1);
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (2, 小红, 2);
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (3, 小张, 1);
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (4, 小李, 2);
INSERT INTO `student` (`id`, `name`, `tid`) VALUES (5, 小王, 1);

正常情况下,要查询一个学生的 id,name 和 老师的 name 需要多表查询。

1
2
3
select s.id, s.name, t.name
from student s, teacher t
where s.id = t.id
id s.name t.name
1 小明 秦老师
2 小红 余老师

但很显然,Mybatis 不能直接这么写,可以使用以下两种方法进行查询。

11.1、按照查询嵌套处理

将对 teacher 表的查询结果作为 Student 的属性 teacher。

1
2
3
4
5
6
7
8
9
10
11
12
<!--方式1 嵌套查询-->
<select id="findStudentById1" resultMap="StudentMap">
select * from mybatis.student where id = #{id}
</select>

<resultMap id="StudentMap" type="Student">
<association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
</resultMap>

<select id="getTeacher" resultType="Teacher">
select * from mybatis.teacher where id = #{tid}
</select>

结果

1
2
3
Student(id=1, name=小明, teacher=Teacher(id=1, name=秦老师))

Process finished with exit code 0

11.2、按照结果嵌套处理

将查询出来的 tname 作为属性直接传给 Teacher 对象。

1
2
3
4
5
6
7
8
9
10
11
12
<!--方法2 联表查询-->
<select id="findStudentById2" resultMap="StudentMap2">
select s.id sid , s.name sname, t.name tname from student s, teacher t where s.tid = t.id and s.id = #{id}
</select>

<resultMap id="StudentMap2" type="Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<association property="teacher" javaType="Teacher">
<result property="name" column="tname"/>
</association>
</resultMap>

要给查询列取别名,不然 resultMap 无法获取到

12、一对多查询

12.1、按照查询嵌套处理

1
2
3
4
5
6
7
8
9
10
11
12
<!--按照条件嵌套查询-->
<select id="findTeacherById2" resultMap="teacherMap2">
select * from mybatis.teacher where id = #{id}
</select>

<resultMap id="teacherMap2" type="Teacher">
<collection property="students" javaType="ArrayList" ofType="Student" column="id" select="findStudentByTid"/>
</resultMap>

<select id="findStudentByTid" resultType="Student">
select * from mybatis.student where tid = #{id}
</select>

12.2、按照结果嵌套查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="findTeacherById" resultMap="teacherMap">
select t.id tid, t.name tname, s.name sname, s.id sid
from student s, teacher t
where s.tid = t.id and t.id = #{id}
</select>

<resultMap id="teacherMap" type="Teacher">
<result property="id" column="tid"/>
<result property="name" column="tname"/>
<collection property="students" ofType="Student">
<result property="name" column="sname"/>
<result property="id" column="sid"/>
</collection>
</resultMap>

ofType 是泛型的类型,表示集合中的元素类型

13、动态SQL

动态 SQL 是 MyBatis 的强大特性之一。在 JDBC 或其它类似的框架中,开发人员通常需要手动拼接 SQL 语句。根据不同的条件拼接 SQL 语句是一件极其痛苦的工作。

动态 SQL 大大减少了编写代码的工作量,更体现了 MyBatis 的灵活性、高度可配置性和可维护性。

元素 作用 备注
if 判断语句 单条件分支判断
choose(when、otherwise) 相当于 Java 中的 switch case 语句 多条件分支判断
trim、where 辅助元素 用于处理一些SQL拼装问题
foreach 循环语句 在in语句等列举条件常用
bind 辅助元素 拼接参数

13.1、If

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getBolg" resultType="Blog">
select * from mybatis.blog where 1=1
<if test="id != null">
and id = #{id}
</if>
<if test="author != null">
and author = #{author}
</if>
<if test="title != null">
and title = #{title}
</if>
</select>

参数列表使用的是 map,便于控制查询对象;where 1=1 是为了main便于拼接。

13.2、where

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="getBolg" resultType="Blog">
select * from mybatis.blog
<where>
<if test="id != null">
and id = #{id}
</if>
<if test="author != null">
and author = #{author}
</if>
<if test="title != null">
and title = #{title}
</if>
</where>
</select>

在 if 的外侧套一层 where 标签就可以省去 where 1=1 这个 sql 了。

13.3、choose(when、otherwise)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<select id="getBlogWithSwitch" resultType="Blog">
select * from mybatis.blog
<where>
<choose>
<when test="id != null">
and id = #{id}
</when>
<when test="author != null">
and author = #{author}
</when>
<when test="title != null">
and title = #{title}
</when>
<otherwise>
and views = 9999
</otherwise>
</choose>
</where>
</select>

choose 等同于 switch,when 等同于 case,otherwise 等同于 default。

当有一个 when 成立时,便不会执行后面的 when 语句。若都不满足,则会执行 otherwise。

13.4、set

1
2
3
4
5
6
7
8
9
<update id="updateBlog">
update mybatis.blog
<set>
<if test="name != null">name = #{name},</if>
<if test="author != null">author = #{author},</if>
<if test="title != null">title = #{title},</if>
</set>
where id = #{id}
</update>

set 标签会去掉多余,,但不会添加,,因此在每个 sql 后都加个,就可以了。

13.5、trim

trim 一般用于去除 SQL 语句中多余的 AND 关键字、逗号或者给 SQL 语句前拼接 where、set 等后缀,可用于选择性插入、更新、删除或者条件查询等操作。

属性 描述
prefix 给SQL语句拼接的前缀,为 trim 包含的内容加上前缀
suffix 给SQL语句拼接的后缀,为 trim 包含的内容加上后缀
prefixOverrides 去除 SQL 语句前面的关键字或字符,该关键字或者字符由 prefixOverrides 属性指定。
suffixOverrides 去除 SQL 语句后面的关键字或者字符,该关键字或者字符由 suffixOverrides 属性指定。
1
2
3
4
5
6
7
8
9
<update id="updateBlogWithTrim">
update mybatis.blog
<trim prefix="set" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="author != null">author = #{author},</if>
<if test="title != null">title = #{title},</if>
</trim>
where id = #{id}
</update>

13.6、foreach

Mybatis foreach 标签用于循环语句,它很好的支持了数据和 List、set 接口的集合,并对此提供遍历的功能。语法格式如下。

1
2
3
<foreach item="item" index="index" collection="list|array|map key" open="(" separator="," close=")">
参数值
</foreach>

foreach 标签主要有以下属性,说明如下。

  • item:表示集合中每一个元素进行迭代时的别名。
  • index:指定一个名字,表示在迭代过程中每次迭代到的位置。
  • open:表示该语句以什么开始(既然是 in 条件语句,所以必然以(开始)。
  • separator:表示在每次进行迭代之间以什么符号作为分隔符(既然是 in 条件语句,所以必然以,作为分隔符)。
  • close:表示该语句以什么结束(既然是 in 条件语句,所以必然以)开始)。
1
2
3
4
5
6
<select id="getBolgWithForeach" resultType="Blog">
select * from mybatis.blog where id in
<foreach collection="list" item="item" index="index" open="(" separator="," close=")">
#{item}
</foreach>
</select>

测试代码

1
2
3
4
5
6
7
8
9
10
@Test
public void test5(){
SqlSession session = MyBatisUtil.getSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
List<String> list = new ArrayList<>();
list.add("1");
list.add("3");
list.add("4");
System.out.println(mapper.getBolgWithForeach(list));
}

13.7、SQL 片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<sql id="sqlSegment">
<if test="id != null">
and id = #{id}
</if>
<if test="author != null">
and author = #{author}
</if>
<if test="title != null">
and title = #{title}
</if>
</sql>

<select id="getBolg" resultType="Blog">
select * from mybatis.blog
<where>
<include refid="sqlSegment"></include>
</where>
</select>

顾名思义,将某一段 sql 语句提取出来,以便后续复用!

14、Mybatis 缓存

缓存可以将数据保存在内存中,是互联网系统常常用到的。目前流行的缓存服务器有 MongoDB、Redis、Ehcache 等。缓存是在计算机内存上保存的数据,读取时无需再从磁盘读入,因此具备快速读取和使用的特点。

和大多数持久化框架一样,MyBatis 提供了一级缓存和二级缓存的支持。默认情况下,MyBatis 只开启一级缓存。

14.1、一级缓存

一级缓存是基于 PerpetualCache(MyBatis自带)的 HashMap 本地缓存,作用范围为 session 域内。当 session flush(刷新)或者 close(关闭)之后,该 session 中所有的 cache(缓存)就会被清空。

在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession 对象调用同一个 mapper 的方法,往往只执行一次 SQL。因为使用 SqlSession 第一次查询后,MyBatis 会将其放在缓存中,再次查询时,如果没有刷新,并且缓存没有超时的情况下,SqlSession 会取出当前缓存的数据,而不会再次发送 SQL 到数据库。

查询不同 blog

1
2
3
4
5
6
7
8
@Test
public void test1(){
SqlSession session = MyBatisUtil.getSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.getBlogById(1));
System.out.println("====================================");
System.out.println(mapper.getBlogById(2));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
==>  Preparing: select * from mybatis.blog where id = ? 
==> Parameters: 1(Integer)
<== Columns: id, title, author, create_time, views
<== Row: 1, 紫梦沁香, DeFlory, 2021-02-09 20:16:46.0, 9999
<== Total: 1
Blog(id=1, title=紫梦沁香, author=DeFlory, createTime=Tue Feb 09 20:16:46 CST 2021, views=9999)
====================================
==> Preparing: select * from mybatis.blog where id = ?
==> Parameters: 2(Integer)
<== Columns: id, title, author, create_time, views
<== Row: 2, 动态sql, 狂神说, 2021-02-09 20:16:46.0, 9999
<== Total: 1
Blog(id=2, title=动态sql, author=狂神说, createTime=Tue Feb 09 20:16:46 CST 2021, views=9999)

查询相同 blog

1
2
3
4
5
6
7
8
@Test
public void test1(){
SqlSession session = MyBatisUtil.getSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.getBlogById(1));
System.out.println("====================================");
System.out.println(mapper.getBlogById(1));
}
1
2
3
4
5
6
7
8
==>  Preparing: select * from mybatis.blog where id = ? 
==> Parameters: 1(Integer)
<== Columns: id, title, author, create_time, views
<== Row: 1, 紫梦沁香, DeFlory, 2021-02-09 20:16:46.0, 9999
<== Total: 1
Blog(id=1, title=紫梦沁香, author=DeFlory, createTime=Tue Feb 09 20:16:46 CST 2021, views=9999)
====================================
Blog(id=1, title=紫梦沁香, author=DeFlory, createTime=Tue Feb 09 20:16:46 CST 2021, views=9999)

显而易见,当运行同样的查询语句时,只会返回结果,而不会到数据库中查询。

14.2、二级缓存

二级缓存是全局缓存,作用域超出 session 范围之外,可以被所有 SqlSession 共享。

一级缓存缓存的是 SQL 语句,二级缓存缓存的是结果对象。

二级缓存的配置

(1)MyBatis 的全局缓存配置需要在 mybatis-config.xml 的 settings 元素中设置,代码如下。

1
2
3
<settings>
<setting name="cacheEnabled" value="true" />
</settings>

(2)在 mapper 文件中设置缓存,默认不开启缓存。需要注意的是,二级缓存的作用域是针对 mapper 的 namescape 而言,即只有再次在 namescape 内的查询才能共享这个缓存,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
<mapper namescape="net.biancheng.WebsiteMapper">
<!-- cache配置 -->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true" />
...

<!--
也可以使用<cache/>,参数全默认。
-->
</mapper>
属性 说明
eviction 代表的是缓存回收策略,目前 MyBatis 提供以下策略。LRU:使用较少,移除最长时间不用的对象;FIFO:先进先出,按对象进入缓存的顺序来移除它们;SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象;WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval 刷新间隔时间,单位为毫秒,这里配置的是 60 秒刷新,如果省略该配置,那么只有当 SQL 被执行的时候才会刷新缓存。
size 引用数目,正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。这里配置的是 512 个对象。
readOnly 只读,默认值为 false,意味着缓存数据只能读取而不能修改,这样设置的好处是可以快速读取缓存,缺点是没有办法修改缓存。

(3)在 mapper 文件配置支持 cache 后,如果需要对个别查询进行调整,可以单独设置 cache,代码如下。

1
2
3
<select id="getWebsiteList" resultType="net.biancheng.po.Website" usecache="false">
...
</select>

测试代码

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test1(){
SqlSession session = MyBatisUtil.getSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.getBlogById(1));
session.close();
System.out.println("====================================");
session = MyBatisUtil.getSession();
mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.getBlogById(1));
}

不开启二级缓存,查询了两次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Opening JDBC Connection
Created connection 233996206.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@df27fae]
==> Preparing: select * from mybatis.blog where id = ?
==> Parameters: 1(Integer)
<== Columns: id, title, author, create_time, views
<== Row: 1, 紫梦沁香, DeFlory, 2021-02-09 20:16:46.0, 9999
<== Total: 1
Blog(id=1, title=紫梦沁香, author=DeFlory, createTime=Tue Feb 09 20:16:46 CST 2021, views=9999)
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@df27fae]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@df27fae]
Returned connection 233996206 to pool.
====================================
Opening JDBC Connection
Checked out connection 233996206 from pool.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@df27fae]
==> Preparing: select * from mybatis.blog where id = ?
==> Parameters: 1(Integer)
<== Columns: id, title, author, create_time, views
<== Row: 1, 紫梦沁香, DeFlory, 2021-02-09 20:16:46.0, 9999
<== Total: 1
Blog(id=1, title=紫梦沁香, author=DeFlory, createTime=Tue Feb 09 20:16:46 CST 2021, views=9999)

开启缓存,只查询了一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Opening JDBC Connection
Created connection 1843289228.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6dde5c8c]
==> Preparing: select * from mybatis.blog where id = ?
==> Parameters: 1(Integer)
<== Columns: id, title, author, create_time, views
<== Row: 1, 紫梦沁香, DeFlory, 2021-02-09 20:16:46.0, 9999
<== Total: 1
Blog(id=1, title=紫梦沁香, author=DeFlory, createTime=Tue Feb 09 20:16:46 CST 2021, views=9999)
Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6dde5c8c]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6dde5c8c]
Returned connection 1843289228 to pool.
====================================
Cache Hit Ratio [com.yqx.mapper.BlogMapper]: 0.5
Blog(id=1, title=紫梦沁香, author=DeFlory, createTime=Tue Feb 09 20:16:46 CST 2021, views=9999)

需要将实体类序列化,否则会报错!

14.3、缓存读取顺序

  1. 先查询二级缓存
  2. 在查询一级缓存
  3. 查询数据库

对于 MyBatis 缓存仅作了解即可,因为面对一定规模的数据量,内置的 Cache 方式就派不上用场了,并且对查询结果集做缓存并不是 MyBatis 所擅长的,它专心做的应该是 SQL 映射。对于缓存,采用 OSCache、Memcached 等专门的缓存服务器来做更为合理。