最近节日
正在加载日历...----
Mybatis框架解决了数据库编程相关的问题,主要是简化了数据库编程。

Mybatis框架解决了数据库编程相关的问题,主要是简化了数据库编程。
当使用Mybatis框架实现数据库编程时,只需要:
Mybatis框架在实现过程中,会自动生成各接口的代理对象,所以,开发人员并不需要关注接口的实现问题。
在Spring Boot项目中,当需要使用Mybatis框架实现数据库编程时,需要添加:
mybatis-spring-boot-startermysql-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.url、spring.datasource.username、spring.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、读取配置等)@SpringBootTestpublic 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字段,且是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时,定义的抽象方法都必须在接口中,通常,接口会使用Mapper作为名称的最后一个单词,例如命令为BrandMapper等。
关于抽象方法的声明原则:
int作为返回值类型,表示“受影响的行数”,其实也可以使用void
,但并不推荐,
如果需要执行的SQL是查询类型的,如果查询最多只返回1个结果,则只需要保证返回值类型可以装得下所需的查询结果即可,
如果查询返回的结果可能超过1条,则必须使用List集合进行封装,且集合的元素类型依然只需要保证可以装得下所需的查询结果即可Mybatis框架只要求开发人员编写接口和抽象方法,不需要开发人员编写实现类,是因为Mybatis会通过代理模式自动生成接口的实现对象, 但是,它需要明确哪些接口是需要生成代理对象的。
可以在各接口上添加@Mapper注解,在启动项目时,Mybatis会对整个项目进行扫描,对于已经添加此注解的接口,就会生成代理对象。
也可以在配置类上添加@MapperScan注解,用于指定各接口所在的包,则Mybatis会扫描此包及其子孙包下的所有接口,并生成这些接口的代理对象。
关于@Mapper和@MapperScan这2种注解,只需要选择其中1种使用即可,通常推荐@MapperScan。
注意:使用@MapperScan时,一定只指向Mapper接口所在的包,并确保此包下无其它接口!
提示:Mybatis框架的@MapperScan,与Spring框架的@ComponentScan没有任何关系,且不会互相影响!
以实现“插入品牌数据”为例,需要执行的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属性,取值为对应的接口的全限定名<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/resources的mapper文件夹下,通过复制粘贴得到AlbumMapper.xml文件AlbumMapper.xml中的根节点的namespaceAlbumMapper.xml中,在根节点内部添加<insert>节点,并配置id、useGeneratedKeys、keyProperty属性,在<insert>
节点内部配置SQL语句src/test/java下的cn.tedu.csmall.server.mapper包(你的包名可能不同)下创建AlbumMapperTests
测试类,在类中自动装配AlbumMapper对象,并编写、执行测试方法目标:根据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中编写并执行测试:
@Testpublic void testDeleteById() { Long id = 1L; int rows = mapper.deleteById(id); System.out.println("删除完成,受影响的行数=" + rows);}目标:实现根据id删除pms_album表中的数据。
目标:实现根据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中编写并执行测试:
@Testpublic void testUpdateNameById() { Long id = 3L; String name = "白萝卜"; int rows = mapper.updateNameById(id, name); System.out.println("修改品牌名称完成,受影响的行数=" + rows);}目标:实现根据id修改pms_album表中某条数据的name字段值。
在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中编写并执行测试:
@Testpublic void testDeleteByIds() { int rows = mapper.deleteByIds(1L, 3L, 5L, 7L, 9L); System.out.println("批量删除完成,受影响的行数=" + rows);}在动态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>目标:统计品牌表中的数据的数量
需要执行的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>)必须配置resultType或resultMap这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>另外,在查询时,一定要明确几个概念:
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>
添加在之前)即可避免出现这样的错误提示。
完成以下查询功能:
gmt_create和gmt_modified字段的值gmt_create和gmt_modified字段的值gmt_create和gmt_modified字段的值where parent_id=?)
gmt_create和gmt_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文件固定声明 - 或用模板 或复制粘贴 或工具生成
必须在根节点
MybatisConfiguration.java
@Autowiredprivate List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstructpublic 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; }
}