Mybatis框架

关于Mybatis框架

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

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

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

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

使用Mybatis框架

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

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

所以,在pom.xml中添加:

<!-- 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中添加必要的配置:

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

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

创建数据库:

create database mall_pms;

连接数据库的配置

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

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

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

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测试类,在测试类中编写测试方法,以尝试连接数据库,即可检验:

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中的数据库连接参数的值是正确的");
}

}

示例:

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语句大致是:

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类,类中的属性应该与数据表对应:

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接口,并在接口中添加抽象方法:

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语句不需要使用分号结束

例如配置为:

<?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中添加:

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

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

<!-- 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语句大致是:

delete from pms_brand where id=?

BrandMapper接口中添加抽象方法:

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

BrandMapper.xml中配置SQL:

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

BrandMapperTests中编写并执行测试:

@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语句大致是:

update pms_brand set name=? where id=?

BrandMapper接口中添加抽象方法:

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

BrandMapper.xml中配置SQL:

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

BrandMapperTests中编写并执行测试:

@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语句大致是:

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

或者:

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

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

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

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

或者:

int deleteByIds(Long[] ids);

或者:

int deleteByIds(List<Long> ids);

BrandMapper.xml中配置SQL语句:

<!-- 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中编写并执行测试:

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

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

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

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

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

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

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

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

int updateById(Brand brand);

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

<!-- 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>标签,其基本格式是:

<choose>
<when test="条件">
满足条件时的SQL片段
</when>
<otherwise>
不满足条件时的SQL片段
</otherwise>
</choose>

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

<if test="pinyin != null">
某代码片段
</if>
<if test="pinyin == null">
某代码片段
</if>

使用Mybatis实现查询数据

统计查询

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

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

select count(*) from pms_brand;

BrandMapper接口中添加抽象方法:

int count();

BrandMapper.xml中配置SQL:

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

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

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

指定条件的单一结果查询

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

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

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

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

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

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接口中添加抽象方法:

BrandDetailVO getById(Long id);

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

<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语句中,自定义别名,使得列名与属性名相同,则可以实现自动封装,例如:

<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属性一起使用,例如:

<select id="xx" resultMap="DetailResultMap">
</select>

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

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

<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语句大致是(暂时使用星号表示字段列表):

select * from pms_brand order by id

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

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

BrandMapper接口中添加抽象方法:

List<BrandListItemVO> list();

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

<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>进行引用,例如:

<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

@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;

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

InsertUpdateTimeInterceptor.java

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;
}

}