学习记录之Mybatis框架
Mybatis框架解决了数据库编程相关的问题,主要是简化了数据库编程。
当使用Mybatis框架实现数据库编程时,只需要:
1.定义数据操作功能的抽象方法(此抽象方法必须在接口中)
2.配置以上抽象方法映射的SQL语句
Mybatis框架在实现过程中,会自动生成各接口的代理对象,所以,开发人员并不需要关注接口的实现问题。
使用Mybatis框架:
在Spring Boot项目中,当需要使用Mybatis框架实现数据库编程时,需要添加:
1.mybatis-spring-boot-starter
2.数据库的依赖,例如mysql-connector-java
所以,在pom.xml中添加:
由于添加以上依赖后,Spring Boot在启动时就会读取连接数据库的配置信息,如果未配置,则启动报错且失败,需要在src/main/resources下的application.properties中添加必要的配置:
提示:以上配置中,属性名称是固定,以上示例值是错误值,但是,启动Spring Boot只是加载以上配置,并不会执行连接,所以,配置值错误并不影响启动项目。
连接数据库的配置:
在Spring Boot项目中,src/main/resources下的application.properties是默认的配置文件,项目在启动时,Spring Boot会自动从此文件中读取相关的配置信息。
在许多配置过程中,需要在application.properties中的配置的属性的名称是固定的!
在配置数据库的连接信息时,至少需要配置spring.datasource.url、spring.datasource.username、spring.datasource.password这3个属性,分别表示连接数据库的URL、登录数据库的用户名、登录数据库的密码
为了检验配置值是否正确,可以在src/test/java下默认的包下创建DatabaseConnectionTests测试类,在测试类中编写测试方法,以尝试连接数据库,即可检验:
关于设计数据表:
关于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语句大致是:
先在项目的默认包下创建pojo.entity.Brand类,类中的属性应该与数据表对应:
接下来,准备接口和抽象方法,在项目的默认包下创建mapper.BrandMapper接口,并在接口中添加抽象方法:
关于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语句不需要使用分号结束
例如配置为:
最后,还需要补充一个配置,用于告诉Mybatis框架这类XML文件的位置!在application.properties中添加:
另外,在插入数据时,还可以配置,得到自动编号的ID值,具体做法是在<insert>节点上添加配置:
<!-- int insert(Brand brand); --><insert id="insert" useGeneratedKeys="true" keyProperty="id"> 暂不关心此处的SQL语句</insert>
使用Mybatis实现批量删除数据
在Mybatis中,有“动态SQL”的机制,它允许根据调用方法时传入的参数值不同,来生成不同的SQL语句。
目标:根据若干个id一次性删除多个品牌。
需要执行的SQL语句大致是:
或者:
注意:以上SQL语句中的id的数量是不确定的。
在BrandMapper接口中,抽象方法可以是:
或者:
或者:
在BrandMapper.xml中配置SQL语句:
由于需要对参数ids(若干个id)进行遍历,需要使用到动态SQL中的<foreach>节点,此节点可以对数组或集合进行遍历!关于<foreach>的配置:
collection属性:表示被遍历的参数对象,当抽象方法的参数只有1个,且没有添加@Param注解时,当参数值的类型是数组时,此属性值为array,当参数值的类型是List时,此属性值为list;否则,此属性值为@Param注解中的参数值item属性:表示被遍历到的元素的名称,是自定义的名称,在<foreach>内部,使用#{}格式的占位符时,也使用此属性来表示每个元素separator属性:表示遍历过程中各元素值之间的分隔符号
最后,在BrandMapperTests中编写并执行测试:
使用Mybatis实现动态SQL的修改数据
在动态SQL机制中,可以使用<if>标签,可用于对某参数值进行判断,从而生成不同的SQL语句片段,常用于设计更新数据的操作。
目标:使用1个方法,实现多种不同的数据更新(想更新哪些字段就更新哪些字段,不想更新的字段值将保持不变)
需要执行的SQL语句大致是:
注意:以上SQL语句的修改的字段列表应该不是固定的,应该根据传入的参数值来决定。
先在BrandMapper接口中添加抽象方法:
然后,在BrandMapper.xml中进行配置:
需要注意的是,在Mybatis的动态SQL中,<if>并没有对应的<else>,如果一定要实现类似Java中的if...else效果,需要使用<choose>标签,其基本格式是:
或者,也可以使用2个条件完全相反的<if>标签来实现类似效果(但是执行效率偏低),例如:
使用Mybatis实现查询数据
统计查询
目标:统计品牌表中的数据的数量
需要执行的SQL语句大致是:
在BrandMapper接口中添加抽象方法:
在BrandMapper.xml中配置SQL:
注意:所有查询节点(<select>)必须配置resultType或resultMap这2个属性中的其中1个。
当使用resultType声明封装结果的数据类型时,取值与抽象方法的返回值对应,如果是基本类型,直接写类型名称即可,例如resultType="int",如果是引用数据类型,在java.lang包下的可以直接写类名,其它包下的写全限定名。
指定条件的单一结果查询:
目标:根据id查询品牌详情
需要执行的SQL语句大致是:
由于不推荐使用星号表示字段列表,并且,在实际查询时,可能有部分字段是不需要体现在查询结果中的,推荐的做法是针对所需的查询字段,另外创建类型进行结果的封装。
例如,在pojo.vo下创建BrandDetailVO类:
在BrandMapper接口中添加抽象方法:
然后,在BrandMapper.xml中配置SQL:
另外,在查询时,一定要明确几个概念:
字段(Field):在创建数据表时指定的名称(后续也能修改表结构时改名)
列(Column):查询的结果集中的每一竖排,列名默认情况下是字段名,如果查询时指定了别名,则列名就是指定的别名
属性( Property):类中的属性
Mybatis在执行查询时,会尝试自动的将结果集中的数据封装到返回结果类型的对象中,但是,它只能自动处理列名与属性名相同的部分,如果列名与属性名不同,默认并不能自动封装!
可以在查询的SQL语句中,自定义别名,使得列名与属性名相同,则可以实现自动封装,例如:
以上的product_count AS productCount自定义别名可以保证product_count字段的值可以被自动封装!
更推荐使用<resultMap>来指导Mybatis如何封装结果,它将与<select>标签的resultMap属性一起使用,例如:
然后,在<resultMap>内部,使用<result>来配置列名与属性名的对应关系,例如:
提示:在使用<resultMap>配置时,从规范的角度出发,每个列与属性的关系都需要显式的配置出来(即使从功能实现的角度来看可能并不需要),另外,还应该使用<id>节点对主键进行配置。
查询列表
目标:查询品牌列表
需要执行的SQL语句大致是(暂时使用星号表示字段列表):
通常,查询列表时,所需要查询的字段与查单个数据可能是不同的,所以,可能需要自定义新的VO类作为List中的元素类型,为了避免写完代码后发现某个VO不能复用于查询单个数据和查询列表这2个功能,推荐一开始就定义用于封装列表项结果的VO类,例如:
在BrandMapper接口中添加抽象方法:
然后,在BrandMapper.xml中配置SQL:
关于<sql>与<resultMap>
在使用XML配置SQL语句时,可以使用<sql>封装SQL语句片段,并使用<include>进行引用,例如:
通过,使用<sql>封装字段列表,而<resultMap>通常与之对应,所以,这2个节点的id命名通常会使用相同的关键词,例如<sql>配置为id="ListItemQueryFields",并且<resultMap id="ListItemResultMap">,甚至,在编码时,会把这2个节点放在相邻的位置。
提示:使用<sql>封装字段列表时,IntelliJ IDEA可能会误判为错误的语法,将字段列表使用<if test="true>"框住(或将此<if>添加在之前)即可避免出现这样的错误提示。
关于异常
BindingException
绑定异常,异常提示信息如下:
出现此异常是因为找不到与抽象方法对应的SQL语句,原因可能有:
在XML中配置的接口名有误
<mapper>节点的namespace属性值有误在XML中配置的抽象方法名称有误
<insert>或类似节点的id属性值有误在配置文件中指定的XML路径,此项配置有误
application.properties中配置的mybatis.mapper-locations属性有误如果十分确实以上代码都没有问题,则只可能是依赖项出错
先检查
pom.xml中的依赖代码是否正确,如果无误,则删除本地仓库并重新下载
关于#{}和${}格式的占位符
提示:在开发实践中,并不推荐使用${}格式的占位符。
假设需要实现: 分页查询品牌数据。
需要执行的SQL语句大致是:
以上SQL语句中,需要使用到2个参数,分别表示“跳过几条记录”、“查询几条记录”。
在BrandMapper中添加抽象方法:
提示:offset表示“偏移量”。
在BrandMapper.xml中配置SQL:
完成后,在BrandMapperTests中测试:
经过测试,可以发现,在配置SQL时,2个参数无论使用#{}还是${}都可以正常运行!
假设需要实现:根据名称查询品牌详情
在BrandMapper中添加抽象方法:
在BrandMapper.xml中配置SQL:
完成后,在BrandMapperTests中测试:
经过测试,可以发现,在配置SQL时,参数使用#{}可以正常运行,但是使用${}会出错!
其实,#{}和${}这2种占位符的处理机制是不同的!
当占位符是#{}时,是预编译的,会先将SQL语句中的参数使用?表示,当编译通过后,再将参数值代入并执行。
当占位符是${}时,不是预编译的,会将各参数值先拼接到SQL语句中,然后再执行编译流程,完成后,再执行SQL语句。
以分页查询为例,当使用#{}时,其大致SQL是:
以上SQL会先执行编译流程,完成后,再将参数值代入并执行SQL语句。
当使用${}时,需要先将参数值代入,假设offset的值是5,count的值是3,则其SQL大致是:
然后,会将以上SQL语句进行编译流程,最终执行SQL语句。
换成根据名称来查询时,当#{}占位符时,其SQL语句大致是:
当使用${}占位符时,依然是先使用参数值替换占位符,得到的SQL语句大致是:
由于华为这2个字并没有添加引号表示这是一个字符串,所以,会被视为字段名,最终执行时的错误就是:
所以,当使用${}格式的占位符,且参数值是字符串类型时(其实,其它非数值型都是如此),需要考虑数据类型的问题,例如,以上出错时,将测试数据的华为改成'华为'即可。
结论:使用${}格式的占位符,需要自行考虑数据类型的问题,但是,使用#{}时,并不需要。
另外,如果不使用预编译的做法,由于参数值可以改变语义,所以,还存在SQL注入的风险!
所以,为了保证SQL语句不会被注入,不应该使用${}格式的占位符!
提示:即便${}格式的占位符是有SQL注入风险的,但是,也并不是不能解决的,可以在执行SQL语句之前,对参数值使用正则表达式检查,如果存在SQL注入的关键字时,不执行SQL语句即可。
Mybatis的缓存机制
缓存:是一种临时存储数据的机制,甚至,使用这种机制存储的数据也只是临时使用而已。
通常,使用缓存存储数据时,都会比其它的某种处理机制更快(访问数据的速度更快,效率更高)!
当使用Mybatis实现增删改查的数据访问时,本质上,程序是运行在APP服务器上,而数据库通常都在另一台服务器上,则访问数据的效率非常低(需要在2台电脑之间进行互相通信,另外,还可以处理SQL语句、查询结果等),特别是查询数据非常多的数据表,在没有索引等机制的情况下,每次查询耗时都非常久!
通常,会使用缓存来解决查询效率低下的问题(与增删改无关),Mybatis内置的缓存机制就是将查询到的结果临时存储到APP服务器上,下次执行相同的查询时,直接将APP服务器上的结果进行返回即可,并不会真实的查询MySQL数据库服务器中的数据。'

