关于Mybatis框架

Mybatis框架解决了数据库编程相关的问题,主要是简化了数据库编程。

当使用Mybatis框架实现数据库编程时,只需要:

  • 定义数据操作功能的抽象方法(此抽象方法必须在接口中)
  • 配置以上抽象方法映射的SQL语句

Mybatis框架在实现过程中,会自动生成各接口的代理对象,所以,开发人员并不需要关注接口的实现问题。

使用Mybatis框架

在Spring Boot项目中,当需要使用Mybatis框架实现数据库编程时,需要添加:

  • mybatis-spring-boot-starter
  • 数据库的依赖,例如mysql-connector-java

所以,在pom.xml中添加:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Mybatis框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- MySQL依赖项,仅运行时需要 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

由于添加以上依赖后,Spring
Boot在启动时就会读取连接数据库的配置信息,如果未配置,则启动报错且失败,需要在src/main/resources
下的application.properties中添加必要的配置:

1
spring.datasource.url=jdbc:mysql://localhast:8888

提示:以上配置中,属性名称是固定,以上示例值是错误值,但是,启动Spring Boot只是加载以上配置,并不会执行连接,所以,配置值错误并不影响启动项目。

创建数据库:

1
create database mall_pms;

连接数据库的配置

在Spring Boot项目中,src/main/resources下的application.properties是默认的配置文件,项目在启动时,Spring
Boot会自动从此文件中读取相关的配置信息。

在许多配置过程中,需要在application.properties中的配置的属性的名称是固定的!

在配置数据库的连接信息时,至少需要配置spring.datasource.urlspring.datasource.usernamespring.datasource.password
这3个属性,分别表示连接数据库的URL、登录数据库的用户名、登录数据库的密码

1
2
3
4
5
spring.datasource.url=jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai

spring.datasource.username=root

spring.datasource.password=root

为了检验配置值是否正确,可以在src/test/java下默认的包下创建DatabaseConnectionTests测试类,在测试类中编写测试方法,以尝试连接数据库,即可检验:

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 cn.tedu.csmall.server;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import javax.sql.DataSource;

// 测试类必须:
// 1. 在组件扫描的包下(默认的包及其子孙包)
// 2. 只有添加了@SpringBootTest注解才会加载整个项目的运行环境(包括Spring、读取配置等)
@SpringBootTest
public class DatabaseConnectionTests {

// Spring Boot自动读取application.properties的配置,并创建了数据源对象
@Autowired
DataSource dataSource;

// 连接数据库,可用于检查application.properties中的连接参数值是否正确
@Test
public void testConnection() throws Exception {
// 获取与MySQL的连接,此方法的调用会实质的连接数据库
dataSource.getConnection();
System.out.println("当你看到这句话时,表示在application.properties中的数据库连接参数的值是正确的");
}

}

示例:

1
2
3
4
5
6
7
8
9
create table user(
id bigint unsigned auto_increment comment '数据id',
age tinyint unsigned comment '年龄',
category int unsigned comment '分类',
username varchar(32) comment '用户名',
password char(32) comment '密码',
content text(32) comment '正文',
primary key(id)
)charset utf8mb4;

java: byte short int long String
mysql: tinyint smallint int bigint varchar/char/text

关于设计数据表

关于id

阿里巴巴的建议是:每张表都应该有id字段,且是bigint unsigned类型,其中,bigint对应Java中的long类型,unsigned
表示“无符号位”,
将使得此字段值不会出现负数,且取值区间是原正数的2倍……以tinyint为例,没有添加unsigned时,取值区间是[-128, 127]
,添加unsigned以后,
取值区间是[0, 255]。

当把id的类型设置为bigint时,理论上id值足够使用,即使不添加unsigned也不会出现id值不够用的情况,但仍推荐添加,其目的是为了表现语义。

关于编码

应该在创建表时指定编码,创建库时可以不指定编码。

在MySQL / MariaDB强烈推荐使用utf8mb4

关于字符串的字段类型

如果某个字符串类型的字段的值的长度变化可能较大,应该使用varchar类型,例如用户名,如果某个字符串类型的字段的值的长度**
变化不大**,
应该使用char类型。

注意:某些数据可能是纯数字的,但并不具备算术运算含义,也应该使用字符串类型,不要使用数值类型。

在使用varchar时,指定的长度一定是“大于必要长度”的标准,例如,现行的标准是“用户名最多15个字符”,则建议设计为varchar(25)
或比25更大的值,
但是,也不要过于夸张,避免影响语义。

