Mybatis
Mybatis
1. 使用步骤
1. Maven配置文件【pom.xml】
<dependencies>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<!-- mysql 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- 添加slf4j日志api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- 添加logback-classic依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- 添加logback-core依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
2.properties配置为文件
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.username=root
jdbc.password=abc123
jdbc.url=jdbc:mysql://localhost:3306/atguigudb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
3. 核心配置文件【mybatis-config.xml】
properties:resource是properties的文件名,此方式可以更好管理数据库连接信息
settings:每个setting子标签都是进行全局设置,name属性可以是下列值,value是布尔类型
- mapUnderscoreToCamelCase(下划线改驼峰),true则自动转换
- lazyLoadingEnabled(延迟加载),开启延迟加载后,sql查询时只加载需要的属性,true则开启延迟加载,此设置可以根据需要,在各个配置文件中自定义映射的
<association>,<collection>标签的fetchType属性进行设置,lazy为延迟加载,eager为立即加载typeAliases:给实体类起别名,采用包扫描,package子标签的name属性填写包名,使得类在配置文件中默认值为实体类名,不配置的话需要写全类名
environments:配置数据库连接环境信息。可以配置多个environment,通过default属性切换不同的environment
transactionManager
JDBC:手动事务管理,即手动提交回滚
MANAGED:事务被管理,如Spring中的AOP
dataSource:POOLED(使用连接池),UNPOOLED,JNDI
Mapper:包扫描的方式,package子标签的name属性填写包名,使得包下所有Mapper配置文件得到匹配
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"></properties>
<settings>
<!--将表中字段的下划线自动转换为驼峰-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!--开启延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
<typeAliases>
<package name="com.botuer.pojo"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!--数据库连接信息-->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.botuer.mapper"/>
</mappers>
</configuration>
4. 映射配置文件【BrandMapper.xml】
文件名和接口名相同,以”类+Mapper“的格式命名
路径和同名接口的路径相同(com/botuer/mapper)注意用/代替.
【namespace】填写代理接口全类名
每个Statement的【id】必须和同名接口的方法一一对应
【resultType】填写含包的类的完整路径,但是字段名和对应属性名的命名方式不一样,尽管可以使用字段别名,但还是很繁琐,可以用sql标签提供sql片段,提高复用,但很不灵活
- 我们定义resultMap标签来替换resultType属性
- resultMap的id属性作为唯一标识,替换resultType的属性值
- resultMap的type属性是映射的类型,一般为pojo的类名
- result子标签是对一般字段名的映射,id子标签是对主键字段名的映射
- 子标签中column属性为字段名,property为属性名,形成替换
【可省略】配置
ParameterType来指定参数类型特殊字段
使用转义字符,
<就是<使用<![CDATA[内容]]>,idea中输入CD直接提示生成
<?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="com.botuer.mapper.BrandMapper">
<!--此部分使用resultMap标签替换
<select id="selectAll" resultType="com.botuer.pojo.Brand">
select * from Brands2;
</select>
-->
<resultMap id="brandResultMap" type="Brand">
<result column="brand_name" property="brandName"/>
<result column="company_name" property="companyName"/>
</resultMap>
<select id="selectAll" resultMap="brandResultMap">
select *
from tb_brand;
</select>
</mapper>
5.日志配置文件【logback.xml】
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
CONSOLE :表示当前的日志信息是可以输出到控制台的。
-->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%level] %blue(%d{HH:mm:ss.SSS}) %cyan([%thread]) %boldGreen(%logger{15}) - %msg %n</pattern>
</encoder>
</appender>
<logger name="com.botuer" level="DEBUG" additivity="false">
<appender-ref ref="Console"/>
</logger>
<!--
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
, 默认debug
<root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。
-->
<root level="DEBUG">
<appender-ref ref="Console"/>
</root>
</configuration>
6. 提供对应的实体类
放在pojo包下
7. 提供对应的接口
放在mapper包下
接口名要和映射文件名相同
方法名要和Statement的id相同
public interface BrandMapper {
//查询所有
List<Brand> selectAll();
//查看详情:根据Id查询
Brand selectById(int id);
//添加
void add(Brand brand);
//修改
int update(Brand brand);
}
8. 创建Utils类
在utils包下创建
提供获取SqlSession的静态方法
- sqlSessionFactory.openSession(true)此方法调用时,true表示自动提交,空参默认false
还可以直接提供获取对应实体类Mapper的静态方法
public class SqlSessionUtils {
public static SqlSession getSqlSession(){
//1. 加载mybatis的核心配置文件,获取 SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取SqlSession对象,用它来执行sql
return sqlSessionFactory.openSession(true);
}
public static BrandMapper getBrandMapper(){
//3. 获取UserMapper接口的代理对象
SqlSession sqlSession = getSqlSession();
return sqlSession.getMapper(BrandMapper.class);
}
}
9. 创建测试类
在测试文件夹的对应包下创建
public class BrandTest {
@Test
public void sestSelectAll throws IOException {
//3. 获取UserMapper接口的代理对象,并执行sql
BrandMapper brandMapper = SqlSession.getBrandMapper();
List<Brand> brands = brandMapper.selectAll();
System.out.println(brands);
//4. 释放资源
sqlSession.close();
}
}
2. 基础操作
1. 占位符
参数占位符#{}替换?,底层使用的仍然是
PreparedStatement#{}的{}中填写的内容和属性对应
${},特殊情况才用(用的话加单引号),底层使用的是
Statement,存在Sql注入
2.sql片段(可选)
不仅可以为字段起别名,更重要的是提高复用性
<sql id="empColumns">
eid,ename,age,sex,did
</sql>
select <include refid="empColumns"></include>
from t_emp
3. 获取参数值
单条件查询时,一个参数,#{}中可以填任意值,但是一般填
对应属性名多条件查询需要用到多字段,多个字面量类型,有四种方式:自动生成Map,手动创建Map,创建实体类对象,使用@param注解
3.1
(不推荐)接口中方法的参数为多个时,自动把参数放到map集合中,以arg0,arg1...为键,以参数为值,或者以param1,param2...为键,以参数为值,通过#{}访问map集合的键来获取对应参数值#{}可以写成#{arg0} and #
#{}也可以写成#{param1} and #
#{}还可以写成#{arg0} and #
List<Brand> selectByCondition(String brandName,String companyName);<select id="selectByCondition" resultMap="brandResultMap"> select * from tb_brand where brand_name = #{arg0} and company_name = #{arg1}; </select>List<Brand> brands = brandMapper.selectByCondition("三只松鼠", "三只松鼠股份有限公司");
3.2 接口方法参数类型为map,可以手动创建map集合
在测试类中创建map集合
往map集合中添加键值对
List<Brand> selectByCondition(Map map);<select id="selectByCondition" resultMap="brandResultMap"> select * from tb_brand where brand_name = #{brandName} and company_name = #{companyName}; </select>Map<String,String> map = new HashMap<>(); map.put("brandName","三只松鼠"); map.put("companyName","三只松鼠股份有限公司"); List<Brand> brand = brandMapper.selectByCondition(map);
3.3
使用最频繁接口方法参数类型为实体类,可以手动创建实体类对象在测试类创建实体类对象
通过set方法给属性赋值
int insertBrand(Brand brand);<insert id="insertBrand"> insert into tb_brand (brand_name,company_name) values (#{brandName},#{companyName}); </insert>Brand brand = new Brand(); brand.setBrandName("联想"); brand.setCompanyName("联想集团"); int count = brandMapper.insertBrand(brand);
3.4
应用范围最广使用@param注解List<Brand> selectByCondition(@Param("brandName") String brandName,@Param("companyName") String companyName);<select id="selectByCondition" resultMap="brandResultMap"> select * from tb_brand where brand_name = #{brandName} and company_name = #{companyName}; </select>List<Brand> brands = brandMapper.selectByCondition("三只松鼠", "三只松鼠股份有限公司");
4. 返回值类型
4.1 增删改返回值为void或int,int用于接收受影响的行数
4.2 查询的返回值,要根据Statement语句具体分析
List集合可以接收0个、1个、多个实体类对象
map可以接收0个、1个、多个(map键都设为String,值都设为Object)
- 一个
Map<String,Object> selectByIdToMap(@Param("id")int id);<select id="selectByIdToMap" resultType="java.util.Map"> select * from tb_brand where id = #{id}; </select>Map<String, Object> stringObjectMap = brandMapper.selectByIdToMap(2);- 多个(方式一:用List接收Map)
List<Map<String,Object>> selectByIdToMap(@Param("id")int id);<select id="selectByIdToMap" resultType="java.util.Map"> select * from tb_brand where id <![CDATA[ < ]]>#{id}; </select>List<Map<String, Object>> maps = brandMapper.selectAllByIdToMap(3);- 多个(方式二:用@MapKey(“id”)注解,来确定多个Map以什么为键输出,一般用主键)
@MapKey("id") Map<String,Object> selectAllByIdToMap2(@Param("id")int id);<select id="selectByIdToMap" resultType="java.util.Map"> select * from tb_brand where id <![CDATA[ < ]]>#{id}; </select>Map<String, Object> stringObjectMap = brandMapper.selectAllByIdToMap2(3);
5. 自定义映射<resultMap>
5.1 resultMap标签不仅可以匹配别名(不管字段和属性匹不匹配,都写上),更重要的功能是多表查询
<id>子标签匹配主键别名,column属性是字段名,property属性是属性名,还可以在核心配置文件用<setting>进行全局配置<id property="id" column="id"></id> <result property="brandName" column="brand_name"></result>
5.2 多表查询--多对一(一对一)映射(此处以员工表和部门表为例,通常多方表含少方的主键,如:员工表含部门表的主键)
两个实体类必须有联系,
多对一映射是以emp为主表
Emp类中添加Dept的实例为对象private Dept dept;
方式一:级联
Employee selectByEmpId1(@Param("empId") Integer empId);<resultMap id="EmpResultMap1" type="employee"> <id column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="dept_id" property="deptId"/> <result column="dept_id" property="dept.deptId"/> <result column="dept_name" property="dept.deptName"/> </resultMap> <select id="selectByEmpId" resultMap="EmpResultMap1"> select * from emp left join dept on emp.dept_id = dept.dept_id where emp_id = #{empId} </select>Employee employee = employeeMapper.selectByEmpId(2);方式二:
<association>子标签表示关联,property属性是Emp类中两实体类关联属性(即Dept的实例),javaType属性是Dept类Employee selectByEmpId1(@Param("empId") Integer empId);<resultMap id="EmpResultMap2" type="employee"> <id column="emp_id" property="empId"/> <result column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="age" property="age"/> <result column="dept_id" property="deptId"/> <association property="dept" javaType="department"> <id column="dept_id" property="deptId"/> <result column="dept_name" property="deptName"/> </association> </resultMap> <select id="selectByEmpId1" resultMap="EmpResultMap2"> select * from emp left join dept on emp.dept_id = dept.dept_id where emp_id = #{empId} </select>Employee employee = employeeMapper.selectByEmpId1(2);方式三:分步查询
<association>,property属性是Emp类中两实体类关联属性(即Dept的实例),select属性是namespace.sqlId,column属性是关联条件的字段- 步骤一:查员工信息,得到部门id
Employee selectByEmpIdStepOne(@Param("empId") Integer empId);<resultMap id="EmpResultMap3" type="employee"> <id column="emp_id" property="empId"/> <result column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="age" property="age"/> <result column="dept_id" property="deptId"/> <association property="dept" select="com.botuer.mapper.DepartmentMapper.selectByEmpIdStepTwo" column="dept_id"> </association> </resultMap> <select id="selectByEmpIdStepOne" resultMap="EmpResultMap3"> select * from emp where emp_id = #{empId} </select>- 步骤二:根据得到的部门id查部门信息
Deprecated selectByEmpIdStepTwo(@Param("deptId") Integer deptId);<resultMap id="selectByEmpIdStepTwo" type="department"> <id column="dept_id" property="deptId"/> <result column="dept_name" property="deptName"/> </resultMap> <select id="selectByEmpIdStepTwo" resultMap="selectByEmpIdStepTwo"> select * from dept where dept_id = #{deptId} </select>Employee employee = employeeMapper.selectByEmpIdStepOne(3);
5.3 多表查询--一对多映射
两个实体类必须有联系,
多对一映射是以dept为主表
Dept类中添加Emp的实例为对象
private List<Employee> emps;方式一:
<collection>子标签表示集合,用法和<association>基本相同,property属性是Dept类中两实体类关联属性(即Emp的实例),ofType属性是Emp类Department selectByDeptId1(@Param("deptId") Integer deptId);<resultMap id="selectByDeptId1" type="department"> <id column="dept_id" property="deptId"/> <result column="dept_name" property="deptName"/> <collection property="emps" ofType="employee"> <id column="emp_id" property="empId"/> <result column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="age" property="age"/> <result column="dept_id" property="deptId"/> </collection> </resultMap> <select id="selectByDeptId1" resultMap="selectByDeptId1"> select * from dept left join emp on dept.dept_id = emp.dept_id where dept.dept_id = #{deptId} </select>Department department = departmentMapper.selectByDeptId1(2);方式二:
<collection>子标签表示集合,property属性是Dept类中两实体类关联属性(即Emp的实例),select属性是namespace.sqlId,column属性是关联条件的字段- 步骤一:查询部门信息
Department selectByDeptIdStepOne(@Param("deptId") Integer deptId);<resultMap id="selectByDeptIdStepOne" type="department"> <id column="dept_id" property="deptId"/> <result column="dept_name" property="deptName"/> <collection property="emps" select="com.botuer.mapper.EmployeeMapper.selectByDeptIdStepTwo" column="dept_id"></collection> </resultMap> <select id="selectByDeptIdStepOne" resultMap="selectByDeptIdStepOne"> select * from dept where dept_id = #{deptId} </select>- 步骤二:根据部门id查询员工信息
List<Employee> selectByDeptIdStepTwo(@Param("deptId") Integer deptId);<resultMap id="selectByDeptIdStepTwo" type="employee"> <id column="emp_id" property="empId"/> <result column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="age" property="age"/> <result column="dept_id" property="deptId"/> </resultMap> <select id="selectByDeptIdStepTwo" resultMap="selectByDeptIdStepTwo"> select * from emp where dept_id = #{deptId} </select>Department department = departmentMapper.selectByDeptIdStepOne(2);
5.4 延迟加载
核心配置文件中<settings>标签中<setting>子标签的name属性的值为lazyLoadingEnabled表示延迟加载,开启延迟加载后,sql查询时只加载需要的属性,value为true则开启延迟加载,此设置可以根据需要,在各个配置文件中自定义映射的<association>,<collection>标签的fetchType属性进行设置,lazy为延迟加载,eager为立即加载,设置的前提是lazyLoadingEnabled为true,否则都是立即加载
3. 应用场景
通用步骤
条件表达式
连接方式
编写接口方法
判断有无参数
判断返回结果类型
在映射配置文件中编写SQL语句
编写测试方法并执行
1. 多条件查询
条件表达式需要用到
模糊查询- 模糊查询需要用到%,有三种实现方式
不推荐方式一:'%${mohu}%'(存在sql注入)- 方式二:concat('%',#{mohu},'%')
最常用方式三:"%"#{mohu}"%"
连接方式用
AND,考虑动态性实际应用中,每个参数都可能不填,因此需要解决sql语句中不填写内容和开头就and的情况
解决参数为null,但仍需有返回,需要
<if test = "形参 != null and 形参 != '' "></if>解决where开始就and,方式一:加恒等式 1 = 1
解决where开始就and,方式二:需要
<where>``</where>,<where>标签只能去行首and或者or,不能去行尾的List<Brand> selectByBrandNameAndCompanyName(@Param("brandName")String brandName,@Param("companyName")String companyName);<select id="selectByBrandNameAndCompanyName" resultMap="brandResultMap"> select * from tb_brand <where> <if test="brandName != null and brandName != '' "> brand_name like "%"#{brandName}"%"; </if> <if test="companyName != null and companyName != '' "> and company_name like "%"#{companyName}"%"; </if> </where> </select>List<Brand> brands = brandMapper.selectByBrandNameAndCompanyName("三只", null);解决where开始就and:方式三:需要
<trim>``</trim>,四个属性(prefix、suffix 行首或行尾添加,prefixOverrides、suffixOverrides 行首或行尾去除)<select id="selectByBrandNameAndCompanyName" resultMap="brandResultMap"> select * from tb_brand <trim prefix = "where" prefixOverrides = "and" suffixOverrides = "or"> <if test="brandName != null and brandName != '' "> brand_name like "%"#{brandName}"%" or; </if> <if test="companyName != null and companyName != '' "> and company_name like "%"#{companyName}"%"; </if> </trim> </select>
2. 单条件查询
<if>标签没有if else的功能,在单选控件中使用很不方便<choos><when><otherewise>,<when>中有属性test填写条件List<Brand> selectByBrandNameAndCompanyNameAndDescription(@Param("brandName")String brandName,@Param("companyName")String companyName,@Param("description")String description);<select id="selectByBrandNameAndCompanyNameAndDescription" resultMap="Map"> select * from tb_brand <where> <choose> <when test="brandName != null and brandName != ''"> brand_name = #{brandName} </when> <when test="companyName != null and companyName != ''"> company_name = "%"#{companyName}"%" </when> <when test="description != null and description != ''"> description = "%"#{description}"%" </when> </choose> </where>; </select>List<Brand> brands = brandMapper.selectByBrandNameAndCompanyNameAndDescription("三只松鼠", "三只", "");
3. 添加数据并获取主键
添加数据:见基础操作--获取参数值--手动创建实体类
添加主键返回
在 insert 标签上添加如下属性:
useGeneratedKeys:获取自动增长的主键值。true表示获取
keyProperty :指定将获取到的主键值封装到哪个属性里
int insertBrand2( Brand brand);<insert id="insertBrand2" useGeneratedKeys="true" keyProperty="id"> insert into tb_brand (id, brand_name, company_name, ordered, description) values (#{id},#{brandName},#{companyName},#{ordered},#{description}); </insert>Brand brand = new Brand(null,"哇哈哈","娃哈哈集团",5,"niubi",null); brandMapper.insertBrand2(brand); System.out.println(brand);
4. 修改数据(动态)
实际应用中,很多不要的更新的未被修改,set语句中为设置的反应到数据库中就变成了null
这就需要
<set>``</set>,这样,未被修改的字段值还是原来的void update(Brand brand);<update id="update"> update tb_brand <set> <if test="brandName != null and brandName != ''"> brand_name = #{brandName}, </if> <if test="companyName != null and companyName != ''"> company_name = #{companyName}, </if> <if test="ordered != null"> ordered = #{ordered}, </if> <if test="description != null and description != ''"> description = #{description}, </if> <if test="status != null"> status = #{status} </if> </set> where id = #{id}; </update>brandMapper.update(brand);
5. 批量删除
批量删除的关键在个数是不固定的,删除几个,删除什么,是变化的
因此,删除操作执行前,首先要知道哪些数据需要删除,也就是要直到数据的唯一标识,一般为主键
把主键存放在一个数组或集合中,通过循环的方式实现批量删除,就需要用到
<foreach>标签,含5个属性- collection:需要循环的数组或集合
- 传入数组如果不用@Param注解给参数命名,那么默认为array,换句话说,不用注解,collection只能等于array
- 传入集合必须用@Param注解
- item:循环的元素,一般为主键或实体类对象
- 如果表示实体类对象,那循环的#{}中的属性,必须指明对象的属性,如#
- separator:每个元素的分隔符(连接符) separator="," separator="or"
- open:拼接内容开始符 open="("
- close:拼接内容结束符 close=")"
- collection:需要循环的数组或集合
目的是拼接后的效果要符合sql语法
例一:形如 delete from tb_brand where id in (1,2,3);
- 方式一:用
<foreach>标签
void deleteByIds1(int[] ids);<delete id="deleteByIds1"> delete from tb_brand where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </delete>int[] ids = {5,7,8}; brandMapper.deleteByIds1(ids);不推荐方式二:用${}拼串
void deleteMoreBatch(@Param("ids") String ids);<delete id="deleteMoreBatch"> delete from tb_brand where id in ${ids}; </delete>brandMapper.deleteMoreBatch("(5,7,9)");例二:形如 delete from tb_brand where id = 3 or id = 5;
void deleteByIds2(int[] ids);<delete id="deleteByIds2"> delete from tb_brand where id <foreach collection="ids" item="id" separator="or"> #{id} </foreach> </delete>int[] ids = {3,5}; brandMapper.deleteByIds2(ids);- 方式一:用
6.批量添加与修改
<foreach>还可用于批量添加,批量修改,只不过要把进行循环的元素(实体类对象)封装为List<>集合collection传入集合必须用@Param注解
item:循环的元素,如果表示实体类对象,那循环的#{}中的属性,必须指明对象的属性,如#
int insertMoreBatch(@Param("brands") List<Brand> brands);<insert id="insertMoreBatch"> insert into tb_brand values <foreach collection="brands" item="brand" separator=","> (null,#{brand.brandName},#{brand.companyName},#{brand.ordered},#{brand.description},null) </foreach> </insert>Brand brand1 = new Brand(null,"1","",5,"",null); Brand brand2 = new Brand(null,"2","",6,"",null); Brand brand3 = new Brand(null,"3","",7,"",null); List<Brand> brands = Arrays.asList(brand1, brand2, brand3); brandMapper.insertMoreBatch(brands);
7.动态设置表名
实际应用中,为了方便,我们经常会把一张表分为几张分表,查询某个分表的数据
表名不能含单引号,故只能使用$
List<Brand> getAllBrand(@Param("tableName") String tableName);<select id="getAllUser" resultType="User"> select * from ${tableName} </select>List<Brand> brands = brandMapper.getAllBrand("tb_brand");
4.注解实现CRUD
使用注解开发会比配置文件开发更加方便。如下就是使用注解进行开发
@Select(value = "select * from tb_user where id = #{id}")
public User select(int id);
注意:
- 注解是用来替换映射配置文件方式配置的,所以使用了注解,就不需要再映射配置文件中书写对应的
statement
- 查询 :@Select
- 添加 :@Insert
- 修改 :@Update
- 删除 :@Delete
<u>注解完成简单功能,配置文件完成复杂功能,提高代码可读性</u>
5.缓存
MyBatis的一级缓存
一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就 会从缓存中直接获取,不会从数据库重新访问
【失效的四种情况】SqlSession不同;查询条件不同;进行了增删改操作;手动清空(调用clearCache方法)
MyBatis的二级缓存
二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被 缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取
【开启条件】
1.核心配置文件中进行全局设置
<setting name="cacheEnabled" value="true"/>
2.在映射配置文件中设置标签<cache />,默认值为LRU,详情见【标签及属性】
3.SqlSession关闭或提交(调用close、 commit方法)
4.实体类必须实现序列化接口
public class Brand implements Serializable {}
【失效条件】进行了增删改操作(一二级同时实效)
【缓存查询顺序】二级 ----> 一级 ----> 数据库
第三方缓存EHCache
虽然MyBatis自带二级缓存,但有更专业的第三方二级缓存
添加依赖
<!-- Mybatis EHCache整合包 --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.1</version> </dependency> <!-- slf4j日志门面的一个具体实现 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>各个jar包功能
jar包名称 作用 mybatis-ehcache Mybatis和EHCache的整合包 ehcache EHCache核心包 slf4j-api SLF4J日志门面包 logback-classic 支持SLF4J门面接口的一个具体实现 创建EHCache配置文件【ehcache.xml】
<?xml version="1.0" encoding="utf-8" ?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"> <!-- 磁盘保存路径 --> <diskStore path="D:\atguigu\ehcache"/> <defaultCache> <!--必填--> <!--在内存中缓存的element的最大数目--> maxElementsInMemory="1000" <!--在磁盘上缓存的element的最大数目,若是0表示无穷大--> maxElementsOnDisk="10000000" <!--设定缓存的elements是否永远不过期。 如果为true,则缓存的数据始终有效, 如果为false那么还要根据timeToIdleSeconds、timeToLiveSeconds判断--> eternal="false" <!--设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上--> overflowToDisk="true" <!--可选--> <!--当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时, 这些数据便会删除,默认值是0,也就是可闲置时间无穷大--> timeToIdleSeconds="120" <!--缓存element的有效生命期,默认是0.,也就是element存活时间无穷大--> timeToLiveSeconds="120" <!--DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区--> diskSpoolBufferSizeMB = "50" <!--在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false--> diskPersistent = "false" <!--磁盘缓存的清理线程运行间隔,默认是120秒。每个120s, 相应的线程会进行一次EhCache中数据的清理工作--> diskExpiryThreadIntervalSeconds="120" <!--当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。 默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)--> memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>设置二级缓存类型【Mapper.xml】
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>加入logback日志【logback.xml】
存在SLF4J时,作为简易日志的log4j将失效,此时我们需要借助SLF4J的具体实现logback来打印日志。 创建logback的配置文件logback.xml(换句话说,可以用log4j 或 SLF4J + logback)
6. MyBatis逆向工程
正向工程:先创建Java实体类,由框架负责根据实体类生成数据库表。Hibernate是支持正向工程的。 逆向工程:先创建数据库表,由框架负责根据数据库表,反向生成如下资源:
- Java实体类
- Mapper接口
- Mapper映射文件
14.1创建逆向工程
添加依赖和插件
<!-- 依赖MyBatis核心包 --> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies> <!-- 控制Maven在构建过程中相关配置 --> <build> <!-- 构建过程中用到的插件 --> <plugins> <!-- 具体插件,逆向工程的操作是以构建过程中插件形式出现的 --> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.0</version> <!-- 插件的依赖 --> <dependencies> <!-- 逆向工程的核心依赖 --> <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.7</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.2</version> </dependency> </dependencies> </plugin> </plugins> </build>创建MyBatis核心配置文件【mybatis-config.xml】
创建逆向工程配置文件【generatorConfig.xml】
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- targetRuntime: 执行生成的逆向工程的版本 MyBatis3Simple: 生成基本的CRUD(清新简洁版) MyBatis3: 生成带条件的CRUD(奢华尊享版) --> <context id="DB2Tables" targetRuntime="MyBatis3"> <!-- 数据库的连接信息 --> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/atguigudb?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true" userId="root" password="abc123"> </jdbcConnection> <!-- javaBean的生成策略--> <javaModelGenerator targetPackage="com.botuer.pojo" targetProject=".\src\main\java"> <property name="enableSubPackages" value="true" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- SQL映射文件的生成策略 --> <sqlMapGenerator targetPackage="com.botuer.mapper" targetProject=".\src\main\resources"> <property name="enableSubPackages" value="true" /> </sqlMapGenerator> <!-- Mapper接口的生成策略 --> <javaClientGenerator type="XMLMAPPER" targetPackage="com.botuer.mapper" targetProject=".\src\main\java"> <property name="enableSubPackages" value="true" /> </javaClientGenerator> <!-- 逆向分析的表 --> <!-- tableName设置为*号,可以对应所有表,此时不写domainObjectName --> <!-- domainObjectName属性指定生成出来的实体类的类名 --> <table tableName="employees" domainObjectName="Employee"/> <table tableName="departments" domainObjectName="Department"/> </context> </generatorConfiguration>执行MBG插件的generate目标

image-20220513135122560 【注意】:需要手动生成构造器和toString
14.2 使用逆向工程:QBC查询
逆向工程创建的实体类包含两种:含Example的实体类里含满足各种复杂条件的方法
QBC 查询(Query By Criteria”通过条件查询“)
不含ByExample的方法,通过接口实例直接调用
含有ByExample的方法,需要创建$$$Example的实例,通过QBC方式调用$$$Example实例的方法
- 创建$$$Example的实例
- 调用$$$Example实例的QBC方法createCriteria()”创建条件“,返回值为Criteria类型,调用(一个或多个)add***方法
- 如果需要or连接,调用$$$Example实例的or()方法,返回值为Criteria类型,接着调用(一个或多个)add***方法
@Test public void testMBG() throws IOException { InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class); EmployeeExample employeeExample = new EmployeeExample(); employeeExample.createCriteria().andDepartmentIdIsNotNull().andSalaryBetween(6000.0,8000.0); employeeExample.or().andSalaryBetween(20000.0,30000.0); List<Employee> employees = employeeMapper.selectByExample(employeeExample); employees.forEach(System.out::println); }(添加/修改)含有Selective的方法,只更新非空的内容,空值不更新
7.分页插件
1 添加插件步骤
添加依赖
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency>配置分页插件【mybatis-config.xml】
<plugins> <!--设置分页插件--> <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin> </plugins>
2 使用插件方法(两步)
在查询功能之前使用PageHelper.startPage(int pageNum, int pageSize)开启分页功能
- pageNum第几页
- pageSize每页几条数据
在查询获取list集合之后,使用PageInfo
<T>pageInfo = new PageInfo<>(List<T>list, intnavigatePages)获取分页相关数据- list要分页的数据
- intnavigatePages,导航分页页码数(一般为奇数,当前页在中间)
//开启分页功能
Page<Object> page = PageHelper.startPage(2, 3);
//QBC查询
EmployeeExample employeeExample = new EmployeeExample();
employeeExample.createCriteria().andDepartmentIdIsNotNull().andSalaryBetween(6000.0,8000.0);
employeeExample.or().andSalaryBetween(20000.0,30000.0);
List<Employee> employees = employeeMapper.selectByExample(employeeExample);
//获取页面信息
PageInfo<Employee> employeePageInfo = new PageInfo<>(employees, 5);
//输出该页数据和页面信息
System.out.println(employeePageInfo);
employees.forEach(System.out::println);
分页相关数据
PageInfo{ pageNum=8, pageSize=4, size=2, startRow=29, endRow=30, total=30, pages=8,
list=Page{count=true, pageNum=8, pageSize=4, startRow=28, endRow=32, total=30, pages=8, reasonable=false, pageSizeZero=false},
prePage=7, nextPage=0, isFirstPage=false, isLastPage=true, hasPreviousPage=true, hasNextPage=false, navigatePages=5, navigateFirstPage4, navigateLastPage8, navigatepageNums=[4, 5, 6, 7, 8] }
- pageNum:当前页的页码
- pageSize:每页显示的条数
- size:当前页显示的真实条数
- total:总记录数
- pages:总页数
- prePage:上一页的页码
- nextPage:下一页的页码
- isFirstPage/isLastPage:是否为第一页/最后一页
- hasPreviousPage/hasNextPage:是否存在上一页/下一页
- navigatePages:导航分页的页码数
- navigatepageNums:导航分页的页码,[1,2,3,4,5]
8.标签及属性
1 【mybatis-config.xml】
<properties>"属性" 匹配jdbc.properties方便数据库连接 resource属性来赋值<properties resource="jdbc.properties"></properties><settings>"设置" 进行全局配置<setting>有name和value属性- name可为mapUnderscoreToCamelCase”下划线转驼峰“、lazyLoadingEnabled”延迟加载“、cacheEnabled“开启二级缓存”
- value是布尔型
<typeAliases>"类型别名" 给实体类起别名,设置后不必再用全类名<package>有name属性,填写实体类所在的包名
<plugins><plugin>- interceptor"拦截器" 填写插件全类名 分页插件,通过拦截数据进行分页,改变原页面信息,达到分页效果
<environments>"环境" 设置数据库连接,default属性设置默认连接<environment>设置每个连接,id属性是唯一标识<transactionManager>"事务管理器"- type属性可设置为JDBC(手动事务管理)、MANAGED(事务被管理,如Spring中的AOP)
<dataSource>"数据源",type属性可设置为POOLED(使用连接池),UNPOOLED,JNDI<property>"属性"- name填写driver、url、username、password
- value填写"${jdbc.driver}"...(注意,避免重复命名读取不到,jdbc.properties中键尽量详细)
<mappers>"映射"<package>有name属性,填写映射接口所在的包名
2 【Mapper.xml】
<mapper>"映射" namespace”命名空间“填写映射接口全类名,可以使statement语句的唯一标识更具体<sql>"sql片段" id是唯一标识<cache />"二级缓存"tpye 设置缓存类型,默认是MyBatis自带的,可设置为第三方的
eviction“回收”属性可填写LRU“最近最少使用的”、FIFO“先进先出”、FOFT“软引用--移除基于垃圾回收器状态和软引用规则的对象”、WEAK“弱引用--更积极地移除基于垃圾收集器状态和弱引用规则的对象“
flushInterval”刷新间隔“,时间为毫秒
size 引用数目(正整数)--代表缓存最多可以存储多少个对象,太大容易导致内存溢出
readOnly”只读“ 值为true时只读,值为false时读写--返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。
<resultMap>"映射结果", 自定义映射,id是唯一标识,type是返回类型<id>property属性和column属性 填写主键的 属性名和字段名<result>property属性和column属性 填写其他的 属性名和字段名<association>"关联" 用于多对一的多表查询property填写”多“的属性
javaType填写”少”的类名
<id>、<result>select填写第二步查询语句的唯一标识“namespace+id”或“对应接口的对应方法全路径”
column填写关联条件的字段
<collection>"集合" 用于一对多的多表查询- property
- ofType填写“多”的类名
<id>、<result>- select、column
·
<select>"查询"id唯一标识,要与接口方法一致
resultType 结果类型
resultMap填写自定义映射的id
<insert>"添加"- id
- useGeneratedKeys“使用生成的键” 填写true表示使用主键
- keyProperty“键属性” 填写把主键的值赋给哪个属性,一般是主键字段对应的属性
<delete>、<update>------------------------以下为查询或其他方法中用到的标签--------------------------
<include>"包含" refid填写sql片段的id<if>test填写条件<where><trim>- prefix、suffix 在行首、行尾 添加
- prefixOverrides、suffixOverrides 在行首、行尾 去除
<choose><when>test填写条件<otherwise>
<foreach>- collection 填写数组、集合名
- item“项” 填写元素名
- separator “分隔符” 填写连接字符
- open 填写循环内开始符
- close 填写循环内结束符
Mybatis 拦截器
Mybatis的核心对象
| 对象 | 作用 |
|---|---|
| SqlSession | 顶层api,和数据库交互的会话 |
| Executor | 执行器,生成语句,维护缓存 |
| StatementHandler | 封装了JDBC Statement操作,负责对JDBC statement 的操作 |
| ParameterHandler | 参数处理,将用户传递的参数转为Statement需要的参数 |
| ResultSetHandler | 处理结果集,将结果集转为集合 |
| TypeHandler | 处理java与sql数据类型的映射转换 |
| MappedStatement | 维护mapper.xml文件节点的封装 |
| SqlSource | 根据参数对象生成sql,封装到BoundSql中 |
| BoundSql | 动态sql和参数 |
| Configuration | 配置信息 |
两个注解
@Intercepts注解
属性是一个@Signature[]数组,而@Signature有3个属性
- type --- 拦截的目标对象类型,只能拦截Executor、ParameterHandler、StatementHandler、ResultSetHandler四个对象里面的方法
- method --- 拦截的方法
- args --- 方法对应的参数类型
两个注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Intercepts {
Signature[] value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
Class<?> type();
String method();
Class<?>[] args();
}
四个对象
Executor
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
// 增删改
int update(MappedStatement var1, Object var2) throws SQLException;
// 先查缓存
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;
// 查询
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
// 查询结果封装为 Cursor
<E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean var1) throws SQLException;
void rollback(boolean var1) throws SQLException;
CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);
boolean isCached(MappedStatement var1, CacheKey var2);
void clearLocalCache();
void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5);
Transaction getTransaction();
void close(boolean var1);
boolean isClosed();
void setExecutorWrapper(Executor var1);
}
StatementHandler
public interface StatementHandler {
// 从连接中获取一个Statement
Statement prepare(Connection var1, Integer var2) throws SQLException;
// 设置statement执行里所需的参数
void parameterize(Statement var1) throws SQLException;
// 批量
void batch(Statement var1) throws SQLException;
// 增删改
int update(Statement var1) throws SQLException;
// 查
<E> List<E> query(Statement var1, ResultHandler var2) throws SQLException;
<E> Cursor<E> queryCursor(Statement var1) throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
ParameterHandler
public interface ParameterHandler {
// 获取参数
Object getParameterObject();
// 设置参数
void setParameters(PreparedStatement var1) throws SQLException;
}
ResultSetHandler
public interface ResultSetHandler {
// 将Statement执行后产生的结果集(可能有多个结果集)映射为结果列表
<E> List<E> handleResultSets(Statement var1) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;
// 处理存储过程执行后的输出参数
void handleOutputParameters(CallableStatement var1) throws SQLException;
}
使用步骤
- 定义拦截器,实现
org.apache.ibatis.plugin.Interceptor,注册为组件@Component - 实现
Object intercept(Invocation var1) throws Throwable方法 - 配置到Spring管理 --- 配置类 或 配置文件
模糊查询转义
这里有两个问题
- mybatis-plus只能精确处理全模糊,左模糊、右模糊可能会出现按全模糊的情况处理
- mybatis-plus拦截带缓存的query方法时,会再转义回去,导致转义失效
package com.jd.diagnosis.interceptor;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Intercepts(
value = {
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
Object.class, RowBounds.class, ResultHandler.class})
}
)
@Component
@Slf4j
public class MybatisLikeQueryInterceptor implements Interceptor {
// 转义
private static final String ESCAPE_SYMBOL = "\\";
// like 匹配的正则
@Value("${mybatis.escape.regex.like:like\\s}")
private String REGEX_LIKE;
// 可从配置文件中获取,没有的话取【 默认值 \ _ % 】
@Value("#{'${mybatis.escape.symbols:\\,_,%}'.split(',')}")
private String[] symbols;
// 开关 【 默认 开】
@Value("${mybatis.escape.enabled:true}")
private boolean enableEscape;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 开关
if (!enableEscape) {
return invocation.proceed();
}
// 获取 拦截的方法的参数列表,此处只拦截了 query 方法
Object[] args = invocation.getArgs();
// 第一个参数 MappedStatement 封装了mapper.xml 配置和 sql语句
MappedStatement ms = (MappedStatement) args[0];
// 第二个参数 封装了 参数列表
Object parameter = args[1];
// 第三个参数 分页相关配置
RowBounds rowBounds = (RowBounds) args[2];
// 第四个参数 用于结果处理
ResultHandler resultHandler = (ResultHandler) args[3];
// 这里拦截的目标是执行器
Executor executor = (Executor) invocation.getTarget();
// 第五个参数,如果开启了缓存就有
CacheKey cacheKey;
// 第六个参数 动态sql 开启缓存才有
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if (args.length == 4) {
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
// sql类型 UNKNOWN,INSERT,UPDATE,DELETE,SELECT,FLUSH;
SqlCommandType sqlCommandType = ms.getSqlCommandType();
// StatementType
// 1、STATEMENT:直接操作sql,不进行预编译,获取数据:$ >> Statement
// 2、PREPARED:预处理,参数,进行预编译,获取数据:# >> PreparedStatement(默认)
// 3、CALLABLE:执行存储过程————CallableStatement
StatementType statementType = ms.getStatementType();
// 这里只处理 预编译的【查询】语句
if (sqlCommandType == SqlCommandType.SELECT
&& statementType == StatementType.PREPARED) {
// 匹配模糊查询 并 转义
escapeParameterIfContainingLike(boundSql, invocation);
// 执行更改后的SQL
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
return invocation.proceed();
}
private void escapeParameterIfContainingLike(BoundSql boundSql, Invocation invocation) {
if (boundSql == null) {
return;
}
// 获取动态sql
String prepareSql = boundSql.getSql();
// 找到 like 后面的参数 所在的索引位置
List<Integer> position = findLikeParam(prepareSql);
if (position == null || position.size() == 0) {
return;
}
log.info("#### position ={}", position);
// parameterMappings 中有 映射的字段名,且有序
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
log.info("### parameterMappings = {}", parameterMappings);
Map<String, Object> map;
if (invocation.getArgs()[1] instanceof Map) {
// 参数转为map方便获取 --- 有字段名 也有字段值
map = (Map) invocation.getArgs()[1];
// 这里注释掉是因为一些情况下json序列化有问题,会报错
// log.info("### map = {}", JSON.toJSONString(map));
// 把原有的参数先设置到动态sql的参数中,再设置需要修改的参数
map.forEach(boundSql::setAdditionalParameter);
position.forEach((p) -> {
ParameterMapping pm = parameterMappings.get(p);
// 参数名
String property = pm.getProperty();
// 参数值
String oldValue, newValue;
// mybatis-plus 的 QueryWrapper 方式 与原生的 Mybatis 拿到的参数不同,分别处理(Mybatis-plus只能处理like,左右模糊目前无法处理)
if (map.containsKey("ew")) {
// mybatis-plus 的 QueryWrapper
if (map.get("ew") instanceof LambdaQueryWrapper) {
LambdaQueryWrapper wrapper = (LambdaQueryWrapper) map.get("ew");
oldValue = wrapper.getParamNameValuePairs().get(property.replaceFirst("ew.paramNameValuePairs.", "")).toString();
} else if (map.get("ew") instanceof QueryWrapper) {
QueryWrapper wrapper = (QueryWrapper) map.get("ew");
oldValue = wrapper.getParamNameValuePairs().get(property.replaceFirst("ew.paramNameValuePairs.", "")).toString();
} else {
log.error("#### 既不是LambdaQueryWrapper,也不是QueryWrapper");
return;
}
if (oldValue.startsWith("%")) {
// 全模糊处理 + 部分左模糊处理(若用户传入xxx%会按全模糊处理---导致查出更多数据)
newValue = oldValue.endsWith("%") ? "%" + escapeLike(oldValue.substring(1, oldValue.length() - 1)) + "%" : "%" + escapeLike(oldValue.substring(1));
} else {
// 部分右模糊处理(若用户传入%xxx会按全模糊处理---导致查出更多数据)
newValue = escapeLike(oldValue.substring(0, oldValue.length() - 1)) + "%";
}
log.info("## oldValue = {},newValue = {}", oldValue, newValue);
} else {
// 原生的 Mybatis
oldValue = (String) map.get(property);
newValue = escapeLike(oldValue);
log.info("## oldValue = {},newValue = {}", oldValue, newValue);
}
// 设置到动态sql
boundSql.setAdditionalParameter(property, newValue);
});
} else {
// 处理只有一个入参且没有加@Param注解绑定的情形
ParameterMapping pm = parameterMappings.get(0);
// 参数名
String property = pm.getProperty();
String oldValue, newValue;
// 改
oldValue = invocation.getArgs()[1].toString();
newValue = escapeLike(oldValue);
// 设置到动态sql
boundSql.setAdditionalParameter(property, newValue);
}
}
String escapeLike(String value) {
if (value != null) {
for (String symbol : symbols) {
value = value.replace(symbol, ESCAPE_SYMBOL + symbol);
}
return value;
}
return null;
}
List<Integer> findLikeParam(String prepareSql) {
// 正则匹配
Matcher matcher = Pattern.compile(REGEX_LIKE, Pattern.CASE_INSENSITIVE).matcher(prepareSql);
int pos = 0;
// 返回索引集合
List<Integer> indexes = new ArrayList<>();
while (matcher.find(pos)) {
int start = matcher.start();
int index = StringUtils.countMatches(prepareSql.substring(0, start), "?");
indexes.add(index);
pos = matcher.end();
}
return indexes;
}
}
配置类
@Configuration
public class MyBatisConfig {
@Bean
public MybatisMetaInterceptor mybatisInterceptor() {
return new MybatisMetaInterceptor();
}
}
mybatis缓存
mybatis和缓存相关的类都在cache包里面,其中有一个Cache接口,且只有一个实现类PerpetualCache,它是用HashMap实现缓存功能的。
org.apache.ibatis.cache
- decorators (装饰器 --- 主要用于配置二级缓存)
- BlockingCache (阻塞缓存 blocking=true开启)
- FifoCache (FIFO淘汰策略的缓存 eviction=“FIFO”开启)
- LoggingCache (带日志功能的缓存 比如输出缓存命中率)
- LruCache (LRU淘汰策略的缓存 eviction=“LRU” (默认))
- ScheduledCache (定时调度的缓存 flushInterval=?)
- SerializedCache (支持序列化的缓存)
- SoftCache (基于JVM的软引用缓存)
- SynchronizedCache (同步缓存 基于Synchronized关键字实现,解决并发问题)
- TransactionCache (事务缓存)
- WeakCache (基于JVM的弱引用缓存)
- impl
- PerpetualCache (基本缓存 - Cache的唯一实现类)
- Cache (接口)
- CacheException (异常处理类)
- CacheKey (缓存的key)
- NullCackeKey (已弃用)
- TransactionalCacheManager (TCM事务管理器--用于二级缓存)
除此之外,还有很多的装饰器,通过这些装饰器可以实现很多额外的功能:回收策略、日志记录、定时刷新等等
一级缓存
又叫本地缓存,默认开启,无需配置
同一个sqlSession中执行的相同的sql,第一次查会走数据库并写入缓存,第二次直接从一级缓存获取
作用域:sqlSession级别
失效
- 执行了增删改
- 执行了commit
- 执行了close
不足:不同会话的缓存情况不共享,可能导致脏读,如会话1更新了数据清除了缓存,但会话2感知不知道,还在以前的缓存中拿数据,这种情况在分布式环境中是很严重的问题
源码分析
可以看出sqlSession中维护的两个重要属性,如下,Configuration和Executor,Configuration维护的是全局配置,缓存不应该在这里,我们在Executor中找
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
}
可以发现在Executor中有一级缓存PerpetualCache localCache,如下
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
}
在查询时,先查一级缓存,没有再查数据库,如下
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 生成一个缓存的key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 先查缓存,再查数据库queryFromDatabase
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
更新操作,一定清空缓存,没有任何条件,如下
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 直接清空缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
二级缓存
作用域:sqlSessionFactory级别,即namespace,多个sqlSession共享
开启二级缓存需要pojo实现Serializable接口
每当存取数据的时候,都有检测一下cache的生命时间,默认是1小时,如果这个cache存活了一个小时,那么将整个清空一下
当 Mybatis 调用 Dao 层查询数据库时,先查询二级缓存,二级缓存中无对应数据,再去查询一级缓存,一级缓存中也没有,最后去数据库查找
使用场景
- 由于增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义
- 如果多个namespace中有针对于同一个表的操作,比如Blog 表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper里面只操作单表的情况使用
源码解析
还是思考在哪个对象上维护,要想sqlSession间共享,肯定在sqlSession的外层,BaseExecutor是维护不了的,mybatis维护了一个装饰器 CachingExecutor,也实现了 Executor
先回顾下开启二级缓存的步骤
mybatis-config.xml
<!-- 控制全局缓存(二级缓存)-->
<setting name="cacheEnabled" value="true"/>
mapper.xml
<!-- 声明这个namespace使用二级缓存 -->
<cache/>
<!-- <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>-->
type 缓存实现类 需要实现Cache接口,默认是PerpetualCache
size 最多缓存对象个数 默认是1024
eviction 回收策略 LRU - 最近最少使用的,FIFO - 先进先出,SOFT - 软引用,WEAK - 弱引用
flushInterval 过期时间 自动刷新时间,单位ms,未配置时只有调用时刷新
readOnly 是否只读
- true: 只读缓存;会给所有对象的调用者返回对象的相同实例,因此这些对象不能被修改。这提供了很中要的性能优势
- false: 读写缓存,会返回缓存对象的copy(通过序列化),不会共享,性能会慢一些,但是安全,因此默认为false,此 时缓存的对象必须实现序列化接口
blocking 可重入锁实现缓存的并发控制 true: 会使用BlockingCache对Cache进行装饰。默认为false
Mapper.xml配置了之后,select()会被缓存,update()、insert()、delete()会刷新缓存。
只要配置了cacheEnabled=true,基本执行器就会被装饰,mapper中有没有配置,决定了在启动的时候会不会创建这个mapper的cache对象,最终会影响到CachingExecutor中query方法中的判断:也就是说,会被装饰,但没走二级缓存
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 找xml中的cache配置
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 去二级缓存拿
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
对于单个实时性要求高的sql关闭二级缓存
<select id="selectBlog" resultMap="BaseResultMap" useCache="false">
跨namespace
cache-ref 代表引用别的命名空间的Cache 配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。 在这种情况下,多个Mapper的操作都会引起缓存刷新,缓存的意义已经不大了。
<cache-ref namespace="com.gupaoedu.crud.dao.DepartmentMapper" />
只有事务提交后,缓存才生效,二级缓存通过 TransactionalCacheManager(TCM)来管理,
public class CachingExecutor implements Executor {
private final Executor delegate;
// 二级缓存通过tcm统一管理
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
}
最后又调用了TransactionalCache的getObject()、 putObject和commit()方法
TransactionalCache里面又持有了真正的Cache对象,比如是经过层层装饰的PerpetualCache。
在 putObject 的时候,只是添加到了entriesToAddOnCommit 里面,只有它的commit()方法被调用的时候才会调用 flushPendingEntries()真正写入缓存。它就是在DefaultSqlSession调用commit()的时候被调用的
public class TransactionalCacheManager {
// 真正的缓存
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
// query中通过tcm调用putObject,实际调用的是TransactionalCache的putObject
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
在 CachingExecutor 的 update()方法里面会调用 flushCacheIfRequired(ms),isFlushCacheRequired 就是从标签里面渠道的 flushCache 的值。而增删改操作的flushCache属性默认为true
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
自定义Redis二级缓存
实现Cache,重写getId、putObject、getObject、removeObject、clear方法即可
public class MyBatisRedisCacheImpl implements Cache {
private static final Logger log = LoggerFactory.getLogger(MyBatisRedisCacheImpl.class);
private static final String MY_BATIS_Redis_CACHE = "MyBatisRedisCache";
private final String id;
private int cacheSeconds = 600;
private static RedisCache redisCache; // 可以直接用RedisTemplate
public static RedisCache getRedisCache() {
if (redisCache == null) {
redisCache = (RedisCache)SpringContextUtil.getBean("cache");
}
return redisCache;
}
public MyBatisRedisCacheImpl(String id) {
log.info("####################### MyBatisRedisCacheImpl constructor init id : {}", id);
this.id = id;
}
public String getId() {
return this.id;
}
public String getCacheId() {
return "MyBatisRedisCache_" + this.getId();
}
public void putObject(Object key, Object value) {
String fieldMd5 = DigestUtils.md5Hex(key.toString());
log.info("####################### put cache Object keyMd5 : {} valueType : {}", fieldMd5, value.getClass());
String cacheId = this.getCacheId();
try {
boolean isExists = getRedisCache().exists(cacheId);
log.info("putObject isExists : {} ", isExists);
getRedisCache().hsetObj(cacheId, fieldMd5, value);
if (!isExists) {
getRedisCache().expire(cacheId, this.cacheSeconds);
}
} catch (Exception var6) {
var6.printStackTrace();
}
}
public Object getObject(Object key) {
String fieldMd5 = DigestUtils.md5Hex(key.toString());
log.info("####################### get cache Object key : {} ", fieldMd5);
String cacheId = this.getCacheId();
try {
boolean isExists = getRedisCache().exists(cacheId);
if (isExists) {
long ttl = getRedisCache().ttl(cacheId);
Object tmpObj = getRedisCache().hgetObj(cacheId, fieldMd5);
if (ttl < 0L) {
getRedisCache().expire(cacheId, this.cacheSeconds);
log.info("getObject isExists : {} ttl : {} ", isExists, ttl);
}
return tmpObj;
}
} catch (Exception var8) {
var8.printStackTrace();
}
return null;
}
public Object removeObject(Object key) {
String keyMd5 = DigestUtils.md5Hex(key.toString());
String cacheId = this.getCacheId();
log.info("####################### removeObject namespace : {} get key : {} ", cacheId, keyMd5);
try {
getRedisCache().del(cacheId);
} catch (Exception var5) {
var5.printStackTrace();
}
return null;
}
public void clear() {
String cacheId = this.getCacheId();
log.info("####################### clear all namespace : {} cache Object ", cacheId);
try {
getRedisCache().del(cacheId);
} catch (Exception var3) {
var3.printStackTrace();
}
}
public int getSize() {
return 0;
}
public ReadWriteLock getReadWriteLock() {
log.info(" getReadWriteLock ");
return null;
}
}
<!-- 开启基于redis的二级缓存
<cache type="com.jd.stob.unionsaas.cache.impl.MyBatisJimdbCacheImpl"/>-->
