MyBatis与MyBatis-Plus

MyBatis

简介

MyBatis是一款优秀的持久层框架。它支持定制化SQL、存储过程以及高级映射

MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。MyBatis可以通过简单的XML注解来配置和映射原始类型、接口和Java POJO(Plain Old Java Objects,普通老式Java对象)为数据库中的记录

持久化

  • 持久化就是将程序的数据在持久状态瞬时状态转化的过程。
  • 由于内存有断电之后数据就会丢失的特点,有一些对象不能丢失,需要将其持久化。

持久层

  • 完成持久化工作的代码块就是持久层。

传统的JDBC代码太复杂,MyBatis简化了将数据存入数据库这一过程。MyBatis将sql和代码分离,提高了可维护性;提供了映射标签,支持对象与数据库的字段关系的映射(ORM,Object Relation Mapping);提供xml标签,支持编写动态sql


写一个MyBatis程序

搭建数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE DATABASE IF NOT EXISTS mybatis;

USE mybatis;
DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
id INT NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
passwd VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE = InnoDB;

INSERT INTO `user`(id, `name`, passwd)
VALUES (1, '黄铁', '123456'),
(2, '张三', '123456'),
(3, '李四', '123456');

SELECT * FROM `user`;

新建项目

导入相关依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<!-- 连接mysql -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.3</version>
</dependency>
</dependencies>

编写核心配置文件

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例

XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- mybatis 核心配置文件 -->
<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.xml都需要在该配置文件中注册-->
<mappers>
<mapper resource="UserMapper.xml"/>
</mappers>

</configuration>

编写mybatis工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 从sqlSessionFactory获取sqlSession
* sqlSession 完全包含了面向数据库执行SQL命令所需的所有方法
*/
public class MyBatisUtils {
private static SqlSessionFactory sqlSessionFactory;

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

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

编写代码

实体类

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
public class User {
private int id;

private String name;

private String passwd;

public User () {

}

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

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 String getPasswd() {
return passwd;
}

public void setPasswd(String passwd) {
this.passwd = passwd;
}

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

DAO接口

1
2
3
public interface UserDao {
List<User> getUserList();
}

接口实现类由原来的UserDaoImpl转变为一个Mapper配置文件

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace 会绑定一个对应的DAO/Mapper接口 -->
<mapper namespace="com.hunter.dao.UserDao">
<!-- 返回值是list时,resultType填写 List中存放的对象类型 -->
<select id="getUserList" resultType="com.hunter.pojo.User">
select * from user
</select>
</mapper>

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserDaoTest {
@Test
public void test() {
// 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
// 执行sql
UserDao userDao = sqlSession.getMapper(UserDao.class);
List<User> userList = userDao.getUserList();

for (User user : userList) {
System.out.println(user);
}
}
}
}

增加CRUD

将UserDao变更为UserMapper。

  • id

    对应namespace中的方法名。

  • parameterType

    参数类型。

  • resultType