使用Mybatis时定义的抽象方法

使用Mybatis时,定义的抽象方法都必须在接口中,通常,接口会使用Mapper作为名称的最后一个单词,例如命令为BrandMapper等。

关于抽象方法的声明原则:

  • 返回值类型:如果需要执行的SQL是增、删、改类型的,统一使用int作为返回值类型,表示“受影响的行数”,其实也可以使用void
    ,但并不推荐,
    如果需要执行的SQL是查询类型的,如果查询最多只返回1个结果,则只需要保证返回值类型可以装得下所需的查询结果即可,
    如果查询返回的结果可能超过1条,则必须使用List集合进行封装,且集合的元素类型依然只需要保证可以装得下所需的查询结果即可
  • 方法名称:自定义
    • 获取单个对象的方法用 get 做前缀
    • 获取多个对象的方法用 list 做前缀
    • 获取统计值的方法用 count 做前缀
    • 插入的方法用 save/insert 做前缀
    • 删除的方法用 remove/delete 做前缀
    • 修改的方法用 update 做前缀
  • 参数列表:如果需要执行的SQL语句中的参数数量较多,推荐将多个参数封装到自定义类中

关于@Mapper和@MapperScan

Mybatis框架只要求开发人员编写接口和抽象方法,不需要开发人员编写实现类,是因为Mybatis会通过代理模式自动生成接口的实现对象,
但是,它需要明确哪些接口是需要生成代理对象的。

可以在各接口上添加@Mapper注解,在启动项目时,Mybatis会对整个项目进行扫描,对于已经添加此注解的接口,就会生成代理对象。

也可以在配置类上添加@MapperScan注解,用于指定各接口所在的包,则Mybatis会扫描此包及其子孙包下的所有接口,并生成这些接口的代理对象。

关于@Mapper@MapperScan这2种注解,只需要选择其中1种使用即可,通常推荐@MapperScan

注意:使用@MapperScan时,一定只指向Mapper接口所在的包,并确保此包下无其它接口!

提示:Mybatis框架的@MapperScan,与Spring框架的@ComponentScan没有任何关系,且不会互相影响!

使用Mybatis实现插入数据

以实现“插入品牌数据”为例,需要执行的SQL语句大致是:

1
insert into pms_brand (name, pinyin, logo, description, keywords, sort, sales, product_count, comment_count, positive_comment_count, enable, gmt_create, gmt_modified) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);

先在项目的默认包下创建pojo.entity.Brand类,类中的属性应该与数据表对应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Brand implements Serializable {

private Long id;
private String name;
private String pinyin;
private String logo;
private String description;
private String keywords;
private Integer sort;
private Integer sales;
private Integer productCount;
private Integer commentCount;
private Integer positiveCommentCount;
private Integer enable;
private LocalDateTime gmtCreate;
private LocalDateTime gmtModified;

// 按照POJO规范补充后续代码

}

接下来,准备接口和抽象方法,在项目的默认包下创建mapper.BrandMapper接口,并在接口中添加抽象方法:

1
2
3
4
5
6
7
8
9
10
11
12
package cn.tedu.csmall.server.mapper;

public interface BrandMapper {

/**
* 插入品牌数据
* @param brand 品牌数据
* @return 受影响的行数,成功插入数据时,将返回1
*/
int insert(Brand brand);

}

关于SQL语句,可以使用@Insert等注解进行配置,但不推荐!

推荐使用XML文件配置SQL语句,此文件模版可以通过 http://doc.canglaoshi.org/config/Mapper.xml.zip 下载。

然后,在src/main/resources下创建mapper文件夹,将下载得到的zip文件解压,得到SomeMapper.xml
文件,将此XML文件复制到mapper文件夹中。

先将SomeMapper.xml重命名为BrandMapper.xml

关于此文件的配置:

  • 根节名必须是<mapper>
  • 根节点必须配置namespace属性,取值为对应的接口的全限定名
  • 在根节点内部,根据需要执行的SQL语句的类型不同,使用<insert><delete><update><select>节点
  • <insert>等节点上,必须配置id属性,取值为抽象方法的名称(不包含括号及参数)
  • <insert>等节点内部,配置SQL语句,SQL语句不需要使用分号结束

例如配置为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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="cn.tedu.csmall.server.mapper.BrandMapper">

