JDBC(Java Data Base Connection,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成.JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序
jdbc核心api
DBUtils是Java编程中的数据库操作实用工具,小巧简单实用。
DBUtils封装了对JDBC的操作,简化了JDBC操作,可以少写代码。
DBUtils三个核心功能介绍
ORM 对象关系映射
Hibernate劣势
JdbcTemplate针对数据查询提供了多个重载的模板方法,你可以根据需要选用不同的模板方法.如果你的查询很简单,仅仅是传入相应SQL或者相关参数,然后取得一个单一的结果,那么你可以选择如下一组便利的模板方法。
MyBatis 是一款优秀的持久层框架/半自动的ORM,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
优点:
1、与JDBC相比,减少了50%的代码量
2、 最简单的持久化框架,简单易学
3、SQL代码从程序代码中彻底分离出来,可以重用
4、提供XML标签,支持编写动态SQL
5、提供映射标签,支持对象与数据库的ORM字段关系映射
6、支持缓存、连接池、数据库移植…
缺点:
1、SQL语句编写工作量大,熟练度要高
2、数据库移植性比较差,如果需要切换数据库的话,SQL语句会有很大的差异
导入pom
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.30version>
dependency>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
dependency>
添加logback配置文件
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{100} - %msg%npattern>
encoder>
appender>
<logger name="cn.tulingxueyuan.mapper" level="debug">logger>
org.apache.ibatis.transaction
<root level="error">
<appender-ref ref="STDOUT" />
root>
configuration>
Logger LOGGER= LoggerFactory.getLogger(this.getClass());
/**
* 日志级别
* TRACE < DEBUG < INFO < WARN < ERROR。
* 1 2 3 4 5
*/
@Test
public void test02(){
LOGGER.trace("跟踪级别");
LOGGER.debug("调试级别");
LOGGER.info("信息级别");
LOGGER.warn("警告级别");
LOGGER.error("异常级别");
}
在mybatis的项目中,我们发现了有一个mybatis-config.xml的配置文件,这个配置文件是mybatis的全局配置文件,用来进行相关的全局配置,在任何操作下都生效的配置。下面我们要针对其中的属性做详细的解释,方便大家在后续使用的时候更加熟练。
官方说明:
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
mybatis-config.xml
DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties">properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
settings>
<typeAliases>
<package name="cn.tulingxueyuan.bean"/>
typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driverClassname}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
dataSource>
environment>
environments>
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="SQL Server" value="sqlserver"/>
<property name="Oracle" value="orcl"/>
databaseIdProvider>
<mappers>
<package name="cn.tulingxueyuan.dao"/>
mappers>
configuration>
MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。
SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
在每个顶级元素标签中可以添加很多个属性,下面我们开始详细了解下具体的配置。
insert、update、delete元素
属性 | 描述 |
---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
parameterType | 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。 |
parameterMap | 用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。 |
flushCache | 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。 |
statementType | 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
useGeneratedKeys | (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。 |
keyProperty | (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
keyColumn | (仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。 |
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into user(user_name) values(#{userName})
insert>
<insert id="insertUser2" >
<selectKey order="BEFORE" keyProperty="id" resultType="integer">
select max(id)+1 from user
selectKey>
insert into user(id,user_name) values(#{id},#{userName})
insert>
更多详细内容见下边的第三章节
在xml文件中编写sql语句的时候有两种取值的方式,分别是#{}和${},下面来看一下他们之间的区别:
<select id="SelectEmp" resultType="Emp" resultMap="emp_map" >
SELECT id,user_name,create_date FROM EMP where id=#{id}
select>
EmpDao.xml
<select id="selectAllEmp" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp
select>
<select id="selectEmpByEmpReturnMap" resultType="map">
select * from emp where empno = #{empno}
select>
<select id="getAllEmpReturnMap" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp
select>
<resultMap id="emp_map" type="emp" autoMapping="false" extends="common_map">
<result column="create_date" property="cjsj">result>
resultMap>
<resultMap id="common_map" type="emp" autoMapping="false" >
<id column="id" property="id">id>
<result column="user_name" property="username">result>
resultMap>
<select id="SelectEmp" resultType="Emp" resultMap="emp_map" >
SELECT id,user_name,create_date FROM EMP where id=#{id}
select>
emp.java
import java.time.LocalDate;
public class Emp {
private Integer id;
private String username;
private LocalDate createDate;
private deptId deptId;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public LocalDate getCreateDate() {
return createDate;
}
public void setCreateDate(LocalDate createDate) {
this.createDate = createDate;
}
public Integer getDeptId() {
return dept;
}
public void setDeptId(Integer deptId) {
this.dept = dept;
}
@Override
public String toString() {
return "Emp{" +
"id=" + id +
", username='" + username + '\'' +
", createDate=" + createDate +
", deptId=" + deptId+
'}';
}
}
EmpMapper.xml
<resultMap id="QueryEmp_Map" type="QueryEmpDTO">
<id column="e_id" property="id">id>
<result column="user_name" property="username">result>
<result column="d_id" property="deptId">result>
<result column="dept_name" property="deptName">result>
resultMap>
<select id="QueryEmp" resultMap="QueryEmp_Map">
select t1.id as e_id,t1.user_name,t2.id as d_id,t2.dept_name from emp t1
INNER JOIN dept t2 on t1.dept_id=t2.id
where t1.id=#{id}
select>
<resultMap id="QueryEmp_Map" type="map">
<id column="e_id" property="id">id>
<result column="user_name" property="username">result>
<result column="d_id" property="deptId">result>
<result column="dept_name" property="deptName">result>
resultMap>
<select id="QueryEmp" resultMap="QueryEmp_Map">
select t1.id as e_id,t1.user_name,t2.id as d_id,t2.dept_name from emp t1
INNER JOIN dept t2 on t1.dept_id=t2.id
where t1.id=#{id}
select>
QueryEmpDTO
public class QueryEmpDTO {
private String deptName;
private Integer deptId;
private Integer id;
private String username;
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public Integer getDeptId() {
return deptId;
}
public void setDeptId(Integer deptId) {
this.deptId = deptId;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "QueryEmpDTO{" +
"deptName='" + deptName + '\'' +
", deptId=" + deptId +
", id=" + id +
", username='" + username + '\'' +
'}';
}
}
Test
@Test
public void test01() {
try(SqlSession sqlSession = sqlSessionFactory.openSession()){
// Mybatis在getMapper就会给我们创建jdk动态代理
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
QueryEmpDTO dto = mapper.QueryEmp(4);
System.out.println(dto);
}
}
EmpMapper.xml
<resultMap id="QueryEmp_Map2" type="Emp">
<id column="e_id" property="id">id>
<result column="user_name" property="username">result>
<association property="dept">
<id column="d_id" property="id">id>
<id column="dept_name" property="deptName">id>
association>
resultMap>
<select id="QueryEmp2" resultMap="QueryEmp_Map2">
select t1.id as e_id,t1.user_name,t2.id as d_id,t2.dept_name from emp t1
INNER JOIN dept t2 on t1.dept_id=t2.id
where t1.id=#{id}
select>
<resultMap id="SelectDeptAndEmpsMap" type="Dept">
<id column="d_id" property="id">id>
<id column="dept_name" property="deptName">id>
<collection property="emps" ofType="Emp" >
<id column="e_id" property="id">id>
<result column="user_name" property="username">result>
<result column="create_date" property="createDate">result>
collection>
resultMap>
<select id="SelectDeptAndEmps" resultMap="SelectDeptAndEmpsMap">
select t1.id as d_id,t1.dept_name,t2.id e_id,t2.user_name,t2.create_date from dept t1
LEFT JOIN emp t2 on t1.id=t2.dept_id
where t1.id=#{id}
select>
Emp.java
import java.time.LocalDate;
public class Emp {
private Integer id;
private String username;
private LocalDate createDate;
private Dept dept;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public LocalDate getCreateDate() {
return createDate;
}
public void setCreateDate(LocalDate createDate) {
this.createDate = createDate;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
@Override
public String toString() {
return "Emp{" +
"id=" + id +
", username='" + username + '\'' +
", createDate=" + createDate +
", dept=" + dept +
'}';
}
}
Dept.java:
public class Dept {
private Integer id;
private String deptName;
private List<Emp> emps;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
@Override
public String toString() {
return "Dept{" +
"id=" + id +
", deptName='" + deptName + '\'' +
", emps=" + emps +
'}';
}
}
EmpMapper.java:
public interface EmpMapper {
/*徐庶老师实际开发中的实现方式*/
QueryEmpDTO QueryEmp(Integer id);
/*实用嵌套结果实现联合查询 多对一 */
Emp QueryEmp2(Integer id);
/*实用嵌套查询实现联合查询 多对一 */
Emp QueryEmp3(Integer id);
}
DeptMapper.java:
public interface DeptMapper {
//嵌套查询: 一对多 使用部门id查询员工
Dept SelectDeptAndEmps(Integer id);
// 嵌套查询(异步查询): 一对多 查询部门及所有员工
Dept SelectDeptAndEmps2(Integer id);
}
在上述逻辑的查询中,是由我们自己来完成sql语句的关联查询的,那么,我们能让mybatis帮我们实现自动的关联查询吗?
EmpMapper.xml:
<resultMap id="QueryEmp_Map3" type="Emp">
<id column="id" property="id">id>
<result column="user_name" property="username">result>
<association property="dept" column="dept_id" select="cn.tulingxueyuan.mapper.DeptMapper.SelectDept">
association>
resultMap>
<select id="QueryEmp3" resultMap="QueryEmp_Map3">
select * from emp where id=#{id}
select>
DeptMapper.xml
<select id="SelectDept" resultType="dept">
SELECT * FROM dept where id=#{id}
select>
DeptMapper.xml
<resultMap id="SelectDeptAndEmpsMap2" type="Dept">
<id column="d_id" property="id">id>
<id column="dept_name" property="deptName">id>
<collection property="emps" ofType="Emp" column="id" select="cn.tulingxueyuan.mapper.EmpMapper.SelectEmpByDeptId" >
collection>
resultMap>
<select id="SelectDeptAndEmps2" resultMap="SelectDeptAndEmpsMap2">
SELECT * FROM dept where id=#{id}
select>
EmpMapper.xml
<select id="SelectEmpByDeptId" resultType="emp">
select * from emp where dept_id=#{id}
select>
Emp.java
public class Emp {
private Integer id;
private String username;
private LocalDate createDate;
private Dept dept;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public LocalDate getCreateDate() {
return createDate;
}
public void setCreateDate(LocalDate createDate) {
this.createDate = createDate;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
@Override
public String toString() {
return "Emp{" +
"id=" + id +
", username='" + username + '\'' +
", createDate=" + createDate +
", dept=" + dept +
'}';
}
}
Dept.java:
public class Dept {
private Integer id;
private String deptName;
private List<Emp> emps;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
@Override
public String toString() {
return "Dept{" +
"id=" + id +
", deptName='" + deptName + '\'' +
", emps=" + emps +
'}';
}
}
EmpMapper.java:
public interface EmpMapper {
/*徐庶老师实际开发中的实现方式*/
QueryEmpDTO QueryEmp(Integer id);
/*实用嵌套结果实现联合查询 多对一 */
Emp QueryEmp2(Integer id);
/*实用嵌套查询实现联合查询 多对一 */
Emp QueryEmp3(Integer id);
}
DeptMapper.java:
public interface DeptMapper {
//嵌套查询: 一对多 使用部门id查询员工
Dept SelectDeptAndEmps(Integer id);
// 嵌套查询(异步查询): 一对多 查询部门及所有员工
Dept SelectDeptAndEmps2(Integer id);
}
当我们在进行表关联的时候,有可能在查询结果的时候不需要关联对象的属性值(select count(1)…),那么此时可以通过延迟加载来实现功能。在全局配置文件中添加如下属性
mybatis-config.xml
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="lazyLoadTriggerMethods" value=""/>
如果设置了全局加载,但是希望在某一个sql语句查询的时候不使用延时策略,可以添加fetchType下属性:
<association property="dept" fetchType="eager" column="dept_id" select="cn.tulingxueyuan.mapper.DeptMapper.SelectDept">
association>
三种关联关系都有两种关联查询的方式: 嵌套查询,嵌套结果
Mybatis的延迟加载配置, 在全局配置文件中加入下面代码
<settings>
<setting name=”lazyLoadingEnabled” value=”true” />
<setting name=”aggressiveLazyLoading” value=”false”/>
settings>
在映射文件中,元素和元素中都已默认配置了延迟加载属性,即默认属性fetchType=”lazy”(属性fetchType=”eager”表示立即加载),所以在配置文件中开启延迟加载后,无需在映射文件中再做配置
一对一
使用元素进行一对一关联映射非常简单,只需要参考如下两种示例配置即可
一对多
元素中,包含了一个
子元素,MyBatis就是通过该元素来处理一对多关联关系的
子元素的属性大部分与
元素相同,但其还包含一个特殊属性–ofType
ofType属性与javaType属性对应,它用于指定实体对象中集合类属性所包含的元素类型。
元素的使用也非常简单,同样可以参考如下两种示例进行配置,具体代码如下:
多对多
多对多的关联关系查询,同样可以使用前面介绍的元素进行处理(其用法和一对多关联关系查询语句用法基本相同)
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
EmpDao.xml
<select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp where
<if test="empno!=null">
empno = #{empno} and
if>
<if test="ename!=null">
ename like #{ename} and
if>
<if test="sal!=null">
sal > #{sal}
if>
select>
上边代码看起来是比较正常的,但是大家需要注意的是如果我们传入的参数值有缺失会怎么呢?这个时候拼接的sql语句就会变得有问题,例如不传参数或者丢失最后一个参数,那么语句中就会多一个where或者and的关键字,因此在mybatis中也给出了具体的解决方案:
where
where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。
<select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp
<where>
<if test="empno!=null">
empno = #{empno}
if>
<if test="ename!=null">
and ename like #{ename}
if>
<if test="sal!=null">
and sal > #{sal}
if>
where>
select>
现在看起来没有什么问题了,但是我们的条件添加到了拼接sql语句的前后,那么我们该如何处理呢?
trim
<select id="getEmpByCondition" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp
<trim prefix="where" prefixOverrides="and" suffixOverrides="and">
<if test="empno!=null">
empno = #{empno} and
if>
<if test="ename!=null">
ename like #{ename} and
if>
<if test="sal!=null">
sal > #{sal} and
if>
trim>
select>
动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。
<select id="getEmpByDeptnos" resultType="Emp">
select * from emp where deptno in
<foreach collection="deptnos" close=")" index="idx" item="deptno" open="(" separator=",">
#{deptno}
foreach>
select>
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
<select id="getEmpByConditionChoose" resultType="cn.tulingxueyuan.bean.Emp">
select * from emp
<where>
<choose>
<when test="empno!=null">
empno > #{empno}
when>
<when test="ename!=null">
ename like #{ename}
when>
<when test="sal!=null">
sal > #{sal}
when>
<otherwise>
1=1
otherwise>
choose>
where>
select>
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。
<update id="updateEmpByEmpno">
update emp
<set>
<if test="empno!=null">
empno=#{empno},
if>
<if test="ename!=null">
ename = #{ename},
if>
<if test="sal!=null">
sal = #{sal}
if>
set>
<where>
empno = #{empno}
where>
update>
bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
select>
这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。比如:
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password sql>
这个 SQL 片段可以在其它语句中使用,例如:
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/>include>,
<include refid="userColumns"><property name="alias" value="t2"/>include>
from some_table t1
cross join some_table t2
select>
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
当添加上该标签之后,会有如下效果:
在进行配置的时候还会分为一级缓存和二级缓存:
一级缓存是sqlsession级别的缓存,默认是存在的。在下面的案例中,大家发现我发送了两个相同的请求,但是sql语句仅仅执行了一次,那么就意味着第一次查询的时候已经将结果进行了缓存。
@Test
public void test01() {
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
List<Emp> list = mapper.selectAllEmp();
for (Emp emp : list) {
System.out.println(emp);
}
System.out.println("--------------------------------");
List<Emp> list2 = mapper.selectAllEmp();
for (Emp emp : list2) {
System.out.println(emp);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
sqlSession.close();
}
}
在大部分的情况下一级缓存是可以的,但是有几种特殊的情况会造成一级缓存失效:
1、一级缓存是sqlSession级别的缓存,如果在应用程序中只有开启了多个sqlsession,那么会造成缓存失效
@Test
public void test02(){
SqlSession sqlSession = sqlSessionFactory.openSession();
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
List<Emp> list = mapper.selectAllEmp();
for (Emp emp : list) {
System.out.println(emp);
}
System.out.println("================================");
SqlSession sqlSession2 = sqlSessionFactory.openSession();
EmpDao mapper2 = sqlSession2.getMapper(EmpDao.class);
List<Emp> list2 = mapper2.selectAllEmp();
for (Emp emp : list2) {
System.out.println(emp);
}
sqlSession.close();
sqlSession2.close();
}
2、在编写查询的sql语句的时候,一定要注意传递的参数,如果参数不一致,那么也不会缓存结果
3、如果在发送过程中发生了数据的修改,那么结果就不会缓存
@Test
public void test03(){
SqlSession sqlSession = sqlSessionFactory.openSession();
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
Emp empByEmpno = mapper.findEmpByEmpno(1111);
System.out.println(empByEmpno);
System.out.println("================================");
empByEmpno.setEname("zhangsan");
int i = mapper.updateEmp(empByEmpno);
System.out.println(i);
System.out.println("================================");
Emp empByEmpno1 = mapper.findEmpByEmpno(1111);
System.out.println(empByEmpno1);
sqlSession.close();
}
4、在两次查询期间,手动去清空缓存,也会让缓存失效
@Test
public void test03(){
SqlSession sqlSession = sqlSessionFactory.openSession();
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
Emp empByEmpno = mapper.findEmpByEmpno(1111);
System.out.println(empByEmpno);
System.out.println("================================");
System.out.println("手动清空缓存");
sqlSession.clearCache();
System.out.println("================================");
Emp empByEmpno1 = mapper.findEmpByEmpno(1111);
System.out.println(empByEmpno1);
sqlSession.close();
}
特性
一级缓存特性:
一级缓存失效情况:
二级缓存是全局作用域缓存,默认是不开启的,需要手动进行配置。
Mybatis提供二级缓存的接口以及实现,缓存实现的时候要求实体类实现Serializable接口,二级缓存在sqlSession关闭或提交之后才会生效。
二级缓存的使用
步骤:
1、全局配置文件中添加如下配置:
<setting name="cacheEnabled" value="true"/>
2、需要在使用二级缓存的映射文件处使用标签标注
3、实体类必须要实现Serializable接口
@Test
public void test04(){
SqlSession sqlSession = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
EmpDao mapper2 = sqlSession2.getMapper(EmpDao.class);
Emp empByEmpno = mapper.findEmpByEmpno(1111);
System.out.println(empByEmpno);
sqlSession.close();
Emp empByEmpno1 = mapper2.findEmpByEmpno(1111);
System.out.println(empByEmpno1);
sqlSession2.close();
}
缓存的属性
//可以看到会去二级缓存中查找数据,而且二级缓存跟一级缓存中不会同时存在数据,因为二级缓存中的数据是在sqlsession 关闭之后才生效的
@Test
public void test05(){
SqlSession sqlSession = sqlSessionFactory.openSession();
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
Emp empByEmpno = mapper.findEmpByEmpno(1111);
System.out.println(empByEmpno);
sqlSession.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
EmpDao mapper2 = sqlSession2.getMapper(EmpDao.class);
Emp empByEmpno2 = mapper2.findEmpByEmpno(1111);
System.out.println(empByEmpno2);
Emp empByEmpno3 = mapper2.findEmpByEmpno(1111);
System.out.println(empByEmpno3);
sqlSession2.close();
}
缓存查询的顺序是先查询二级缓存再查询一级缓存
@Test
public void test05(){
SqlSession sqlSession = sqlSessionFactory.openSession();
EmpDao mapper = sqlSession.getMapper(EmpDao.class);
Emp empByEmpno = mapper.findEmpByEmpno(1111);
System.out.println(empByEmpno);
sqlSession.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
EmpDao mapper2 = sqlSession2.getMapper(EmpDao.class);
Emp empByEmpno2 = mapper2.findEmpByEmpno(1111);
System.out.println(empByEmpno2);
Emp empByEmpno3 = mapper2.findEmpByEmpno(1111);
System.out.println(empByEmpno3);
Emp empByEmpno4 = mapper2.findEmpByEmpno(7369);
System.out.println(empByEmpno4);
Emp empByEmpno5 = mapper2.findEmpByEmpno(7369);
System.out.println(empByEmpno5);
sqlSession2.close();
}
二级缓存的作用范围
如果设置了全局的二级缓存配置,那么在使用的时候需要注意,在每一个单独的select语句中,可以设置将查询缓存关闭,以完成特殊的设置
1、在setting中设置,是配置二级缓存开启,一级缓存默认一直开启
<setting name="cacheEnabled" value="true"/>
2、select标签的useCache属性:
在每一个select的查询中可以设置当前查询是否要使用二级缓存,只对二级缓存有效
3、sql标签的flushCache属性
增删改操作默认值为true,sql执行之后会清空一级缓存和二级缓存,而查询操作默认是false
4、sqlSession.clearCache()
只是用来清除一级缓存
二级缓存特性
: org.apache.ibatis.session.Configuration#caches key:mapper命名空间 value:erpetualCache.map
key==> sqlid+sql
实现:
失效:
<cache-ref namespace="cn.tulingxueyuan.mapper.DeptMapper"/>
整合redis
添加redis-mybatis 缓存适配器 依赖
<dependencies>
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-redisartifactId>
<version>1.0.0-beta2version>
dependency>
dependencies>
添加redis.properties在resources根目录
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password=无密码可不填
database=0
clientName=
设置mybatis二级缓存实现类
<cache
...
type="org.mybatis.caches.redis.RedisCache" ......
/>
整合ehcache
导入对应的maven依赖
<dependency>
<groupId>org.ehcachegroupId>
<artifactId>ehcacheartifactId>
<version>3.8.1version>
dependency>
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-ehcacheartifactId>
<version>1.2.0version>
dependency>
导入ehcache配置文件
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<diskStore path="D:\ehcache" />
<defaultCache
maxElementsInMemory="1"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
defaultCache>
ehcache>
在mapper文件中添加自定义缓存
<cache type="org.mybatis.caches.ehcache.EhcacheCache">cache>
MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能。需要注意的是,如果没有完全理解MyBatis 的运行原理和插件的工作方式,最好不要使用插件,因为它会改变系底层的工作逻辑,给系统带来很大的影响。
MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理SQL,处理结果。
Mybatis插件典型适用场景
分页功能
mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可;
公共字段统一赋值
一般业务系统都会有创建者,创建时间,修改者,修改时间四个字段,对于这四个字段的赋值,实际上可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可;
性能监控
对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间;
其它
其实mybatis扩展性还是很强的,基于插件机制,基本上可以控制SQL执行的各个阶段,如执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。
实现思考:
第一个问题:
不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?
答案:大家很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。
第二个问题:
我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休假申请,先是项目经理审批,然后是部门经理审批,再是HR 审批,再到总经理审批,怎么实现层层的拦截?
答案:插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。
在之前的源码中我们也发现了,mybatis内部对于插件的处理确实使用的代理模式,既然是代理模式,我们应该了解MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一个运行的节点都是可以被修改的。只有清楚了这些对象的方法的作用,当我们自己编写插件的时候才知道从哪里去拦截。在MyBatis 官网有答案,我们来看一下:https://mybatis.org/mybatis-3/zh/configuration.html#plugins
Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创建CachingExcecutor,再包装拦截。从代码顺序上能看到。我们可以通过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程。
在查看插件原理的前提上,我们需要来看看官网对于自定义插件是怎么来做的,官网上有介绍:通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。这里本人踩了一个坑,在Springboot中集成,同时引入了pagehelper-spring-boot-starter 导致RowBounds参数的值被刷掉了,也就是走到了我的拦截其中没有被设置值,这里需要注意,拦截器出了问题,可以Debug看一下Configuration配置类中拦截器链的包装情况。
自定义分页插件
@Intercepts({
@Signature(type = Executor.class,method = "query" ,args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ), // 需要代理的对象和方法
@Signature(type = Executor.class,method = "query" ,args ={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class} ) // 需要代理的对象和方法
})
public class MyPageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("简易版的分页插件:逻辑分页改成物理分页");
// 修改sql 拼接Limit 0,10
Object[] args = invocation.getArgs();
// MappedStatement 对mapper映射文件里面元素的封装
MappedStatement ms= (MappedStatement) args[0];
// BoundSql 对sql和参数的封装
Object parameterObject=args[1];
BoundSql boundSql = ms.getBoundSql(parameterObject);
// RowBounds 封装了逻辑分页的参数 :当前页offset,一页数limit
RowBounds rowBounds= (RowBounds) args[2];
// 拿到原来的sql语句
String sql = boundSql.getSql();
String limitSql=sql+ " limit "+rowBounds.getOffset()+","+ rowBounds.getLimit();
//将分页sql重新封装一个BoundSql 进行后续执行
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), limitSql, boundSql.getParameterMappings(), parameterObject);
// 被代理的对象
Executor executor= (Executor) invocation.getTarget();
CacheKey cacheKey = executor.createCacheKey(ms, parameterObject, rowBounds, pageBoundSql);
// 调用修改过后的sql继续执行查询
return executor.query(ms,parameterObject,rowBounds, (ResultHandler) args[3],cacheKey,pageBoundSql);
}
}
拦截签名跟参数的顺序有严格要求,如果按照顺序找不到对应方法会抛出异常:
org.apache.ibatis.exceptions.PersistenceException:
### Error opening session. Cause: org.apache.ibatis.plugin.PluginException:
Could not find method on interface org.apache.ibatis.executor.Executor named queryv
MyBatis 启动时扫描 标签, 注册到Configuration 对象的 InterceptorChain 中。property 里面的参数,会调用setProperties()方法处理。
分页插件使用
添加pom依赖:
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelperartifactId>
<version>1.2.15version>
dependency>
插件注册,在mybatis-config.xml 中注册插件:
<configuration>
<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="helperDialect" value="mysql" />
<property name="offsetAsPageNum" value="true" />
<property name="rowBoundsWithCount" value="true" />
<property name="pageSizeZero" value="true" />
<property name="reasonable" value="true" />
<property name="params" value="pageNum=start;pageSize=limit;" />
plugin>
plugins>
configuration>
调用
// 获取配置文件
InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
// 通过加载配置文件获取SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// Mybatis在getMapper就会给我们创建jdk动态代理
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
PageHelper.startPage(1, 5);
List<Emp> list=mapper.selectAll();
PageInfo<ServiceStation> info = new PageInfo<ServiceStation>(list, 3);
System.out.println("当前页码:"+info.getPageNum());
System.out.println("每页的记录数:"+info.getPageSize());
System.out.println("总记录数:"+info.getTotal());
System.out.println("总页码:"+info.getPages());
System.out.println("是否第一页:"+info.isIsFirstPage());
System.out.println("连续显示的页码:");
int[] nums = info.getNavigatepageNums();
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
}
代理和拦截是怎么实现的?
上面提到的可以被代理的四大对象都是什么时候被代理的呢?Executor 是openSession() 的时候创建的; StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler 的创建,创建之后即调用InterceptorChain.pluginAll(),返回层层代理后的对象。代理是由Plugin 类创建。在我们重写的 plugin() 方法里面可以直接调用returnPlugin.wrap(target, this);返回代理对象。
单个插件的情况下,代理能不能被代理?代理顺序和调用顺序的关系? 可以被代理。
因为代理类是Plugin,所以最后调用的是Plugin 的invoke()方法。它先调用了定义的拦截器的intercept()方法。可以通过invocation.proceed()调用到被代理对象被拦截的方法。
PageHelper 原理
先来看一下分页插件的简单用法:
PageHelper.startPage(1, 3);
List<Blog> blogs = blogMapper.selectBlogById2(blog);
PageInfo page = new PageInfo(blogs, 3);
对于插件机制我们上面已经介绍过了,在这里我们自然的会想到其所涉及的核心类 :PageInterceptor。拦截的是Executor 的两个query()方法,要实现分页插件的功能,肯定是要对我们写的sql进行改写,那么一定是在 intercept 方法中进行操作的,我们会发现这么一行代码:
String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
调用到 AbstractHelperDialect 中的 getPageSql 方法:
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
// 获取sql
String sql = boundSql.getSql();
//获取分页参数对象
Page page = this.getLocalPage();
return this.getPageSql(sql, page, pageKey);
}
这里可以看到会去调用 this.getLocalPage(),我们来看看这个方法:
public <T> Page<T> getLocalPage() {
return PageHelper.getLocalPage();
}
//线程独享
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
public static <T> Page<T> getLocalPage() {
return (Page)LOCAL_PAGE.get();
}
可以发现这里是调用的是PageHelper的一个本地线程变量中的一个 Page对象,从其中获取我们所设置的 PageSize 与 PageNum,那么他是怎么设置值的呢?请看:
PageHelper.startPage(1, 3);
public static <E> Page<E> startPage(int pageNum, int pageSize) {
return startPage(pageNum, pageSize, true);
}
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
//设置页数,行数信息
setLocalPage(page);
return page;
}
protected static void setLocalPage(Page page) {
//设置值
LOCAL_PAGE.set(page);
}
在我们调用 PageHelper.startPage(1, 3); 的时候,系统会调用 LOCAL_PAGE.set(page) 进行设置,从而在分页插件中可以获取到这个本地变量对象中的参数进行 SQL 的改写,由于改写有很多实现,我们这里用的Mysql的实现:
在这里我们会发现分页插件改写SQL的核心代码,这个代码就很清晰了,不必过多赘述:
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getPageSize());
} else {
sqlBuilder.append(" LIMIT ");
sqlBuilder.append(page.getStartRow());
sqlBuilder.append(",");
sqlBuilder.append(page.getPageSize());
pageKey.update(page.getStartRow());
}
pageKey.update(page.getPageSize());
return sqlBuilder.toString();
}
PageHelper 就是这么一步一步的改写了我们的SQL 从而达到一个分页的效果。
关键类总结:
引入pom依赖
<dependency>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-coreartifactId>
<version>1.4.0version>
dependency>
编写配置文件:
DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressAllComments" value="true"/>
commentGenerator>
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/bookstore"
userId="root"
password="root">
jdbcConnection>
<javaTypeResolver >
<property name="forceBigDecimals" value="false" />
javaTypeResolver>
<javaModelGenerator
targetPackage="cn.tuling.pojo" targetProject="./src/main/java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
javaModelGenerator>
<sqlMapGenerator targetPackage="cn.tuling.mapper" targetProject="./src/main/resources">
<property name="enableSubPackages" value="true" />
sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="cn.tuling.mapper" targetProject="./src/main/java">
<property name="enableSubPackages" value="true" />
javaClientGenerator>
<table tableName="emp" domainObjectName="Emp" mapperName="EmpMapper" >table>
<table tableName="dept" domainObjectName="Dept" mapperName="DeptMapper" >table>
context>
generatorConfiguration>
编写测试类
public class MBGTest {
@Test
public void test01() throws Exception {
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
File configFile = new File("generatorConfig.xml");
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}
}
调用
@Test
public void test01() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// Mybatis在getMapper就会给我们创建jdk动态代理
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
Emp emp = mapper.selectByPrimaryKey(4);
System.out.println(emp);
}
}
/**
* Mybatis3生成调用方式
*/
@Test
public void test02() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// Mybatis在getMapper就会给我们创建jdk动态代理
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
// 使用Example实现动态条件语句
EmpExample empExample=new EmpExample();
EmpExample.Criteria criteria = empExample.createCriteria();
criteria.andUserNameLike("%帅%")
.andIdEqualTo(4);
List<Emp> emps = mapper.selectByExample(empExample);
System.out.println(emps);
}
}