    sql语句执行的返回类型。返回值类型是List时,resultType填写**List中存放的对象类型**。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id="getUserById" parameterType="int" resultType="com.hunter.pojo.User">
select * from user where id = #{id}
</select>

<!-- 对象中的属性,可以直接取出 -->
<insert id="addUser" parameterType="com.hunter.pojo.User">
insert into user (id, name, passwd) values (#{id}, #{name}, #{passwd})
</insert>

<update id="updateUser" parameterType="com.hunter.pojo.User">
update user set name = #{name}, passwd = #{passwd} where id = #{id}
</update>

<delete id="deleteUser" parameterType="int">
delete from user where id = #{id}
</delete>

测试

注意:增删改需要提交事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void should_success_when_delete_user() {
// 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
// 执行sql
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

int result = userMapper.deleteUser(4);
if (result > 0) {
System.out.println("删除成功");
}

// 提交事务
sqlSession.commit();
}
}

用 Map 传参

如果实体类或数据库中的表,**字段或者参数过多,可以考虑使用Map:parameterType="map"**,传参geng。

1
2
3
4
5
6
7
/**
* 修改用户密码
*
* @param map 通过map传参,无需构造一个完整的user
* @return 影响行数
*/
int updateUserPasswd(Map<String, Object> map);
1
2
3
4
<!--  parameterType 为 map 时,属性名无需和数据库表的字段一一对应  -->
<insert id="updateUserPasswd" parameterType="map">
INSERT INTO user (id, passwd) VALUES (#{userId}, #{password});
</insert>
1
2
3
4
5
6
7
8
9
try(SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

Map<String, Object> map = new HashMap<>();
map.put("userId", 1);
map.put("password", "666666");

userMapper.updateUserPasswd(map);
}

配置解析

核心配置文件

官方建议的文件名:mybatis-config.xml

各标签的顺序必须如下

<configuration> (配置)

  • <properties> (属性)
  • <settings> (设置)
  • <typeAliases> (类型别名)
  • <typeHandlers> (类型处理器)
  • <objectFactory> (对象工厂)
  • <plugins> (插件)
    • mybatis-generator-core
    • mybatis-plus
    • 通用mapper
  • <environments> (环境配置)
    • <environment> (环境变量)
      • <transactionManager> (事务管理器)
      • <dataSource> (数据源)
  • <mappers> (映射器)

环境配置 environments

  • <transactionManager>

    MyBatis中有两种类型的事务管理器(type=”JDBC|MANAGED”)

    如果使用Spring + MyBatis,则没有必要配置事务管理器

  • <dataSource>

    • UNPOOLED
    • POOLED (默认)
    • JNDI

属性 properties

可以通过properties属性来实现引用配置文件

这些属性都是可外部配置且动态替换的,既可以在典型的Java属性文件中(如db.properties),也可通过properties元素的子元素(property)来传递。

编写一个配置文件db.properties

1
2
3
4
5
6
<!-- 引入外部配置文件,
如果外部配置文件有和子元素property相同的字段,外部配置文件更优先 -->
<properties resource="db.properties">
<property name="username" value="root"/>
<property name="password" value="123456"/>
</properties>

类型别名 typeAliases

  • 类型别名是为Java类型设置一个短的名字,用于减少类完全限定名的冗余

    1
    2
    3
    4
    <!-- 给实体类起别名 -->
    <typeAliases>
    <typeAlias type="com.hunter.pojo.User" alias="User"/>
    </typeAliases>
  • 也可以指定一个包名,MyBatis会在包名下搜索需要的Java Bean,比如扫描实体类的包,它的别名就是首字母小写的类名

    1
    2
    3
    4
    <!-- 指定包名 -->
    <typeAliases>
    <package name="com.hunter.pojo"/>
    </typeAliases>

    这种方式无法自定义别名,如果要自定义,需要在实体类上增加注解:

    1
    2
    3
    4
    import org.apache.ibatis.type.Alias;

    @Alias("Hello")
    public class User {...}

设置 settings

  • 可以指定MyBatis所用日志的具体实现,未指定时,将自动查找。

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

映射器 mappers

  • 方式一:注册mapper.xml文件【推荐使用】

    1
    2
    3
    4
    <!-- 每个mapper.xml都需要在该配置文件中注册-->
    <mappers>
    <mapper resource="UserMapper.xml"/>
    </mappers>
  • 方式二:使用class文件绑定注册

    1
    2
    3
    <mappers>
    <mapper class="com.hunter.dao.UserMapper"/>
    </mappers>
    • 接口和Mapper配置文件必须同名
    • 接口和Mapper配置文件必须在同一个包下
  • 方式三:使用扫描包进行注册绑定

    1
    2
    3
    <mappers>
    <package name="com.hunter.dao"/>
    </mappers>
    • 接口和Mapper配置文件必须同名
    • 接口和Mapper配置文件必须在同一个包下

生命周期和作用域

生命周期和作用域至关重要,错误的使用会导致严重的并发问题

  • SqlSessionFactoryBuilder

    一旦创建了SqlSessionFactory,就不再需要它,因此作为局部变量即可。

  • SqlSessionFactory

    可以理解为数据库连接池,一旦被创建,就应该在应用运行期间一直存在,没有任何理由丢弃或重新创建另一个实例,多次重建SqlSessionFactory被视为一种代码坏习惯。因此最简单的就是使用单例模式或者静态单例模式

  • SqlSession

    连接到连接池的一个请求。用完之后需要马上关闭,避免资源的占用。


结果集映射 resultMap

可以解决对象的属性名和数据库中字段名不一致的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mapper namespace="com.hunter.dao.UserMapper">
<!-- 结果集映射 -->
<resultMap id="UserMap" type="User">
<!-- column 数据库中的字段,property 实体类中的属性 -->
<result column="id" property="id"/>
<result column="name" property="name"/>
<result column="passwd" property="pwd"/>
</resultMap>

<!-- 返回值类型是List时,resultType填写List中存放的对象类型 -->
<select id="getUserList" resultMap="UserMap">
SELECT * FROM user;
</select>
</mapper>

resultMap是Mybatis中最强大的元素。其设计思想是,简单的语句不需要配置显式的结果映射,复杂的语句只需要描述它们的关系。但是现实不仅仅只有这么简单的关系


日志工厂

初学者定位异常的手段,通常是控制台输出debug,而日志定位是现实项目的成熟方式。

  • STDOUT_LOGGING 标准日志输出

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

LOG4J

Log4j是Apache的开源项目,可以控制日志信息输送的目的地是控制台、文件、GUI组件。

  • 可以控制每条日志的输出格式。
  • 通过定义每条日志信息的级别,能够更加细致地控制日志的生成过程。
  • 通过配置文件来灵活地配置,不需要修改应用的代码。
  1. 导包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.20.0</version>
    </dependency>
  2. 创建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
    25
    26
    27
    28
    29
    # 将等级为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/mybatis_learning.log
    # 每个log文件的大小,超过之后会生成一个新的日志,KB、MB、GB
    log4j.appender.file.MaxFileSize=1MB
    log4j.appender.file.Threshold=DEBUG
    # 输出的文件格式
    log4j.appender.file.layout=org.apache.log4j.PatternLayout
    log4j.appender.file.layout.ConversionPattern=【%p】【%d{yyyy-MM-dd hh:mm:ss}】【%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
  3. 配置log4j为日志的实现

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

简单使用

  1. 在要使用log4j的类中,导入包org.apache.log4j.Logger

  2. 日志对象,参数为当前类的class

    1
    public static Logger logger = Logger.getLogger(UserMapperTest.class);
  3. 日志级别

    1
    2
    3
    logger.info("进入了test log4j");
    logger.debug("进入了test log4j");
    logger.error("进入了test log4j");

分页

分页能减少数据的处理量。

方式一 使用MySQL中的LIMIT语句

1
2
3
SELECT prod_name
FROM products
LIMIT 5 OFFSET 4; -- 从第4行开始,取5行;等价于 LIMIT 4, 5

MySQL检索出来的第一行是行0而不是行1。

  1. 接口

    1
    2
    3
    4
    5
    6
    /**
    * 分页查询用户
    *
    * @return List<User>
    */
    List<User> getUserListByLimit(Map<String, Integer> map);
  2. Mapper.xml

    1
    2
    3
    4
    5
    6
    <!--  分页  -->
    <select id="getUserListByLimit" parameterType="map" resultType="user">
    SELECT *
    FROM user
    LIMIT #{startIndex}, #{pageSize};
    </select>
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Test
    public void should_success_when_get_user_list_by_limit() {
    // 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
    try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
    // 执行sql
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    Map<String, Integer> map = new HashMap<>();
    map.put("startIndex", 1);
    map.put("pageSize", 2);
    List<User> userList = userMapper.getUserListByLimit(map);
    for (User user : userList) {
    System.out.println(user);
    }
    }
    }

方式二 RowBounds