<!-- int insert(Brand brand); -->
<insert id="insert">
insert into pms_brand (
name, pinyin, logo, description, keywords,
sort, sales, product_count, comment_count, positive_comment_count,
enable, gmt_create, gmt_modified
) values (
#{name}, #{pinyin}, #{logo}, #{description}, #{keywords},
#{sort}, #{sales}, #{productCount}, #{commentCount}, #{positiveCommentCount},
#{enable}, #{gmtCreate}, #{gmtModified}
)
</insert>

</mapper>

最后,还需要补充一个配置,用于告诉Mybatis框架这类XML文件的位置!在application.properties中添加:

1
mybatis.mapper-locations=classpath:mapper/*.xml

另外,在插入数据时,还可以配置,得到自动编号的ID值,具体做法是在<insert>节点上添加配置:

1
2
3
4
<!-- int insert(Brand brand); -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
暂不关心此处的SQL语句
</insert>

练习

目标:实现向pms_album表中插入数据,并要求可以获取自动编号的id。

开发步骤:

  • entity包创建Album实体类,类中的属性与pms_album表保持一致,且符合POJO规范
  • mapper包中创建AlbumMapper接口
  • AlbumMapper接口中添加抽象方法:int insert(Album album);(记得添加注释)
  • src/main/resourcesmapper文件夹下,通过复制粘贴得到AlbumMapper.xml文件
  • 配置AlbumMapper.xml中的根节点的namespace
  • AlbumMapper.xml中,在根节点内部添加<insert>节点,并配置iduseGeneratedKeyskeyProperty属性,在<insert>
    节点内部配置SQL语句
  • src/test/java下的cn.tedu.csmall.server.mapper包(你的包名可能不同)下创建AlbumMapperTests
    测试类,在类中自动装配AlbumMapper对象,并编写、执行测试方法

使用Mybatis实现删除数据

目标:根据id删除某个品牌

需要执行的SQL语句大致是:

1
delete from pms_brand where id=?

BrandMapper接口中添加抽象方法:

1
2
3
4
5
6
/**
* 根据品牌id,删除品牌数据
* @param id 期望删除的品牌数据的id
* @return 受影响的行数,当删除成功时,将返回1,如果无此id对应的数据,将返回0
*/
int deleteById(Long id);

BrandMapper.xml中配置SQL:

1
2
3
4
<!-- int deleteById(Long id); -->
<delete id="deleteById">
delete from pms_brand where id=#{id}
</delete>

BrandMapperTests中编写并执行测试:

1
2
3
4
5
6
@Test
public void testDeleteById() {
Long id = 1L;
int rows = mapper.deleteById(id);
System.out.println("删除完成,受影响的行数=" + rows);
}

练习

目标:实现根据id删除pms_album表中的数据。

使用Mybatis实现修改数据

目标:实现根据id修改pms_brand表中某条数据的name字段值。

需要执行的SQL语句大致是:

1
update pms_brand set name=? where id=?

BrandMapper接口中添加抽象方法:

1
2
3
4
5
6
7
/**
* 根据id修改品牌的名称
* @param id 品牌id
* @param name 新的品牌名称
* @return 受影响的行数,当修改成功时,将返回1,如果无此id对应的数据,将返回0
*/
int updateNameById(@Param("id") Long id, @Param("name") String name);

BrandMapper.xml中配置SQL:

1
2
3
4
<!-- int updateNameById(@Param("id") Long id, @Param("name") String name); -->
<update id="updateNameById">
update pms_brand set name=#{name} where id=#{id}
</update>

BrandMapperTests中编写并执行测试:

1
2
3
4
5
6
7
@Test
public void testUpdateNameById() {
Long id = 3L;
String name = "白萝卜";
int rows = mapper.updateNameById(id, name);
System.out.println("修改品牌名称完成,受影响的行数=" + rows);
}

练习

目标:实现根据id修改pms_album表中某条数据的name字段值。

使用Mybatis实现批量删除数据

在Mybatis中,有“动态SQL”的机制,它允许根据调用方法时传入的参数值不同,来生成不同的SQL语句。

目标:根据若干个id一次性删除多个品牌。

需要执行的SQL语句大致是:

1
delete from pms_brand where id=? or id=? or id=?;

或者:

1
delete from pms_brand where id in (?, ?, ?);

注意:以上SQL语句中的id的数量是不确定的。

BrandMapper接口中,抽象方法可以是:

1
int deleteByIds(Long... ids); // 注意:可变参数在处理时,本质上就是数组

或者:

1
int deleteByIds(Long[] ids);

或者:

1
int deleteByIds(List<Long> ids);