Mybatis在处理缓存时,区分为一级缓存和二级缓存(是2种不同的缓存机制,但可同时存在)。
Mybatis的一级缓存又被称之为“会话缓存”,是默认开启的,且无法关闭,其能够缓存数据的条件是:
必须是同一个会话
必须是通过同一个Mapper接口的对象执行的查询
必须是执行完全相同的SQL语句
必须是完全相同的SQL参数
当满足以上条件时,查询到的每个数据都会被Mybatis缓存下来,并且,下次查询的还是此数据时,将直接返回此前缓存的结果。
一级缓存会在关闭Session、通过SqlSession清空缓存、通过此会话的Mapper使得表中的数据发生修改时自动清空缓存数据!
Mybatis的二级缓存又称之为"namespace缓存",是作用于每一个XML配置的缓存,也就是说,只要执行的是同一个XML中的同一个查询,且参数相同,即使使用不同的会话,也可以共用缓存数据!
在整合Spring的Mybatis工程中(包括Spring Boot),二级缓存默认是全局开启,但各namespace默认未开启的,如果需要开启各namespace缓存,需要在XML文件中添加<cache/>节点(此节点直属于根节点,与其它同级节点不区分先后顺序)。
注意:二级缓存的数据必须在sqlSession提交(commit)或关闭(close)后才会产生。
注意:Mybatis在执行查询时,会先查找二级缓存,如果命中,直接返回结果,如果未命中,则查询一级缓存,如果命中则返回一级缓存中的结果,如果仍未命中,则执行实际的查询(连接到数据库服务器查询数据)。
提示:使用二级缓存后,输出中的日志中将包含Cache Hit Ratio信息,此信息表示的是“缓存数据的命中率”,将在日志的尾部通过0.0、0.5类似的数值表示。
另外,在每个查询的<select>上还可以配置useCache属性,取值为true / false,表示此查询是否使用缓存,默认值为true。
另外,当开启二级缓存后,封装查询结果的类型必须实现Serializable接口,否则就会出现不可序列化的异常!
二级缓存也会因为当前namespace中执行了增删改操作而清空缓存数据!
【总结】
Mybatis内置的缓存机制有一级缓存和二级缓存这2种,并且,在执行查询时,会先查找二级缓存,再查找一级缓存。
其中,一级缓存是会话缓存,必须是同一个会话、同一个Mapper、执行同样的SQL、且使用同样的SQL参数,才会应用缓存,且一级缓存会因为关闭会话、在会话中主动清除缓存、使用此会话的Mapper执行了任何写操作后自动清除缓存数据,一级缓存是默认开启的,一定程度上人为不可控;二级缓存是Namespace缓存,即使不同的会话也可以使用到缓存数据,默认是全局开启、各Namespace未开启的状态,当需要使用Namespace缓存时,需要在XML文件中添加<cache/>节点以开启当前Namespace的二级缓存,另外,还可以在各<select>节点上配置useCache属性,以配置某个查询功能是否使用二级缓存,并且,使用二级缓存时,需要封装结果的类型实现了Serializable接口,二级缓存也会因为写数据而被清除。
由于无论是一级缓存还是二级缓存,都会因为发生了数据的写操作后清除缓存数据,以保证缓存数据的准确性,但是,这种机制并不一定符合开发实践中的应用需求,在开发实践中,可能并不需要每时每刻关注每个数据的准确性,例如微博的热搜、头条的热榜的机制是每10分钟或15分钟更新一次缓存数据,通过Mybatis的缓存机制是做不到这一点的,所以,通常会使用自定义策略的缓存做法,例如使用Redis来处理缓存。
自动更新gmt_create和gmt_modified的Mybatis拦截器
MybatisConfiguration.java
InsertUpdateTimeInterceptor.java