  1. 接口

    1
    2
    3
    4
    5
    6
    /**
    * 分页查询用户
    *
    * @return List<User>
    */
    List<User> getUserListByRowBounds();
  2. Mapper.xml

    1
    2
    3
    4
    5
    <!--  分页  -->
    <select id="getUserListByRowBounds" resultType="user">
    SELECT *
    FROM user;
    </select>
  3. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Test
    public void should_success_when_get_user_list_by_row_bounds() {
    // 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
    try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
    RowBounds rowBounds = new RowBounds(1, 2);

    List<User> userList = sqlSession.selectList("com.hunter.dao.UserMapper.getUserListByRowBounds", null, rowBounds);
    for (User user : userList) {
    System.out.println(user);
    }
    }
    }

方式三 分页插件 pagehelper

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>

使用注解开发

在真正的开发中,很多时候我们会选择面向接口编程,因为面向接口是一种解耦的方式,易扩展、能提高复用率。

分层开发中,上层不用关注具体的实现,大家都遵守共同的标准,使得开发变得容易,规范性更好。

  1. 注解在接口上的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Select("SELECT * FROM user")
    List<User> getUserList();

    @Insert("INSERT INTO user(id, name, passwd) VALUES (#{id}, #{name}, #{passwd})")
    int addUser(User user);

    @Update("UPDATE user SET name=#{name}, passwd=#{passwd} WHERE id=#{id}")
    int updateUser(User user);

    @Delete("DELETE FROM user WHERE id=#{uid}")
    int deleteUser(@Param("uid") int id);
  2. 需要在核心配置文件中绑定接口

    1
    2
    3
    <mappers>
    <mapper class="com.hunter.dao.UserMapper"/>
    </mappers>

