Mybatis的介绍、基本使用、高级使用
作者:麦芽糖0219
一 数据库操作框架的历程
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 取个名字 class 不同实现类会输出到不同地方 ch.qos.logback.core.ConsoleAppender 输出到控制台 --> <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> <!--cn.tulingxueyuan.mapper--> <!--控制跟细粒度的日志级别 根据包\根据类--> <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("异常级别"); }
2.2 mybatis-config.xml全局配置文件详解
在mybatis的项目中,我们发现了有一个mybatis-config.xml的配置文件,这个配置文件是mybatis的全局配置文件,用来进行相关的全局配置,在任何操作下都生效的配置。下面我们要针对其中的属性做详细的解释,方便大家在后续使用的时候更加熟练。
官方说明:
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- environments(环境配置)
- dataSource(数据源)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
mybatis-config.xml
<?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> <!--引入外部配置文件,类似于Spring中的property-placeholder resource:从类路径引入 url:从磁盘路径或者网络路径引入 --> <properties resource="db.properties"></properties> <!--用来控制mybatis运行时的行为,是mybatis中的重要配置--> <settings> <!--设置列名映射的时候是否是驼峰标识--> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <!--typeAliases表示为我们引用的实体类起别名,默认情况下我们需要写类的完全限定名 如果在此处做了配置,那么可以直接写类的名称,在type中配置上类的完全限定名,在使用的时候可以忽略大小写 还可以通过alias属性来表示类的别名 --> <typeAliases> <!-- <typeAlias type="cn.tulingxueyuan.bean.Emp" alias="Emp"></typeAlias>--> <!--如果需要引用多个类,那么给每一个类起别名肯定会很麻烦,因此可以指定对应的包名,那么默认用的是类名--> <package name="cn.tulingxueyuan.bean"/> </typeAliases> <!-- 在实际的开发过程中,我们可能分为开发环境,生产环境,测试环境等等,每个环境的配置可以是不一样的 environment就用来表示不同环境的细节配置,每一个环境中都需要一个事务管理器以及数据源的配置 我们在后续的项目开发中几乎都是使用spring中配置的数据源和事务管理器来配置,此处不需要研究 --> <!--default:用来选择需要的环境--> <environments default="development"> <!--id:表示不同环境的名称--> <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> <!-- 在不同的数据库中,可能sql语句的写法是不一样的,为了增强移植性,可以提供不同数据库的操作实现 在编写不同的sql语句的时候,可以指定databaseId属性来标识当前sql语句可以运行在哪个数据库中 --> <databaseIdProvider type="DB_VENDOR"> <property name="MySQL" value="mysql"/> <property name="SQL Server" value="sqlserver"/> <property name="Oracle" value="orcl"/> </databaseIdProvider> <!--将sql的映射文件适用mappers进行映射--> <mappers> <!-- 指定具体的不同的配置文件 class:直接引入接口的全类名,可以将xml文件放在dao的同级目录下,并且设置相同的文件名称,同时可以使用注解的方式来进行相关的配置 url:可以从磁盘或者网络路径查找sql映射文件 resource:在类路径下寻找sql映射文件 --> <!-- <mapper resource="EmpDao.xml"/> <mapper resource="UserDao.xml"/> <mapper class="cn.tulingxueyuan.dao.EmpDaoAnnotation"></mapper>--> <!-- 当包含多个配置文件或者配置类的时候,可以使用批量注册的功能,也就是引入对应的包,而不是具体的配置文件或者类 但是需要注意的是, 1、如果使用的配置文件的形式,必须要将配置文件跟dao类放在一起,这样才能找到对应的配置文件. 如果是maven的项目的话,还需要添加以下配置,原因是maven在编译的文件的时候只会编译java文件 <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build> 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语句的时候有两种取值的方式,分别是#{}和${},下面来看一下他们之间的区别:
<!--获取参数的方式: 1.#{} ==> jdbc String sql=" SELECT id,user_name FROM EMP WHERE id=?" 1.会经过JDBC当中PreparedStatement的预编译,会根据不同的数据类型来编译成对应数据库所对应的数据。 2.能够有效的防止SQL注入。 推荐使用!! 特殊用法: 自带很多内置参数的属性:通常不会使用。了解 javaType、jdbcType、mode、numericScale、resultMap、typeHandler. 比如 需要改变默认的NULL===>OTHER:#{id,javaType=NULL} 想保留小数点后两位:#{id,numericScale=2} 2.${} ==> jdbc String sql=" SELECT id,user_name FROM EMP WHERE id="+id 1.不会进行预编译,会直接将输入进来的数据拼接在SQL中。 2.存在SQL注入的风险。不推荐使用。 特殊用法: 1.调试情况下可以临时使用。 2.实现一些特殊功能:前提一定要保证数据的安全性。 比如:动态表、动态列. 动态SQL. --> <select id="SelectEmp" resultType="Emp" resultMap="emp_map" > SELECT id,user_name,create_date FROM EMP where id=#{id} </select>
3.2 select的参数传递
<!-- 参数传递的处理: 1.单个参数:SelectEmp(Integer id); mybatis 不会做任何特殊要求 获取方式: #:{输入任何字符获取参数} 2.多个参数:Emp SelectEmp(Integer id,String username); mybatis 会进行封装 会将传进来的参数封装成map: 1个值就会对应2个map项 : id===> {key:arg0 ,value:id的值},{key:param1 ,value:id的值} username===> {key:arg1 ,value:id的值},{key:param2 ,value:id的值} 获取方式: 没使用了@Param: id=====> #{arg0} 或者 #{param1} username=====> #{arg1} 或者 #{param2} 除了使用这种方式还有别的方式,因为这种方式参数名没有意义: 设置参数的别名:@Param(""):SelectEmp(@Param("id") Integer id,@Param("username") String username); 当使用了@Param: id=====> #{id} 或者 #{param1} username=====> #{username} 或者 #{param2} 3. javaBean的参数: 单个参数:Emp SelectEmp(Emp emp); 获取方式:可以直接使用属性名 emp.id=====>#{id} emp.username=====>#{username} 多个参数:Emp SelectEmp(Integer num,Emp emp); num===> #{param1} 或者 @Param emp===> 必须加上对象别名: emp.id===> #{param2.id} 或者 @Param("emp")Emp emp ====>#{emp.id} emp.username===> #{param2.username} 或者 @Param("emp")Emp emp ====>#{emp.username} 4.集合或者数组参数: Emp SelectEmp(List<String> usernames); 如果是list,MyBatis会自动封装为map: {key:"list":value:usernames} 没用@Param("")要获得:usernames.get(0) =====> #{list[0]} :usernames.get(0) =====> #{agr0[0]} 有@Param("usernames")要获得:usernames.get(0) =====> #{usernames[0]} :usernames.get(0) =====> #{param1[0]} 如果是数组,MyBatis会自动封装为map: {key:"array":value:usernames} 没用@Param("")要获得:usernames.get(0) =====> #{array[0]} :usernames.get(0) =====> #{agr0[0]} 有@Param("usernames")要获得:usernames.get(0) =====> #{usernames[0]} :usernames.get(0) =====> #{param1[0]} 5.map参数 和javaBean的参数传递是一样。 一般情况下: 请求进来的参数 和pojo对应,就用pojo 请求进来的参数 没有和pojo对应,就用map 请求进来的参数 没有和pojo对应上,但是使用频率很高,就用TO、DTO(就是单独为这些参数创建一个对应的javaBean出来,使参数传递更规范、更重用) --> <!-- 接口:SelectEmp(String username,@Param("id") Integer id); username====> #{arg0} #{param1} id====> #{id} #{param2} 接口:SelectEmp(@Param("beginDate") String beginDate, String endDate, Emp emp); beginDate====> #{beginDate} #{param1} endDate====> #{arg1} #{param2} emp.id====>#{arg2.id} #{param2.id} 接口:SelectEmp(List<Integer> ids, String[] usernames, @Param("beginDate") String beginDate, String endDate,); ids.get(0)=====> #{list[0]} #{param1[0]} usernames[0]=====> #{array[0]} #{param2[0]} beginDate====> #{beginDate} #{param3} end====> #{arg3} #{param4} -->
3.3 处理集合返回结果
EmpDao.xml
<!--当返回值的结果是集合的时候,返回值的类型依然写的是集合中具体的类型--> <select id="selectAllEmp" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp </select> <!--在查询的时候可以设置返回值的类型为map,当mybatis查询完成之后会把列的名称作为key 列的值作为value,转换到map中 --> <select id="selectEmpByEmpReturnMap" resultType="map"> select * from emp where empno = #{empno} </select> <!--注意,当返回的结果是一个集合对象的时候,返回值的类型一定要写集合具体value的类型, 同时在dao的方法上要添加@MapKey的注解,来设置key是什么结果 @MapKey("empno") Map<Integer,Emp> getAllEmpReturnMap();--> <select id="getAllEmpReturnMap" resultType="cn.tulingxueyuan.bean.Emp"> select * from emp </select>
3.4 自定义结果集—resultMap
<!--1.声明resultMap自定义结果集 resultType 和 resultMap 只能使用一个。 id 唯一标识, 需要和<select 上的resultMap 进行对应 type 需要映射的pojo对象, 可以设置别名 autoMapping 自动映射,(默认=true) 只要字段名和属性名遵循映射规则就可以自动映射,但是不建议,哪怕属性名和字段名一一对应上了也要显示的配置映射 extends 如果多个resultMap有重复映射,可以声明父resultMap,将公共的映射提取出来, 可以减少子resultMap的映射冗余 --> <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 需要映射的数据库字段名 property 需要映射的pojo属性名 --> <id column="id" property="id"></id> <result column="user_name" property="username"></result> </resultMap> <!--2.使用resultMap 关联 自定义结果集的id--> <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
<!-- 实现表联结查询的方式: 可以映射: DTO --> <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> <!-- 实现表联结查询的方式: 可以映射map --> <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); } }
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 指定对象中的嵌套对象属性 --> <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 指定需要映射的“多”的属性,一般声明为List ofType 需要指定list的类型 --> <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 还可以单独为某个分步查询设置立即加载 <association fetchType="eager" --> <resultMap id="QueryEmp_Map3" type="Emp"> <id column="id" property="id"></id> <result column="user_name" property="username"></result> <!-- association 实现多对一中的 “一” property 指定对象中的嵌套对象属性 column 指定将哪个字段传到分步查询中 select 指定分步查询的 命名空间+ID 以上3个属性是实现分步查询必须的属性 fetchType 可选, eager|lazy eager立即加载 lazy跟随全局配置文件中的lazyLoadingEnabled --> <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
<!-- 根据部门id查询部门--> <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 指定需要映射的“多”的属性,一般声明为List ofType 需要指定list的类型 column 需要将当前查询的字段传递到异步查询的参数 select 指定异步查询 --> <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
<!-- 根据部门id所有员工 --> <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="aggressiveLazyLoading" value="false"/>--> <!--设置对象的哪些方法调用会加载延迟查询 默认:equals,clone,hashCode,toString--> <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”表示立即加载),所以在配置文件中开启延迟加载后,无需在映射文件中再做配置
一对一
使用元素进行一对一关联映射非常简单,只需要参考如下两种示例配置即可
一对多
<resultMap>
元素中,包含了一个<collection>
子元素,MyBatis就是通过该元素来处理一对多关联关系的<collection>
子元素的属性大部分与<association>
元素相同,但其还包含一个特殊属性–ofType
ofType属性与javaType属性对应,它用于指定实体对象中集合类属性所包含的元素类型。<collection >
元素的使用也非常简单,同样可以参考如下两种示例进行配置,具体代码如下:
多对多
多对多的关联关系查询,同样可以使用前面介绍的元素进行处理(其用法和一对多关联关系查询语句用法基本相同)
五 动态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
<!-- trim截取字符串: prefix:前缀,为sql整体添加一个前缀 prefixOverrides:去除整体字符串前面多余的字符 suffixOverrides:去除后面多余的字符串 --> <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 条件语句的时候)。
<!--foreach是对集合进行遍历 collection="deptnos" 指定要遍历的集合 close="" 表示以什么结束 index="" 给定一个索引值 item="" 遍历的每一个元素的值 open="" 表示以什么开始 separator="" 表示多个元素的分隔符 --> <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语句仅仅执行了一次,那么就意味着第一次查询的时候已经将结果进行了缓存。
@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(); }
特性
一级缓存特性:
- 默认就开启了,也可以关闭一级缓存 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接口
@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(); }
缓存的属性
- eviction:表示缓存回收策略,默认是
- LRU LRU:最近最少使用的,移除最长时间不被使用的对象
- FIFO:先进先出,按照对象进入缓存的顺序来移除
- SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象
- flushInternal:刷新间隔,单位毫秒
- 默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
- size:引用数目,正整数
- 代表缓存最多可以存储多少个对象,太大容易导致内存溢出
- readonly:只读,true/false
- true:只读缓存,会给所有调用这返回缓存对象的相同实例,因此这些对象不能被修改。
- false:读写缓存,会返回缓存对象的拷贝(序列化实现),这种方式比较安全,默认值
//可以看到会去二级缓存中查找数据,而且二级缓存跟一级缓存中不会同时存在数据,因为二级缓存中的数据是在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()
只是用来清除一级缓存
二级缓存特性
- 默认开启了,没有实现
- 作用域:基于全局范围,应用级别。
- 缓存默认实现类PerpetualCache ,使用map进行存储的但是二级缓存根据不同的mapper命名空间多包了一层map
: org.apache.ibatis.session.Configuration#caches key:mapper命名空间 value:erpetualCache.map key==> sqlid+sql
- 事务提交的时候(sqlSession关闭)
- 先从二级缓存中获取,再从一级缓存中获取
实现:
- 开启二级缓存
<setting name="cacheEnabled" value="true"/>
- 在需要使用到二级缓存的映射文件中加入,基于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=localhost port=6379 connectionTimeout=5000 soTimeout=5000 password=无密码可不填 database=0 clientName=
设置mybatis二级缓存实现类
<cache ... type="org.mybatis.caches.redis.RedisCache" ....../>
整合ehcache
导入对应的maven依赖
<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache --> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.8.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.0</version> </dependency>
导入ehcache配置文件
<?xml version="1.0" encoding="UTF-8"?> <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便会采用<defalutCache/>指定的的管理策略 以下属性是必须的: 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> <!-- com.github.pagehelper为PageHelper类所在包名 --> <plugin interceptor="com.github.pagehelper.PageHelper"> <property name="helperDialect" value="mysql" /> <!-- 该参数默认为false --> <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 --> <!-- 和startPage中的pageNum效果一样 --> <property name="offsetAsPageNum" value="true" /> <!-- 该参数默认为false --> <!-- 设置为true时,使用RowBounds分页会进行count查询 --> <property name="rowBoundsWithCount" value="true" /> <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 --> <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) --> <property name="pageSizeZero" value="true" /> <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 --> <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 --> <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 --> <property name="reasonable" value="true" /> <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 --> <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 --> <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 --> <!-- 不理解该含义的前提下,不要随便复制该配置 --> <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>
编写配置文件:
<?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> <!-- 指定数据库驱动 用java代码的方式生成可以不指定(只需要吧mybatis-generator和数据库驱动依赖到项目) <classPathEntry location ="F:\java\jar\mysql-connector-java-5.1.22-bin.jar" /> --> <!-- targetRuntime MyBatis3 可以生成通用查询,可以指定动态where条件 MyBatis3Simple 只生成CURD --> <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> <!-- java类型生成方式 --> <javaTypeResolver > <!-- forceBigDecimals true 当数据库类型为decimal 或number 生成对应的java的BigDecimal false 会根据可数据的数值长度生成不同的对应java类型 useJSR310Types false 所有数据库的日期类型都会生成java的 Date类型 true 会将数据库的日期类型生成对应的JSR310的日期类型 比如 mysql的数据库类型是date===>LocalDate 必须jdk是1.8以上 --> <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <!-- pojo的生成规则 --> <javaModelGenerator targetPackage="cn.tuling.pojo" targetProject="./src/main/java"> <property name="enableSubPackages" value="true" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- sql映射文件的生成规则 --> <sqlMapGenerator targetPackage="cn.tuling.mapper" targetProject="./src/main/resources"> <property name="enableSubPackages" value="true" /> </sqlMapGenerator> <!-- dao的接口生成规则 UserMapper--> <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); } }
到此这篇关于Mybatis的介绍、基本使用、高级使用的文章就介绍到这了,更多相关Mybatis的介绍和使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!