关于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 <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > 2.2.2</version > </dependency > <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.url
、spring.datasource.username
、spring.datasource.password
这3个属性,分别表示连接数据库的URL、登录数据库的用户名、登录数据库的密码
1 2 3 4 5 spring.datasource.url =jdbc:mysql://localhost:3306 /xxx?useUnicode=true &characterEncoding=utf-8 &serverTimezone=Asia/Shanghaispring.datasource.username =rootspring.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;@SpringBootTest public class DatabaseConnectionTests { @Autowired DataSource dataSource; @Test public void testConnection () throws Exception { 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; }
接下来,准备接口和抽象方法,在项目的默认包下创建mapper.BrandMapper
接口,并在接口中添加抽象方法:
1 2 3 4 5 6 7 8 9 10 11 12 package cn.tedu.csmall.server.mapper;public interface BrandMapper { 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"> < ! < 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 < ! < 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
中的根节点的namespace
在AlbumMapper.xml
中,在根节点内部添加<insert>
节点,并配置id
、useGeneratedKeys
、keyProperty
属性,在<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 int deleteById (Long id) ;
在BrandMapper.xml
中配置SQL:
1 2 3 4 < ! < 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 int updateNameById (@Param("id") Long id, @Param("name") String name) ;
在BrandMapper.xml
中配置SQL:
1 2 3 4 < ! < 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 < ! < delete id= "deleteByIds"> delete from pms_brand where id in ( < foreach collection= "array" item= "id"> #{id}, < / foreach> 0 ) < / delete > < ! < 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 < ! < 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
接口中添加抽象方法:
在BrandMapper.xml
中配置SQL:
1 2 3 4 < ! < select id= "count" resultType= "int"> SELECT count (* ) FROM pms_brand < / select >
注意:所有查询节点(<select>
)必须配置resultType
或resultMap
这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 { 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; }
在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 { }
在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_create
和gmt_modified
字段的值
根据id查询类别详情
除了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文件固定声明 - 或用模板 或复制粘贴 或工具生成
必须在根节点的namespace中指定对应的接口
应根据sql语句来选择、、、节点
在等4个节点中,必须配置id属性,取值为抽象方法的名称
实现插入数据,还可以配置useGeneratedKeys和keyProperty属性,用于获取自动编号的id
必须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;@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" ; private static final int SQL_TYPE_OTHER = 0 ; private static final int SQL_TYPE_INSERT = 1 ; private static final int SQL_TYPE_UPDATE = 2 ; private static final String SQL_TYPE_PATTERN_INSERT = "^insert\\s" ; private static final String SQL_TYPE_PATTERN_UPDATE = "^update\\s" ; private static final String SQL_STATEMENT_PATTERN_MODIFIED = ",\\s*" + FIELD_MODIFIED + "\\s*=" ; private static final String SQL_STATEMENT_PATTERN_CREATE = ",\\s*" + FIELD_CREATE + "\\s*[,)]?" ; private static final String SQL_STATEMENT_PATTERN_WHERE = "\\s+where\\s+" ; private static final String SQL_STATEMENT_PATTERN_VALUES = "\\)\\s*values?\\s*\\(" ; @Override public Object intercept (Invocation invocation) throws Throwable { log.debug("准备拦截SQL语句……" ); BoundSql boundSql = getBoundSql(invocation); String sql = getSql(boundSql); log.debug("原SQL语句:{}" , sql); String newSql = null ; switch (getOriginalSqlType(sql)) { case SQL_TYPE_INSERT: log.debug("原SQL语句是【INSERT】语句,准备补充更新时间……" ); newSql = appendCreateTimeField(sql, LocalDateTime.now()); break ; case SQL_TYPE_UPDATE: log.debug("原SQL语句是【UPDATE】语句,准备补充更新时间……" ); newSql = appendModifiedTimeField(sql, LocalDateTime.now()); break ; } 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); 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) { Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_CREATE, Pattern.CASE_INSENSITIVE); if (gmtPattern.matcher(sqlStatement).find()) { log.debug("已经包含 gmt_create 不再添加 时间字段" ); return null ; } StringBuilder sql = new StringBuilder (sqlStatement); Pattern valuesClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_VALUES, Pattern.CASE_INSENSITIVE); Matcher valuesClauseMatcher = valuesClausePattern.matcher(sql); 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); 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) { } 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失败!请检查拦截器配置!" ); } } private String getSql (BoundSql boundSql) { return boundSql.getSql().toLowerCase().replaceAll("\\s+" , " " ).trim(); } 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); } 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; } }