关于@Param()注解

  • 基本类型的参数或者String类型,需要加上
  • 引用类型不需要加
  • 如果只有一个基本类型,可以忽略,但是建议还是加上
  • 在SQL中引用的,就是在@Param()中设定的属性名

Lombok

Lombok项目其实就是一个java库,它可以自动插入到编辑器和构建工具中,增强java的性能。以后你只需要一些简单的注解,就可以再也不用在类中反复去写getter、equals等方法,让代码变得简介,不用过多地去关注相应的方法。属性修改时,也简化了维护这些属性所生成的get、set方法。

使用方式:

  1. IDEA下载Lombok插件

  2. 项目中导入Lombok的jar包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.26</version>
    </dependency>
  3. 实体类上使用Lombok注解

Lombok注解

  • @Data

    自动创建无参构造函数、get、set、toString、hashcode、equals方法

  • @AllArgsConstructor

    自动创建所有参数的构造函数

  • @NoArgsConstructor

    自动创建无参构造函数

  • @EqualsAndHashCode

  • @ToString

  • @Getter/@Setter

  • @Builder

    可以类似于链式地调用对象进行赋值


复杂查询

搭建数据库表

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

INSERT INTO teacher(`id`, `name`) VALUES (1, 'H老师');

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;

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

测试环境搭建

  1. 导入lombok
  2. 新建实体类Teacher、Student
  3. 建立Mapper接口
  4. 建立Mapper.xml
  5. 在核心配置文件中绑定注册Mapper.xml文件(方式很多)
  6. 测试查询

多对一处理

举例:多个学生归属一个老师(如班主任概念)


方式一 嵌套查询处理

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
<!-- namespace 会绑定一个对应的DAO/Mapper接口 -->
<mapper namespace="com.hunter.dao.StudentMapper">
<!-- 结果集映射 -->
<resultMap id="StudentWithTeacherName" type="student">
<!-- column 数据库中的字段,property 实体类中的属性 -->
<result column="id" property="id"/>
<result column="name" property="name"/>
<!-- 复杂的属性需要单独处理:
对象:association
集合:collection
-->
<!--
column:在查询的结果集中,用该列值作为查询条件执行下一条SQL语句
select: 下一条执行的语句
javaType:将执行的结果集封装成某个Java类型
-->
<association property="teacher" column="tid" select="getTeacher" javaType="teacher"/>
</resultMap>

<!-- 使用结果集映射resultMap 映射数据库的字段和实体类的属性 -->
<select id="getStudentList" resultMap="StudentWithTeacherName">
SELECT *
FROM student;
</select>

<select id="getTeacher" resultType="teacher">
SELECT *
FROM teacher
WHERE id = #{tid};
</select>
</mapper>