BrandMapper.xml中配置SQL语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- int deleteByIds(Long... ids); -->
<delete id="deleteByIds">
delete from pms_brand where id in (
<foreach collection="array" item="id">
#{id},
</foreach>
0
)
</delete>

<!-- int deleteByIds(Long... ids); -->
<delete id="deleteByIds">
delete from pms_brand where id in (
<foreach collection="array" item="id" separator=",">
#{id}
</foreach>
)
</delete>

由于需要对参数ids(若干个id)进行遍历,需要使用到动态SQL中的<foreach>
节点,此节点可以对数组或集合进行遍历!关于<foreach>的配置:

  • collection属性:表示被遍历的参数对象,当抽象方法的参数只有1个,且没有添加@Param
    注解时,当参数值的类型是数组时,此属性值为array
    当参数值的类型是List时,此属性值为list;否则,此属性值为@Param注解中的参数值
  • item属性:表示被遍历到的元素的名称,是自定义的名称,在<foreach>内部,使用#{}格式的占位符时,也使用此属性来表示每个元素
  • separator属性:表示遍历过程中各元素值之间的分隔符号

最后,在BrandMapperTests中编写并执行测试:

1
2
3
4
5
@Test
public void testDeleteByIds() {
int rows = mapper.deleteByIds(1L, 3L, 5L, 7L, 9L);
System.out.println("批量删除完成,受影响的行数=" + rows);
}

使用Mybatis实现动态SQL的修改数据

在动态SQL机制中,可以使用<if>标签,可用于对某参数值进行判断,从而生成不同的SQL语句片段,常用于设计更新数据的操作。

目标:使用1个方法,实现多种不同的数据更新(想更新哪些字段就更新哪些字段,不想更新的字段值将保持不变)

需要执行的SQL语句大致是:

1
update pms_brand set name=?, pinyin=?, logo=? where id=?

注意:以上SQL语句的修改的字段列表应该不是固定的,应该根据传入的参数值来决定。

先在BrandMapper接口中添加抽象方法:

1
int updateById(Brand brand);

然后,在BrandMapper.xml中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- int updateById(Brand brand); -->
<update id="updateById">
UPDATE
pms_brand
<set>
<if test="name != null">
name=#{name},
</if>
<if test="pinyin != null">
pinyin=#{pinyin},
</if>
<if test="logo != null">
logo=#{logo},
</if>
</set>
WHERE
id=#{id}
</update>

需要注意的是,在Mybatis的动态SQL中,<if>并没有对应的<else>,如果一定要实现类似Java中的if...else
效果,需要使用<choose>标签,其基本格式是:

1
2
3
4
5
6
7
8
<choose>
<when test="条件">
满足条件时的SQL片段
</when>
<otherwise>
不满足条件时的SQL片段
</otherwise>
</choose>

或者,也可以使用2个条件完全相反的<if>标签来实现类似效果(但是执行效率偏低),例如:

1
2
3
4
5
6
<if test="pinyin != null">
某代码片段
</if>
<if test="pinyin == null">
某代码片段
</if>

使用Mybatis实现查询数据

统计查询

目标:统计品牌表中的数据的数量

需要执行的SQL语句大致是:

1
select count(*) from pms_brand;

BrandMapper接口中添加抽象方法:

1
int count();

BrandMapper.xml中配置SQL:

1
2
3
4
<!-- int count(); -->
<select id="count" resultType="int">
SELECT count(*) FROM pms_brand
</select>

注意:所有查询节点(<select>)必须配置resultTyperesultMap这2个属性中的其中1个。

当使用resultType声明封装结果的数据类型时,取值与抽象方法的返回值对应,如果是基本类型,直接写类型名称即可,
例如resultType="int",如果是引用数据类型,在java.lang包下的可以直接写类名,其它包下的写全限定名。

指定条件的单一结果查询

目标:根据id查询品牌详情

需要执行的SQL语句大致是:

1
select id, name, pinyin ... from pms_brand where id=?

由于不推荐使用星号表示字段列表,并且,在实际查询时,可能有部分字段是不需要体现在查询结果中的,推荐的做法是针对所需的查询字段,另外创建类型进行结果的封装。

