一 数据库操作框架的历程
1.1 JDBC
JDBC(Java Data Base Connection,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成.JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序
- 优点:运行期:快捷、高效
- 缺点:编辑期:代码量大、繁琐异常处理、不支持数据库跨平台
jdbc核心api
- DriverManager 连接数据库
- Connection 连接数据库的抽象
- Statment 执行SQL
- ResultSet 数据结果集
1.2 DBUtils
DBUtils是Java编程中的数据库操作实用工具,小巧简单实用。
DBUtils封装了对JDBC的操作,简化了JDBC操作,可以少写代码。
DBUtils三个核心功能介绍
- QueryRunner中提供对sql语句操作的API
- ResultSetHandler接口,用于定义select操作后,怎样封装结果集
- DBUtils类,它就是一个工具类,定义了关闭资源与事务处理的方法
1.3 Hibernate
ORM 对象关系映射
- object java对象
- relational 关系型数据
- mapping 映射
- Hibernate 是由 Gavin King 于 2001 年创建的开放源代码的对象关系框架。它强大且高效的构建具有关系对象持久性和查询服务的 Java 应用程序。
- Hibernate 将 Java 类映射到数据库表中,从 Java 数据类型中映射到 SQL 数据类型中,并把开发人员从 95% 的公共数据持续性编程工作中解放出来。
- Hibernate 是传统 Java 对象和数据库服务器之间的桥梁,用来处理基于 O/R 映射机制和模式的那些对象。
Hibernate 优势
- Hibernate 使用 XML 文件来处理映射 Java 类别到数据库表格中,并且不用编写任何代码。
- 为在数据库中直接储存和检索 Java 对象提供简单的 APIs。
- 如果在数据库中或任何其它表格中出现变化,那么仅需要改变 XML 文件属性。
- 抽象不熟悉的 SQL 类型,并为我们提供工作中所熟悉的 Java 对象。
- Hibernate 不需要应用程序服务器来操作。
- 操控你数据库中对象复杂的关联。
- 最小化与访问数据库的智能提取策略。
- 提供简单的数据询问。
Hibernate劣势
- hibernate的完全封装导致无法使用数据的一些功能。
- Hibernate的缓存问题。
- Hibernate对于代码的耦合度太高。
- Hibernate寻找bug困难。
- Hibernate批量数据操作需要大量的内存空间而且执行过程中需要的对象太多
1.4 JDBCTemplate
JdbcTemplate针对数据查询提供了多个重载的模板方法,你可以根据需要选用不同的模板方法.如果你的查询很简单,仅仅是传入相应SQL或者相关参数,然后取得一个单一的结果,那么你可以选择如下一组便利的模板方法。
- 优点:运行期:高效、内嵌Spring框架中、支持基于AOP的声明式事务
- 缺点:必须于Spring框架结合在一起使用、不支持数据库跨平台、默认没有缓存
1.5 Mybatis
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语句会有很大的差异
二 MyBatis的配置文件详解
2.1 MyBatis日志配置
导入pom
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.30</version></dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
添加logback配置文件
<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{100} - %msg%n</pattern> </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 */@Testpublic void test02(){ LOGGER.trace("跟踪级别"); LOGGER.debug("调试级别"); LOGGER.info("信息级别"); LOGGER.warn("警告级别"); LOGGER.error("异常级别");}
2.2 mybatis-config.xml全局配置文件详解
在mybatis的项目中,我们发现了有一个mybatis-config.xml的配置文件,这个配置文件是mybatis的全局配置文件,用来进行相关的全局配置,在任何操作下都生效的配置。下面我们要针对其中的属性做详细的解释,方便大家在后续使用的时候更加熟练。
官方说明:
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
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> <!-- --> <!-- 当包含多个配置文件或者配置类的时候,可以使用批量注册的功能,也就是引入对应的包,而不是具体的配置文件或者类 但是需要注意的是, 1、如果使用的配置文件的形式,必须要将配置文件跟dao类放在一起,这样才能找到对应的配置文件. 如果是maven的项目的话,还需要添加以下配置,原因是maven在编译的文件的时候只会编译java文件 src/main/java **/*.xml 2、将配置文件在resources资源路径下创建跟dao相同的包名 --> <package name="cn.tulingxueyuan.dao"/> </mappers></configuration>
2.3 Mybatis SQL映射文件详解
MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。
SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
- cache – 该命名空间的缓存配置。
- cache-ref – 引用其它命名空间的缓存配置。
- resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
- parameterMap – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。
- sql – 可被其它语句引用的可重用语句块。
- insert – 映射插入语句。
- update – 映射更新语句。
- delete – 映射删除语句。
- select – 映射查询语句。
在每个顶级元素标签中可以添加很多个属性,下面我们开始详细了解下具体的配置。
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>
更多详细内容见下边的第三章节
三 MyBatis基于XML的详细使用-参数、返回结果处理
3.1 参数的取值方式
在xml文件中编写sql语句的时候有两种取值的方式,分别是#{}和${},下面来看一下他们之间的区别:
3.3 处理集合返回结果
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> <!--注意,当返回的结果是一个集合对象的时候,返回值的类型一定要写集合具体value的类型, 同时在dao的方法上要添加@MapKey的注解,来设置key是什么结果 @MapKey("empno") Map getAllEmpReturnMap();--> <select id="getAllEmpReturnMap" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp </select>
3.4 自定义结果集—resultMap
<!--1.声明resultMap自定义结果集 resultType 和 resultMap 只能使用一个。 id 唯一标识, 需要和<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" > <!-- 主键必须使用 对底层存储有性能作用 column 需要映射的数据库字段名 property 需要映射的pojo属性名 --> <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>
四 MyBatis基于XML的详细使用——高级结果映射
4.1 联合查询
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 + ",token operator">+ id + ", username='" + username + '\'' + '}'; }}
Test
@Testpublic 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); }}
4.2 嵌套结果
4.2.1 多对一
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>
4.2.2 一对多
<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);}
4.3 嵌套查询
在上述逻辑的查询中,是由我们自己来完成sql语句的关联查询的,那么,我们能让mybatis帮我们实现自动的关联查询吗?
4.3.1 多对一
EmpMapper.xml:
<!--嵌套查询(分步查询) 多 对 一 联合查询和分步查询区别: 性能区别不大 分部查询支持 懒加载(延迟加载) 需要设置懒加载,一定要使用嵌套查询的。 要启动懒加载可以在全局配置文件中设置 lazyLoadingEnabled=true 还可以单独为某个分步查询设置立即加载 <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>
4.3.2 一对多
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);}
4.4 延迟查询
当我们在进行表关联的时候,有可能在查询结果的时候不需要关联对象的属性值(select count(1)…),那么此时可以通过延迟加载来实现功能。在全局配置文件中添加如下属性
mybatis-config.xml
<setting name="lazyLoadingEnabled" value="true"/><!--当开启式, 使用pojo中任意属性都会加载延迟查询 ,默认是false--><setting name="lazyLoadTriggerMethods" value=""/>
如果设置了全局加载,但是希望在某一个sql语句查询的时候不使用延时策略,可以添加fetchType下属性:
<association property="dept" fetchType="eager" column="dept_id" select="cn.tulingxueyuan.mapper.DeptMapper.SelectDept"></association>
4.5 总结
三种关联关系都有两种关联查询的方式: 嵌套查询,嵌套结果
Mybatis的延迟加载配置, 在全局配置文件中加入下面代码
<settings><setting name=”lazyLoadingEnabled” value=”true” /><setting name=”aggressiveLazyLoading” value=”false”/></settings>
在映射文件中,元素和元素中都已默认配置了延迟加载属性,即默认属性fetchType=”lazy”(属性fetchType=”eager”表示立即加载),所以在配置文件中开启延迟加载后,无需在映射文件中再做配置
一对一
使用元素进行一对一关联映射非常简单,只需要参考如下两种示例配置即可
一对多
元素中,包含了一个
子元素,MyBatis就是通过该元素来处理一对多关联关系的
子元素的属性大部分与
元素相同,但其还包含一个特殊属性–ofType
ofType属性与javaType属性对应,它用于指定实体对象中集合类属性所包含的元素类型。
元素的使用也非常简单,同样可以参考如下两种示例进行配置,具体代码如下:
多对多
多对多的关联关系查询,同样可以使用前面介绍的元素进行处理(其用法和一对多关联关系查询语句用法基本相同)
五 动态sql
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis 之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
- bind
- sql片段
5.1 if
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>
5.2 foreach
动态 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>
5.3 choose、when、otherwise
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,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>
5.4 set
用于动态更新语句的类似解决方案叫做 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>
5.5 bind
bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:
<select id="selectBlogsLike" resultType="Blog"> <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" /> SELECT * FROM BLOG WHERE title LIKE #{pattern}</select>
5.6 sql
这个元素可以用来定义可重用的 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缓存
6.1 缓存介绍
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
当添加上该标签之后,会有如下效果:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
在进行配置的时候还会分为一级缓存和二级缓存:
- 一级缓存:线程级别的缓存,是本地缓存,sqlSession级别的缓存
- 二级缓存:全局范围的缓存,不止局限于当前会话
6.2 一级缓存的使用
一级缓存是sqlsession级别的缓存,默认是存在的。在下面的案例中,大家发现我发送了两个相同的请求,但是sql语句仅仅执行了一次,那么就意味着第一次查询的时候已经将结果进行了缓存。
@Testpublic 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、如果在发送过程中发生了数据的修改,那么结果就不会缓存
@Testpublic 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、在两次查询期间,手动去清空缓存,也会让缓存失效
@Testpublic 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();}
特性
一级缓存特性:
- 默认就开启了,也可以关闭一级缓存 localCacheScope=STATEMENT
- 作用域:是基于sqlSession(默认),一次数据库操作会话。
- 缓存默认实现类PerpetualCache ,使用map进行存储的
- 查询完就会进行存储
- 先从二级缓存中获取,再从一级缓存中获取
key==> sqlid+sql
一级缓存失效情况:
- 不同的sqlSession会使一级缓存失效
- 同一个SqlSession,但是查询语句不一样
- 同一个SqlSession,查询语句一样,期间执行增删改操作
- 同一个SqlSession,查询语句一样,执行手动清除缓存
6.3 二级缓存的使用
二级缓存是全局作用域缓存,默认是不开启的,需要手动进行配置。
Mybatis提供二级缓存的接口以及实现,缓存实现的时候要求实体类实现Serializable接口,二级缓存在sqlSession关闭或提交之后才会生效。
二级缓存的使用
步骤:
1、全局配置文件中添加如下配置:
<setting name="cacheEnabled" value="true"/>
2、需要在使用二级缓存的映射文件处使用标签标注
3、实体类必须要实现Serializable接口
@Testpublic 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();}
缓存的属性
- eviction:表示缓存回收策略,默认是LRU
- LRU:最近最少使用的,移除最长时间不被使用的对象
- FIFO:先进先出,按照对象进入缓存的顺序来移除
- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象
- flushInternal:刷新间隔,单位毫秒
- 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
- size:引用数目,正整数
- 代表缓存最多可以存储多少个对象,太大容易导致内存溢出
- readonly:只读,true/false
- true:只读缓存,会给所有调用这返回缓存对象的相同实例,因此这些对象不能被修改。
- false:读写缓存,会返回缓存对象的拷贝(序列化实现),这种方式比较安全,默认值
//可以看到会去二级缓存中查找数据,而且二级缓存跟一级缓存中不会同时存在数据,因为二级缓存中的数据是在sqlsession 关闭之后才生效的@Testpublic 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();}
缓存查询的顺序是先查询二级缓存再查询一级缓存
@Testpublic 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()
只是用来清除一级缓存
二级缓存特性
- 默认开启了,没有实现
- 作用域:基于全局范围,应用级别。
- 缓存默认实现类PerpetualCache ,使用map进行存储的但是二级缓存根据不同的mapper命名空间多包了一层map
: org.apache.ibatis.session.Configuration#caches key:mapper命名空间 value:erpetualCache.map key==> sqlid+sql
- 事务提交的时候(sqlSession关闭)
- 先从二级缓存中获取,再从一级缓存中获取
实现:
- 开启二级缓存
- 在需要使用到二级缓存的映射文件中加入,基于Mapper映射文件来实现缓存的,基于Mapper映射文件的命名空间来存储的
- 在需要使用到二级缓存的javaBean中实现序列化接口implements Serializable
配置成功就会出现缓存命中率 同一个sqlId: 从缓存中拿出的次数/查询总次数
失效:
- 同一个命名空间进行了增删改的操作,会导致二级缓存失效
但是如果不想失效:可以将SQL的flushCache 这是为false,但是要慎重设置,因为会造成数据脏读问题,除非你能保证查询的数据永远不会执行增删改 - 让查询不缓存数据到二级缓存中useCache=“false”
- 如果希望其他mapper映射文件的命名空间执行了增删改清空另外的命名空间就可以设置:
<cache-ref namespace="cn.tulingxueyuan.mapper.DeptMapper"/>
6.4 整合第三方缓存
整合redis
添加redis-mybatis 缓存适配器 依赖
<dependencies> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency></dependencies>
添加redis.properties在resources根目录
host=localhostport=6379connectionTimeout=5000soTimeout=5000password=无密码可不填database=0clientName=
设置mybatis二级缓存实现类
<cache ... type="org.mybatis.caches.redis.RedisCache" ....../>
整合ehcache
导入对应的maven依赖
<dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.8.1</version> </dependency> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.0</version> </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><!--属性说明:l diskStore:指定数据在磁盘中的存储位置。l defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用指定的的管理策略以下属性是必须的:l maxElementsInMemory - 在内存中缓存的element的最大数目l maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大l eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断l overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上以下属性是可选的:l timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大l timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.l diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作l memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)-->
在mapper文件中添加自定义缓存
<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
七 MyBatis分页插件&逆向工程
7.1 分页插件
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.pagehelper</groupId><artifactId>pagehelper</artifactId><version>1.2.15</version></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" /><!-- 启用合理化时,如果pageNumpages会查询最后一页 --><!-- 禁用合理化时,如果pageNumpages会返回空数据 --><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 从而达到一个分页的效果。
关键类总结:
7.2 MyBatis逆向工程
引入pom依赖
<dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.4.0</version></dependency>
编写配置文件:
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"><generatorConfiguration><!-- 指定数据库驱动用java代码的方式生成可以不指定(只需要吧mybatis-generator和数据库驱动依赖到项目) --> <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); }}
调用
@Testpublic 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生成调用方式 */@Testpublic 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); }}