方式二 联表查询处理

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
<!-- namespace 会绑定一个对应的DAO/Mapper接口 -->
<mapper namespace="com.hunter.dao.StudentMapper">
<!-- 结果集映射 -->
<resultMap id="StudentWithTeacherName" type="student">
<!-- column 数据库中的字段,property 实体类中的属性 -->
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<!-- 复杂的属性需要单独处理:
对象:association
集合:collection
-->
<!-- javaType:将执行的结果集封装成某个Java类型 -->
<association property="teacher" javaType="teacher">
<result property="id" column="tid"/>
<result property="name" column="tname"/>
</association>
</resultMap>

<!-- 使用结果集映射resultMap 映射数据库的字段和实体类的属性 -->
<select id="getStudentList" resultMap="StudentWithTeacherName">
SELECT s.id AS sid, s.name AS sname, t.id AS tid, t.name AS tname
FROM student AS s
INNER JOIN teacher AS t
ON s.tid = t.id;
</select>
</mapper>

一对多处理

举例:一个老师拥有多个学生

联表查询

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
<mapper namespace="com.hunter.dao.TeacherMapper">
<select id="getTeacher" resultMap="teacherWithStudents">
SELECT s.id AS sid, s.name AS sname, t.id AS tid, t.name AS tname
FROM student AS s
INNER JOIN teacher AS t
ON s.tid = t.id AND t.id = #{tid};
</select>

<!-- 结果集映射 -->
<resultMap id="teacherWithStudents" type="teacher">
<!-- column 数据库中的字段,property 实体类中的属性 -->
<result property="id" column="tid"/>
<result property="name" column="tname"/>
<!-- 复杂的属性需要单独处理:
对象:association
集合:collection
-->
<!-- 集合中的泛型信息,使用ofType获取 -->
<collection property="studentList" ofType="student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="tid" column="tid"/>
</collection>
</resultMap>
</mapper>

动态SQL

动态SQL指根据不同的条件,生成不同的SQL语句。其实动态SQL就是在拼接SQL语句,只要保证SQL的正确性,按照SQL的格式去排列组合就可以了。

搭建数据库表

1
2
3
4
5
6
7
CREATE TABLE `blog`(
`id` VARCHAR(50) NOT NULL COMMENT '博客id',
`title` VARCHAR(100) NOT NULL COMMENT '博客标题',
`author` VARCHAR(30) NOT NULL COMMENT '博客作者',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`views` INT(30) NOT NULL COMMENT '浏览量'
)ENGINE=INNODB;

测试环境搭建

  1. 导入lombok

  2. 新建实体类Blog

  3. 建立Mapper接口

  4. 建立Mapper.xml

  5. 在核心配置文件中绑定注册class文件BlogMapper(方式很多)

  6. 开启自动提交事务

    1
    2
    3
    4
    public static SqlSession getSqlSession() {
    // autoCommit:自动开启事务
    return sqlSessionFactory.openSession(true);
    }
  7. 初始化数据库表数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class BlogMapperTest {
    @Test
    public void initBlog() {
    try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);

    mapper.addBlog(generateNewBlog(IdUtils.generateId(), "Mybatis学习", "Hunter", new Date(), 0));
    mapper.addBlog(generateNewBlog(IdUtils.generateId(), "Java学习", "Hunter", new Date(), 0));
    mapper.addBlog(generateNewBlog(IdUtils.generateId(), "Spring学习", "Hunter", new Date(), 0));
    mapper.addBlog(generateNewBlog(IdUtils.generateId(), "微服务学习", "Hunter", new Date(), 0));
    }
    }

    private Blog generateNewBlog(String id, String title, String author, Date date, int views) {
    Blog blog = new Blog();
    blog.setId(id);
    blog.setTitle(title);
    blog.setAuthor(author);
    blog.setCreateTime(date);
    blog.setViews(views);
    return blog;
    }
    }

if语句