例如,在pojo.vo下创建BrandDetailVO类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class BrandDetailVO implements Serializable {

/**
* 记录id
*/
private Long id;

/**
* 品牌名称
*/
private String name;

/**
* 品牌名称的拼音
*/
private String pinyin;

/**
* 品牌logo的URL
*/
private String logo;

/**
* 品牌简介
*/
private String description;

/**
* 关键词列表,各关键词使用英文的逗号分隔
*/
private String keywords;

/**
* 自定义排序序号
*/
private Integer sort;

/**
* 销量(冗余)
*/
private Integer sales;

/**
* 商品种类数量总和(冗余)
*/
private Integer productCount;

/**
* 买家评论数量总和(冗余)
*/
private Integer commentCount;

/**
* 买家好评数量总和(冗余)
*/
private Integer positiveCommentCount;

/**
* 是否启用,1=启用,0=未启用
*/
private Integer enable;

// 参考POJO规范添加其它代码

}

BrandMapper接口中添加抽象方法:

1
BrandDetailVO getById(Long id);

然后,在BrandMapper.xml中配置SQL:

1
2
3
4
5
6
7
8
9
10
<select id="getById" resultType="cn.tedu.csmall.server.pojo.vo.BrandDetailVO">
SELECT
id, name, pinyin, logo, description,
keywords, sort, sales, product_count, comment_count,
positive_comment_count, enable
FROM
pms_brand
WHERE
id=#{id}
</select>

另外,在查询时,一定要明确几个概念:

  • 字段(Field):在创建数据表时指定的名称(后续也能修改表结构时改名)
  • 列(Column):查询的结果集中的每一竖排,列名默认情况下是字段名,如果查询时指定了别名,则列名就是指定的别名
  • 属性( Property):类中的属性

Mybatis在执行查询时,会尝试自动的将结果集中的数据封装到返回结果类型的对象中,但是,它只能自动处理列名与属性名相同的部分,如果列名与属性名不同,默认并不能自动封装!

可以在查询的SQL语句中,自定义别名,使得列名与属性名相同,则可以实现自动封装,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="getById" resultType="cn.tedu.csmall.server.pojo.vo.BrandDetailVO">
SELECT
id, name, pinyin, logo, description,
keywords, sort, sales,

product_count AS productCount,

comment_count,
positive_comment_count, enable
FROM
pms_brand
WHERE
id=#{id}
</select>

以上的product_count AS productCount自定义别名可以保证product_count字段的值可以被自动封装!

更推荐使用<resultMap>来指导Mybatis如何封装结果,它将与<select>标签的resultMap属性一起使用,例如:

1
2
3
4
5
<select id="xx" resultMap="DetailResultMap">
</select>

<resultMap id="DetailResultMap" type="cn.tedu.csmall.server.pojo.vo.BrandDetailVO">
</resultMap>

然后,在<resultMap>内部,使用<result>来配置列名与属性名的对应关系,例如:

1
2
3
4
5
<resultMap id="DetailResultMap" type="cn.tedu.csmall.server.pojo.vo.BrandDetailVO">
<result column="product_count" property="productCount"/>
<result column="comment_count" property="commentCount"/>
<result column="positive_comment_count" property="positiveCommentCount"/>
</resultMap>

提示:在使用<resultMap>
配置时,从规范的角度出发,每个列与属性的关系都需要显式的配置出来(即使从功能实现的角度来看可能并不需要),另外,还应该使用<id>
节点对主键进行配置。

查询列表

目标:查询品牌列表

需要执行的SQL语句大致是(暂时使用星号表示字段列表):

1
select * from pms_brand order by id

通常,查询列表时,所需要查询的字段与查单个数据可能是不同的,所以,可能需要自定义新的VO类作为List
中的元素类型,为了避免写完代码后发现某个VO不能复用于查询单个数据和查询列表这2个功能,推荐一开始就定义用于封装列表项结果的VO类,例如:

1
2
3
public class BrandListItemVO implements Serializable {
// 可暂时与BrandDetailVO使用完全相同的属性
}

BrandMapper接口中添加抽象方法:

1
List<BrandListItemVO> list();

然后,在BrandMapper.xml中配置SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id="list" resultMap="ListItemResultMap">
SELECT
id, name, pinyin, logo, description,
keywords, sort, sales, product_count, comment_count,
positive_comment_count, enable
FROM
pms_brand
ORDER BY id
</select>

<resultMap id="ListItemResultMap" type="cn.tedu.csmall.server.pojo.vo.BrandListItemVO">
<id column="id" property="id" />
<result column="product_count" property="productCount" />
<result column="comment_count" property="commentCount" />
<result column="positive_comment_count" property="positiveCommentCount" />
</resultMap>

关于<sql><resultMap>

