MyBatis⭐️
1. MyBatis 简介
1.1 MyBatis 历史
MyBatis 前身为ibatis
,提供包括SQL Maps
和 Data Access Objects (DAO)
1.2 MyBatis 特性
- MyBatis 是支持定制化SQL,存储过程以及高级映射的优秀的持久层框架
- MyBatis避免了几乎所有的
JDBC
代码和手动设置参数以及获取结果集 - MyBatis可以使用简单的xml或注解用于配置和原始映射,将接口和java的
POJO(Plain Old java Objects, 普通的java对象)
映射成数据库中的记录 - MyBatis 是一个半自动的
ORM(Object Relation Mapping)
框架
1.3 MyBatis 和其他持久化层技术的对比
-
JDBC
-
SQL夹杂在java代码中,造成耦合度太高,导致硬编码内伤
-
维护不易且实际开发需求中SQL有变化,频繁修改的情况多见
-
代码冗长,开发效率低
-
-
Hibernate和JPA(没用过)
- 操作简便,开发效率高
- 程序中的长难复杂SQL需要绕过框架
- 内部自动生产的SQL,不容易做特殊优化
- 基于全映射的全自动框架,大量字段的POJO进行部分映射比较困难
- 反射操作太多,导致数据库性能下降
-
MyBatis
- 轻量级,性能出色
- SQL和java编码分开,功能边界清晰。java代码专注业务、SQL语句专注数据
- 开发效率逊色于Hibernate,但完全能够接收
2. 搭建mybatis
2.1 创建Maven工程
- 设置打包方式为
jar
- 引入依赖
2.2 创建mybatis核心配置文件
从mybatis.pdf
官方文档中直接可以找到
<?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文件 以后就能使用el表达式来配置数据源信息-->
<properties resource="jdbc.properties"/>
<!--
typeAliases: 为一个具体的类型设置一个别名,在mybatis的范围中就可以使用这个别名
-->
<typeAliases>
<!--
type: 别名类型
alias: 设置某个类型的别名
如果不设置alias,那么就会有一个默认的别名,就是类名,并且该别名不区分大小写
-->
<!-- <typeAlias type="com.cczj.mybatis.pojo.User" alias="User"/>-->
<!-- <typeAlias type="com.cczj.mybatis.pojo.User"/> -->
<!-- 以包名设置别名,该包下的所有类都有他们默认的别名-->
<package name="com.cczj.mybatis.pojo"/>
</typeAliases>
<!-- 配置连接数据库的环境 default: 默认环境选择-->
<environments default="development">
<!--
environment: 设置一个具体连接数据库的属性
id: 设置环境的唯一标识,不能重复
-->
<environment id="development">
<!--
transactionManager: 事务管理器
type: 事务管理方式
type="JDBC/MANAGED"
JDBC: 使用JDBC中原生的事务管理方式 -> 可以自动提交事务/回滚,也可以手动提交事务/回滚
MANAGED: 被管理,如 Spring
-->
<transactionManager type="JDBC"/>
<!--
dataSource: 设置数据源
type: 设置数据源的类型
type="POOLED/UNPOOLED/JNDI"
POOLED: 表示数据库连接池
UNPOOLED: 表示不使用数据库连接池
JNDI: 表示使用上下文中的数据源
-->
<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>
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<!-- 引入mappers配置文件 -->
<mappers>
<!-- <mapper resource="mappers/UserMapper.xml"/>-->
<!--
以包的形式引入mapper映射文件,但必须满足两个条件
1. mapper接口和映射文件所在的包名必须一致
2. mapper接口的名字和文件的名字必须一致
-->
<package name="com.cczj.mybatis.mapper"/>
</mappers>
</configuration>
注意:
The content of element type “configuration” must match “(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)”.
配置
tag
要按照要求的顺序
2.3 创建mapper接口
该mapper接口类似于DAO (Data Access Object)
数据访问对象
2.4 创建mapper接口的映射文件
相关概念:ORM (Object Relationship Mapping)
对象映射关系
- 对象:java的实体类对象
- 关系:关系型数据库
- 映射:二者之间对应的关系
java概念 | 数据库概念 |
---|---|
类 | 表 |
属性 | 字段/列 |
对象 | 记录/行 |
<?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.cczj.mybatis.mapper.UserMapper">
<!-- int insertUser(); -->
<insert id="insertUser">
insert into ssm.t_user
values (1, 'admin', '123456', 18, '男', '123456@qq.com');
</insert>
</mapper>
2.5 log4j.xml配置文件
日志的级别
FATAL(致命) -> ERROR(错误) -> WARN(警告) -> INFO(信息) -> DEBUG(调试)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
<appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
<param name="Encoding" value="UTF-8"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS}
%m (%F:%L) \n"/>
</layout>
</appender>
<logger name="java.sql">
<level value="debug"/>
</logger>
<logger name="org.apache.ibatis">
<level value="info"/>
</logger>
<root>
<level value="debug"/>
<appender-ref ref="STDOUT"/>
</root>
</log4j:configuration>
3. MyBatis获取参数值的两种方式
3.1 Mybatis 中获取参数方式的介绍
${}
本质是字符串拼接、#{}
本质是占位符赋值- 使用
${}
拼接时,若为字符串类型或日期类型的字段进行赋值时,需要手动加单引号 - 使用
#{}
赋值时,若为字符串类型或日期类型的字段进行赋值时,可以自动加单引号
3.2 传入单个参数的情况
- 在接口中定义的方法,如果是单个参数,最好加入
@param("指定的变量名")
作为指定名 ${}
中可以使用任意变量名,但是需要加上''
作为字符串拼接来使用。#{}
中可以使用任意的变量名,因为在底层实现过程中,此标记仅仅作为 ? 这个占位符来使用,而传进来的具体参数值作为一个字面量是不会改变的。
3.3 传入多个参数的情况
- 在Mybatis内部,会将多个参数封装进一个集合中,这时的自变量名只能使用系统定义的参数名
- 集合中的参数调用名有两种:
arg0、arg1、arg2...
和param1、param2、...
- 自定义Map集合:将参数值手动放入Map中,并将map作为参数传入接口中。
- 使用自定义Map集合时、参数调用名就是自己设定的
Key
Mybatis 映射中传入参数时,为以下结果自定义了别名
右侧为Map类型,左侧为类型别名:
3.4 解决模糊查询的三个方法
由于普通的#{username}
在引号''
中会失去占位符的效果
select * from table where username like '%${usename}%'
select * from table where username like "%"#{username}"%"
select * from table where username like concat('%', #{username}, '%')
3.5 获取自增的主键
增删改的操作下返回值都是一个int的该变量,所以我们要手动设置一个主键KEY
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id"></insert>
useGeneratedKeys
: 表示当前添加功能使用了自增的主键
keyProperty
: 将添加的数据的自增主键作为实体类对象参数的主键名
4. ResultMap
4.1 如果pojo
包中属性名和数据库表中字段名不同的解决办法
-
由JDBC可知,查询结果会通过getter、setter的方法呈现。如果名称不一致,那么无法进行存储值。因此解决办法就是为SQL语句结果设置别名。将查询语句结果别名设置为成员属性名。
-
在Mybatis的核心配置文件中设置一个全局变量,可以自动将下划线映射为驼峰
-
<setting> mapUnderscoreToCamelCase 将 蛇形命名法 映射为 小驼峰命名法 <setting name="mapUnderscoreToCamelCase" value="true"/> </setting>
-
4.2 自定义映射关系
<!--
resultMap: 自定义映射关系 id: 自定义映射关系名称 type: pojo包中的类名
id: 关于主键的唯一标识
result: 处理普通字段和实体类属性的映射关系
column: 映射关系中的字段名称 (数据库中字段名称)
property: 设置映射关系查询结果的属性名 (实体类中成员属性名称)
association: 处理多对一的映射关系 (处理实体类类型的属性)
-->
<resultMap id="empResultMap" type="Emp">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="emp_gender" property="empGender"/>
<result column="emp_age" property="empAge"/>
</resultMap>
4.3 多对一的处理方式
因为成员属性的类型可能是一个类,那么要将多张表查询到一张表中,就需要用到以下处理方式
-
级联处理方式:
-
<resultMap id="empResultMap" type="Emp"> <id column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="emp_gender" property="empGender"/> <result column="emp_age" property="empAge"/> <!--注意以下两个result--> <result column="dept_id" property="dept.deptId"/> <result column="dept_name" property="dept.deptName"/> </resultMap> <select id="getEmpAndDeptByEmpId" resultMap="empResultMap"> select t_dept.*, t_emp.* from t_emp left join t_dept on t_emp.dept_id = t_dept.dept_id where t_emp.emp_id = #{empId}; </select>
-
-
association:处理多对一的映射关系 (处理实体类类型的属性)
-
<resultMap id="empResultMap" type="Emp"> <id column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="emp_gender" property="empGender"/> <result column="emp_age" property="empAge"/> <!-- assciation: 处理多对一的映射关系 (处理实体类类型的属性) property: 设置需要处理映射关系的属性的属性名 javaType: 设置需要处理映射关系的属性的类型 --> <association property="dept" javaType="Dept"> <id column="dept_id" property="deptId"/> <result column="dept_name" property="deptName"/> </association> </resultMap> <select id="getEmpAndDeptByEmpId" resultMap="empResultMap"> select t_dept.*, t_emp.* from t_emp left join t_dept on t_emp.dept_id = t_dept.dept_id where t_emp.emp_id = #{empId}; </select>
-
-
分步查询方式:
-
通过两个不同的接口,调用不同的方法。即用子表的结果查询父表。
-
<resultMap id="empResultMap" type="Emp"> <id column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="emp_gender" property="empGender"/> <result column="emp_age" property="empAge"/> <!-- property: 查询哪个成员属性 select: 下个查询方法名 column: 将子表查询结果中的哪个字段作为下一个表进行查询的条件 --> <association property="dept" select="com.cczj.mybatis.mapper.DeptMapper.方法名" column="dept_id"> </association> </resultMap> <select id="getEmpAndDeptByEmpId" resultMap="empResultMap"> select * from t_emp where t_emp.emp_id = #{empId} </select>
-
<resultMap id="empResultMap" type="Emp"> <id column="dept_id" property="deptId"/> <result column="dept_name" property="deptName"/> </resultMap> <select id="getEmpAndDeptByEmpId" resultMap="empResultMap"> select * from t_dept where t_dept.dept_id = #{deptId} </select>
-
-
分布查询有什么优势呢?
-
可以实现延迟加载,但是必须在核心配置文件中进行全局配置
-
<settings> <!-- lazyLoadingEnabled: 是否开启延迟加载 默认为false aggressiveLazyLoading: 是否开启按需加载 默认为false 按需加载开启后,只会进行分步执行的第一步结果 --> <settins name="lazyLoadingEnabled" value="true"/> <settins name="aggressiveLazyLoading" value="false"/> </settings>
-
4.4. 一对多的处理方式
-
collection: 处理一对多和多对多的方式
-
<resultMap id="empResultMap" type="Emp"> <id column="dept_id" property="deptId"/> <result column="dept_name" property="deptName"/> <!-- property: 设置集合的名称 ofType: 该集合类的类型 --> <collection property="emps" ofType="Emp"> <id column="emp_id" property="empId"/> <result column="emp_name" property="empName"/> <result column="emp_gender" property="empGender"/> <result column="emp_age" property="empAge"/> </collection> </resultMap> <select id="getEmpAndDeptByEmpId" resultMap="empResultMap"> select t_emp.* , t_dept.* from t_dept left join t_emp on t_emp.dept_id = t_dept.dept_id where t_emp.emp_id = #{empId}; </select>
-
-
分布查询方法和多对一基本一致
5. 动态SQL
5.1 If where
- 动态拼接SQL语句的第一种写法:
<select id="方法名" resultType="结果类型">
select * from t_emp where 1=1
<if test="empName != null and empName != ''">
and empName = #{empName}
</if>
<if test="empAge != null and empAge != ''">
and empAge = #{empAge}
</if>
</select>
- 动态拼接SQL语句的第二种写法:
<select id="方法名" resultType="结果类型">
select * from t_emp
<where>
<if test="empName != null and empName != ''">
empName = #{empName} and
</if>
<if test="empAge != null and empAge != ''">
empAge = #{empAge}
</if>
</where>
</select>
5.2 trim
trim:
prefix、suffix: 在标签中内容前面或者后面添加指定内容
prefixOverrides、suffixOverrides: 在标签内容前面或者后面去掉指定内容
<select id="方法名" resultType="结果类型">
select * from t_emp
<trim prefix="where" suffixOverrides="and">
<if test="empName != null and empName != ''">
empName = #{empName}
</if>
<if test="empAge != null and empAge != ''">
empAge = #{empAge}
</if>
</trim>
</select>
5.3 choose、when、otherwise
if、if else 和 else
<select id="方法名" resultType="结果类型">
select * from t_emp
<where>
<choose>
<when test="empName != null and empName != ''">
empName = #{empName}
</when>
<when test="empAge != null and empAge != ''">
empAge = #{empAge}
</when>
<otherwise>
类似于else ... 也可以看作default
</otherwise>
</choose>
</where>
</select>
5.4 foreach
collection: 表示当前传入集合的参数名 (建议通过@param进行手动设置)
item: 表示当前循环的参数名 (类似于foreach中的每一次循环的结果)
separate: 循环分隔符
open: 当前循环所有的内容以什么开始 close:循环的内容以什么结束
index: 当前索引的下标是多少
<insert id="insertMoreEmp">
insert into t_emp values
<foreach collection="emps" item="emp" separator=",">
(null, #{emp.empName}, #{emp.empAge})
</foreach>
</insert>
<delete id="deleteMoreEmp">
delete from t_emp where emp_id in
(
<foreach collection="empIds" item="empId" separator=",">
#{empId}
</foreach>
)
delete from t_emp where emp_id in
<foreach collection="empIds" item="empId" separator="or">
emp_id = #{empId}
</foreach>
</delete>
5.5 SQL片段
将部分sql语句通过sql片段存起来,用到的时候通过include标签进行调用
<sql id="empColumns">
emp_id,emp_name,emp_age,emp_gender
</sql>
<select>
select <include refid="empColumns"></include> from t_emp
</select>
6. Mybatis Cache
6.1 Mybatis 的一级缓存
一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库中重新访问。
一级缓存失效的四种情况:
- 不同的SqlSession对应不同的一级缓存
- 同一个SqlSession但是查询条件不同
- 同一个SqlSession两次查询期间执行了任何一次增删改操作(即使更新的不是缓存数据)
- 同一个SqlSession两次查询期间手动清空了缓存
sqlSession.clearCache();
该方法用于清除一级缓存
6.2 Mybatis 的二级缓存
二级缓存是SqlSessionFactory级别的,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后如果再次执行相同的查询语句,结果就会从缓存中获取
二次缓存的开启条件:
- 在核心配置文件中,设置全局配置属性
cacheEnabled="true"
默认为true,不需要设置 - 在映射文件中设置标签
cache
(仅仅添加一个<cache/>
即可) - 二级缓存必须在
SqlSession
关闭或提交之后有效 - 查询的数据所转换的实体类类型必须实现序列化的接口
使二级缓存失效的情况:
两次查询过程中执行了任意增删改的操作,会使一级和二级缓存同时失效
6.3 Mybatis 二级缓存中的配置信息
eviction: 缓存回收策略,有这几种回收策略
LRU
- 最近最少回收,移除最长时间不被使用的对象FIFO
- 先进先出,按照缓存进入的顺序来移除它们SOFT
- 软引用,移除基于垃圾回收器状态和软引用规则的对象WEAK
- 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
默认是 LRU 最近最少回收策略
flushinterval
缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值readOnly
: 是否只读;true 只读,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改size
: 缓存存放多少个元素type
: 指定自定义缓存的全类名(实现Cache 接口即可)blocking
: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
6.4 Mybatis 缓存查询的顺序
先查询二级缓存,因为二级缓存中可能会有其他程序已经查询出来的结果,可以直接拿来使用
如果二级缓存也没有命中,那么再查询一级缓存 (当一级缓存没有结束时的情况下会发生该情况)
如果一级缓存也没有命中,那么再查询数据库
SqlSession 关闭后,一级缓存中的数据会写入二级缓存