1
2
3
4
5
6
7
8
9
10
11
12
13
<select id="queryBlogIf" parameterType="map" resultType="blog">
SELECT *
FROM blog
<!-- where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。 -->
<where>
<if test="title != null">
AND title = #{title}
</if>
<if test="author != null">
AND author = #{author}
</if>
</where>
</select>

choose, when, otherwise语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<select id="queryBlogChoose" parameterType="map" resultType="blog">
SELECT *
FROM blog
<!-- where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。 -->
<where>
<choose>
<when test="title != null">
AND title = #{title}
</when>
<when test="author != null">
AND author = #{author}
</when>
<otherwise>
AND views = #{views}
</otherwise>
</choose>
</where>
</select>

set语句

1
2
3
4
5
6
7
8
9
10
11
12
13
<update id="updateBlog" parameterType="map">
UPDATE blog
<!-- set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。-->
<set>
<if test="title != null">
title = #{title},
</if>
<if test="author != null">
author = #{author},
</if>
</set>
WHERE id = #{id}
</update>

sql片段

使用sql标签抽取出公共的部分,在使用的地方使用include标签引用即可。

  • 最好基于单表来定义sql片段
  • 不要存在where标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<sql id="if-title-author">
<if test="title != null">
AND title = #{title}
</if>
<if test="author != null">
AND author = #{author}
</if>
</sql>

<select id="queryBlogIf" parameterType="map" resultType="blog">
SELECT *
FROM blog
<!-- where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。 -->
<where>
<include refid="if-title-author"></include>
</where>
</select>

foreach

1
2
3
4
5
6
7
8
9
10
<!-- SELECT * FROM blog WHERE (id = 1 OR id = 2 OR id = 3) -->
<select id="queryBlogForEach" parameterType="map" resultType="blog">
SELECT *
FROM blog
<where>
<foreach collection="ids" item="id" open="(" separator="OR" close=")">
id = #{id}
</foreach>
</where>
</select>

缓存

缓存是存在内存中的临时数据,将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从而提高查询效率,一定程度上解决了高并发系统的性能问题

经常查询并且不经常改变的数据适合缓存。

MyBatis中定义了两级缓存:一级缓存二级缓存默认情况下,只开启了一级缓存SqlSession级别的缓存,也称为本地缓存)。二级缓存是基于namespace级别的缓存。为了提高扩展性,MyBatis定义了缓存接口Cache,可以通过实现Cache接口来自定义二级缓存


一级缓存

一级缓存,也叫本地缓存,是SqlSession级别的缓存,它就是一个map默认开启

  • 与数据库同一次会话期间查询到的数据会放在本地缓存中
  • 之后如果需要获取相同的数据,直接从缓存中拿,没必要再次访问数据库。

缓存存在的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test_cache() {
// 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
// 执行sql
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user);
System.out.println("=============================================================");
user = userMapper.getUserById(1);
System.out.println(user);
}
}

从日志可以看出,只有一次访问数据库的记录。image-20230524222922360


缓存失效的情况:

  1. 对数据库进行增删改操作,由于可能会改变原来的数据,缓存会刷新。

  2. 手动清理

    1
    2
    // 手动清理缓存
    sqlSession.clearCache();

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test_cache() {
// 获得sqlSession对象,try语句在结束时会自动关闭资源,这称为try-with-resources语句
try (SqlSession sqlSession = MyBatisUtils.getSqlSession()) {
// 执行sql
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(1);
System.out.println(user);
System.out.println("=============================================================");
// 增加操作
user = new User(4, "王五", "123456");
userMapper.addUser(user);

System.out.println("=============================================================");
// 重新访问数据库(确保获取数据变动后的内容),然后刷新缓存
user = userMapper.getUserById(1);
System.out.println(user);
}
}

image-20230524222548553


二级缓存

二级缓存,也叫全局缓存,是namespace级别的缓存,一个命名空间,对应一个二级缓存

  • 工作机制:
    • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中
    • 如果当前会话关闭,一级缓存中的数据被保存到二级缓存中
    • 新的会话查询信息,就从二级缓存中获取内容
    • 不同的mapper查出的数据会放在自己对应的缓存(map)中