在使用XML配置SQL语句时,可以使用<sql>封装SQL语句片段,并使用<include>进行引用,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<select id="list" resultMap="ListItemResultMap">
SELECT
<include refid="ListItemQueryFields"/>
FROM
pms_brand
ORDER BY id
</select>

<sql id="ListItemQueryFields">
id, name, pinyin, logo, description,
keywords, sort, sales, product_count, comment_count,
positive_comment_count, enable
</sql>

通过,使用<sql>封装字段列表,而<resultMap>通常与之对应,所以,这2个节点的id命名通常会使用相同的关键词,
例如<sql>配置为id="ListItemQueryFields",并且<resultMap id="ListItemResultMap">,甚至,在编码时,会把这2个节点放在相邻的位置。

提示:使用<sql>封装字段列表时,IntelliJ IDEA可能会误判为错误的语法,将字段列表使用<if test="true">框住(或将此<if>
添加在之前)即可避免出现这样的错误提示。

练习

完成以下查询功能:

  • 统计相册的数量
  • 统计类别的数量
  • 根据id查询相册详情
    • 除了gmt_creategmt_modified字段的值
  • 根据id查询类别详情
    • 除了gmt_creategmt_modified字段的值
  • 查询相册的列表
    • 除了gmt_creategmt_modified字段的值
  • 根据父级类别,查询子级类别列表(条件:where parent_id=?
    • 除了gmt_creategmt_modified字段的值

课后作业

在当前项目(jsd2203-csmall-server)中,通过Mybatis实现:

  • 向插入pms_album表、pms_attribute表等共计12张表中插入数据(已完成的不必重复开发,但均需提交)
  • 根据id删除pms_album表、pms_attribute表等共计12张表中的数据(已完成的不必重复开发,但均需提交)
  • 根据若干个id删除pms_album表、pms_attribute表等共计12张表中的数据(已完成的不必重复开发,但均需提交)

在实现过程中,你可能需要创建必要的类、接口等文件,例如实体类、Mapper接口、XML文件、测试类,请按照课上标准命名,并放在正常的包或文件夹中。

以上所有功能必须有对应的测试。

Mybatis小结:
关于Mybatis使用
1.了解使用Mybatis时需要添加的依赖
mybatis-spring-boot-starter
mysql-connector-java
2.了解使用Mybatis时的一次性配置
application.properties: 数据库连接参数
使用@MapperScan配置接口文件所在的包
application.properties: 配置XML文件的位置
3.掌握抽象方法声明原则
返回值:增删改返回值为int,查询:单个结果-保证能封装结果即可、多条数据-使用List,元素类型保证能封装结果即可
方法名称:参考阿里手册
参数列表:先分析-打草稿(写sql),参数超过1个,声明多个参数-参数前添加@Param注解参数起别名;声明自定义POJO封装数据
4.掌握xml中配置个抽象方法的映射sql
xml文件固定声明 - 或用模板 或复制粘贴 或工具生成
必须在根节点的namespace中指定对应的接口
应根据sql语句来选择必须resultType或resultMap中其中1个属性
封装sql片段,当时用这段sql语句,使用进行引用
可以指导MyBatis封装查询结果,可以配置列与属性对应关系
动态,通常用于实现批量删除、批量插入等
动态通常结合处理更新数据
5.掌握规范的sql语句
在insert语句中,应该显式的指定的字段列表
正例:insert into user(username,password) values(‘Tom’,‘123123’);
反例:insert into user values(‘Tom’,‘123123’);
在统计查询时,count()进行统计
在查询表数据时,不要使用星号(
)表示字段列表
当查询结果超过1条时,建议使用order by进行排序。

自动更新gmt_create和gmt_modified的Mybatis拦截器

MybatisConfiguration.java

1
2
3
4
5
6
7
8
9
10
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;

@PostConstruct
public void addInterceptor() {
Interceptor interceptor = new InsertUpdateTimeInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}

InsertUpdateTimeInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
package cn.tedu.mall.product.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* <p>基于MyBatis的自动更新"最后修改时间"的拦截器</p>
*
* <p>需要SQL语法预编译之前进行拦截,则拦截类型为StatementHandler,拦截方法是prepare</p>
*
* <p>具体的拦截处理由内部的intercept()方法实现</p>
*
* <p>注意:由于仅适用于当前项目,并不具备范用性,所以:</p>
*
* <li>拦截所有的update方法(根据SQL语句以update前缀进行判定),无法不拦截某些update方法</li>
* <li>所有数据表中"最后修改时间"的字段名必须一致,由本拦截器的FIELD_MODIFIED属性进行设置</li>
*
* @see cn.tedu.mall.product.config.InsertUpdateTimeInterceptorConfiguration
*/
@Slf4j
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class InsertUpdateTimeInterceptor implements Interceptor {
/**
* 自动添加的创建时间字段
*/
private static final String FIELD_CREATE = "gmt_create";
/**
* 自动更新时间的字段
*/
private static final String FIELD_MODIFIED = "gmt_modified";
/**
* SQL语句类型:其它(暂无实际用途)
*/
private static final int SQL_TYPE_OTHER = 0;
/**
* SQL语句类型:INSERT
*/
private static final int SQL_TYPE_INSERT = 1;
/**
* SQL语句类型:UPDATE
*/
private static final int SQL_TYPE_UPDATE = 2;
/**
* 查找SQL类型的正则表达式:INSERT
*/
private static final String SQL_TYPE_PATTERN_INSERT = "^insert\\s";
/**
* 查找SQL类型的正则表达式:UPDATE
*/
private static final String SQL_TYPE_PATTERN_UPDATE = "^update\\s";
/**
* 查询SQL语句片段的正则表达式:gmt_modified片段
*/
private static final String SQL_STATEMENT_PATTERN_MODIFIED = ",\\s*" + FIELD_MODIFIED + "\\s*=";
/**
* 查询SQL语句片段的正则表达式:gmt_create片段
*/
private static final String SQL_STATEMENT_PATTERN_CREATE = ",\\s*" + FIELD_CREATE + "\\s*[,)]?";
/**
* 查询SQL语句片段的正则表达式:WHERE子句
*/
private static final String SQL_STATEMENT_PATTERN_WHERE = "\\s+where\\s+";
/**
* 查询SQL语句片段的正则表达式:VALUES子句
*/
private static final String SQL_STATEMENT_PATTERN_VALUES = "\\)\\s*values?\\s*\\(";

@Override
public Object intercept(Invocation invocation) throws Throwable {
// 检查方法的注解,如果方法包含 @IgnoreGmtTimeField 则不进行拦截
// if(invocation.getMethod().getDeclaredAnnotation(IgnoreGmtTimeField.class)!=null){
// return invocation.proceed();
// }

// 日志
log.debug("准备拦截SQL语句……");
// 获取boundSql,即:封装了即将执行的SQL语句及相关数据的对象
BoundSql boundSql = getBoundSql(invocation);
// 从boundSql中获取SQL语句
String sql = getSql(boundSql);
// 日志
log.debug("原SQL语句:{}", sql);
// 准备新SQL语句
String newSql = null;
// 判断原SQL类型
switch (getOriginalSqlType(sql)) {
case SQL_TYPE_INSERT:
// 日志
log.debug("原SQL语句是【INSERT】语句,准备补充更新时间……");
// 准备新SQL语句
newSql = appendCreateTimeField(sql, LocalDateTime.now());
break;
case SQL_TYPE_UPDATE:
// 日志
log.debug("原SQL语句是【UPDATE】语句,准备补充更新时间……");
// 准备新SQL语句
newSql = appendModifiedTimeField(sql, LocalDateTime.now());
break;
}
// 应用新SQL
if (newSql != null) {
// 日志
log.debug("新SQL语句:{}", newSql);
reflectAttributeValue(boundSql, "sql", newSql);
}

// 执行调用,即拦截器放行,执行后续部分
return invocation.proceed();
}

public String appendModifiedTimeField(String sqlStatement, LocalDateTime dateTime) {
Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_MODIFIED, Pattern.CASE_INSENSITIVE);
if (gmtPattern.matcher(sqlStatement).find()) {
log.debug("原SQL语句中已经包含gmt_modified,将不补充添加时间字段");
return null;
}
StringBuilder sql = new StringBuilder(sqlStatement);
Pattern whereClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_WHERE, Pattern.CASE_INSENSITIVE);
Matcher whereClauseMatcher = whereClausePattern.matcher(sql);
// 查找 where 子句的位置
if (whereClauseMatcher.find()) {
int start = whereClauseMatcher.start();
int end = whereClauseMatcher.end();
String clause = whereClauseMatcher.group();
log.debug("在原SQL语句 {} 到 {} 找到 {}", start, end, clause);
String newSetClause = ", " + FIELD_MODIFIED + "='" + dateTime + "'";
sql.insert(start, newSetClause);
log.debug("在原SQL语句 {} 插入 {}", start, newSetClause);
log.debug("生成SQL: {}", sql);
return sql.toString();
}
return null;
}