使用步骤

  1. 在mybatis的核心配置文件中,开启全局缓存

    1
    2
    <!-- 显式地开启全局缓存(默认已开启) -->
    <setting name="cacheEnabled" value="true"/>
  2. 要使用二级缓存的Mapper中开启缓存

    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 使用二级缓存 -->
    <cache/>

    <!-- 也可自定义参数 -->
    <cache eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>

    如果readOnly参数没有设置,需要将实体类序列化(implements Serializable


获取数据顺序

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

自定义缓存 Ehcache

  1. 导包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.2.3</version>
    </dependency>
  2. 在mapper中指定自定义的缓存实现

    1
    2
    <!-- 使用EhcacheCache -->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
  3. 搭配相应的配置文件ehcache.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
    <?xml version="1.0" encoding="UTF-8" ?>
    <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
    updateCheck="false">

    <diskStore path="./tmpdir/Tmp_EhCache"/>

    <defaultCache
    eternal="false"
    maxElementsInMemory="10000"
    overflowToDisk="false"
    diskPersistent="false"
    timeToIdleSeconds="1800"
    timeToLiveSeconds="259200"
    memoryStoreEvictionPolicy="LRU"/>

    <cache
    name="cloud_user"
    eternal="false"
    maxElementsInMemory="5000"
    overflowToDisk="false"
    diskPersistent="false"
    timeToIdleSeconds="1800"
    timeToLiveSeconds="1800"
    memoryStoreEvictionPolicy="LRU"/>
    </ehcache>

但是目前都用Redis来做缓存,本章了解一下。


MyBatis-Plus

1
2
3
4
5
6
7
8
9
10
11
12
<!-- mybatis plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!-- 代码生成器,逆向工程 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3.2</version>
</dependency>

代码生成器

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
public class CodeGenerator {
private static final String URL = "jdbc:mysql://127.0.0.1:3306/seckill" +
"?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
private static final String AUTHOR = "Hunter";

public static void main(String[] args) {
FastAutoGenerator.create(URL, USERNAME, PASSWORD)
// 全局配置
.globalConfig(builder -> {
builder.author(AUTHOR) // 设置作者
.enableSpringdoc() // 开启 Springdoc 模式
.outputDir(System.getProperty("user.dir") + "/src/main/java"); // 指定输出目录,System.getProperty("user.dir")能获取到项目所在的绝对路径
})
// 配置数据源
.dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
int typeCode = metaInfo.getJdbcType().TYPE_CODE;
if (typeCode == Types.SMALLINT) {
// 自定义类型转换
return DbColumnType.INTEGER;
}
return typeRegistry.getColumnType(metaInfo);

}))
// 包配置
.packageConfig(builder -> {
builder.parent("com.hunter") // 设置父包名
.moduleName("seckill") // 设置模块名
.entity("pojo")
.mapper("mapper")
.service("service")
.serviceImpl("service.impl")
.controller("controller")
.pathInfo(Collections.singletonMap(OutputFile.xml,
System.getProperty("user.dir") + "/src/main/resources/mapper")); // 设置mapper.xml生成路径;
})
// 配置模板,可以从 mybatis-plus-generator-3.5.3.2.jar!\templates 下复制,进行自定义
.templateConfig(builder -> {
builder.entity("templates/entity2.java")
.mapper("templates/mapper2.java")
.service("templates/service2.java")
.serviceImpl("templates/serviceImpl2.java")
.controller("templates/controller2.java");
})
// 策略配置
.strategyConfig(builder -> {
builder.addInclude("t_user") // 设置需要生成的表名
.addTablePrefix("t_", "c_") // 设置过滤表前缀
.entityBuilder().enableFileOverride() // 覆盖已生成文件
.mapperBuilder().enableFileOverride()
.serviceBuilder().enableFileOverride()
.controllerBuilder().enableFileOverride();
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}

  • com.baomidou.mybatisplus.extension.service.IService#save

  • com.baomidou.mybatisplus.extension.service.IService#getOne(com.baomidou.mybatisplus.core.conditions.Wrapper)

mybatis plus做了封装,插入数据自动回传主键到对象。orderMapper.insert(order);