public String appendCreateTimeField(String sqlStatement, LocalDateTime dateTime) {
// 如果 SQL 中已经包含 gmt_create 就不在添加这两个字段了
Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_CREATE, Pattern.CASE_INSENSITIVE);
if (gmtPattern.matcher(sqlStatement).find()) {
log.debug("已经包含 gmt_create 不再添加 时间字段");
return null;
}
// INSERT into table (xx, xx, xx ) values (?,?,?)
// 查找 ) values ( 的位置
StringBuilder sql = new StringBuilder(sqlStatement);
Pattern valuesClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_VALUES, Pattern.CASE_INSENSITIVE);
Matcher valuesClauseMatcher = valuesClausePattern.matcher(sql);
// 查找 ") values " 的位置
if (valuesClauseMatcher.find()) {
int start = valuesClauseMatcher.start();
int end = valuesClauseMatcher.end();
String str = valuesClauseMatcher.group();
log.debug("找到value字符串:{} 的位置 {}, {}", str, start, end);
// 插入字段列表
String fieldNames = ", " + FIELD_CREATE + ", " + FIELD_MODIFIED;
sql.insert(start, fieldNames);
log.debug("插入字段列表{}", fieldNames);
// 定义查找参数值位置的 正则表达 “)”
Pattern paramPositionPattern = Pattern.compile("\\)");
Matcher paramPositionMatcher = paramPositionPattern.matcher(sql);
// 从 ) values ( 的后面位置 end 开始查找 结束括号的位置
String param = ", '" + dateTime + "', '" + dateTime + "'";
int position = end + fieldNames.length();
while (paramPositionMatcher.find(position)) {
start = paramPositionMatcher.start();
end = paramPositionMatcher.end();
str = paramPositionMatcher.group();
log.debug("找到参数值插入位置 {}, {}, {}", str, start, end);
sql.insert(start, param);
log.debug("在 {} 插入参数值 {}", start, param);
position = end + param.length();
}
if (position == end) {
log.warn("没有找到插入数据的位置!");
return null;
}
} else {
log.warn("没有找到 ) values (");
return null;
}
log.debug("生成SQL: {}", sql);
return sql.toString();
}


@Override
public Object plugin(Object target) {
// 本方法的代码是相对固定的
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}

@Override
public void setProperties(Properties properties) {
// 无须执行操作
}

/**
* <p>获取BoundSql对象,此部分代码相对固定</p>
*
* <p>注意:根据拦截类型不同,获取BoundSql的步骤并不相同,此处并未穷举所有方式!</p>
*
* @param invocation 调用对象
* @return 绑定SQL的对象
*/
private BoundSql getBoundSql(Invocation invocation) {
Object invocationTarget = invocation.getTarget();
if (invocationTarget instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) invocationTarget;
return statementHandler.getBoundSql();
} else {
throw new RuntimeException("获取StatementHandler失败!请检查拦截器配置!");
}
}

/**
* 从BoundSql对象中获取SQL语句
*
* @param boundSql BoundSql对象
* @return 将BoundSql对象中封装的SQL语句进行转换小写、去除多余空白后的SQL语句
*/
private String getSql(BoundSql boundSql) {
return boundSql.getSql().toLowerCase().replaceAll("\\s+", " ").trim();
}

/**
* <p>通过反射,设置某个对象的某个属性的值</p>
*
* @param object 需要设置值的对象
* @param attributeName 需要设置值的属性名称
* @param attributeValue 新的值
* @throws NoSuchFieldException 无此字段异常
* @throws IllegalAccessException 非法访问异常
*/
private void reflectAttributeValue(Object object, String attributeName, String attributeValue) throws NoSuchFieldException, IllegalAccessException {
Field field = object.getClass().getDeclaredField(attributeName);
field.setAccessible(true);
field.set(object, attributeValue);
}

/**
* 获取原SQL语句类型
*
* @param sql 原SQL语句
* @return SQL语句类型
*/
private int getOriginalSqlType(String sql) {
Pattern pattern;
pattern = Pattern.compile(SQL_TYPE_PATTERN_INSERT, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(sql).find()) {
return SQL_TYPE_INSERT;
}
pattern = Pattern.compile(SQL_TYPE_PATTERN_UPDATE, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(sql).find()) {
return SQL_TYPE_UPDATE;
}
return SQL_TYPE_OTHER;
}

}