这篇文章主要介绍“Mybatis是什么及怎么使用”,在日常操作中,相信很多人在Mybatis是什么及怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Mybatis是什么及怎么使用”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
JDBC(Java Data Base Connection,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成.JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序
优点:运行期:快捷、高效
缺点:编辑期:代码量大、繁琐异常处理、不支持数据库跨平台
jdbc核心api
DriverManager 连接数据库
Connection 连接数据库的抽象
Statment 执行SQL
ResultSet 数据结果集
DBUtils是Java编程中的数据库操作实用工具,小巧简单实用。
DBUtils封装了对JDBC的操作,简化了JDBC操作,可以少写代码。
DBUtils三个核心功能介绍
QueryRunner中提供对sql语句操作的API
ResultSetHandler接口,用于定义select操作后,怎样封装结果集
DBUtils类,它就是一个工具类,定义了关闭资源与事务处理的方法
ORM 对象关系映射
object java对象
relational 关系型数据
mapping 映射
Hibernate 是由 Gavin King 于 2001 年创建的开放源代码的对象关系框架。它强大且高效的构建具有关系对象持久性和查询服务的 Java 应用程序。
Hibernate 将 Java 类映射到数据库表中,从 Java 数据类型中映射到 SQL 数据类型中,并把开发人员从 95% 的公共数据持续性编程工作中解放出来。
Hibernate 是传统 Java 对象和数据库服务器之间的桥梁,用来处理基于 O/R 映射机制和模式的那些对象。
Hibernate 使用 XML 文件来处理映射 Java 类别到数据库表格中,并且不用编写任何代码。
为在数据库中直接储存和检索 Java 对象提供简单的 APIs。
如果在数据库中或任何其它表格中出现变化,那么仅需要改变 XML 文件属性。
抽象不熟悉的 SQL 类型,并为我们提供工作中所熟悉的 Java 对象。
Hibernate 不需要应用程序服务器来操作。
操控你数据库中对象复杂的关联。
最小化与访问数据库的智能提取策略。
提供简单的数据询问。
hibernate的完全封装导致无法使用数据的一些功能。
Hibernate的缓存问题。
Hibernate对于代码的耦合度太高。
Hibernate寻找bug困难。
Hibernate批量数据操作需要大量的内存空间而且执行过程中需要的对象太多
JdbcTemplate针对数据查询提供了多个重载的模板方法,你可以根据需要选用不同的模板方法.如果你的查询很简单,仅仅是传入相应SQL或者相关参数,然后取得一个单一的结果,那么你可以选择如下一组便利的模板方法。
优点:运行期:高效、内嵌Spring框架中、支持基于AOP的声明式事务
缺点:必须于Spring框架结合在一起使用、不支持数据库跨平台、默认没有缓存
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.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("异常级别"); }
在mybatis的项目中,我们发现了有一个mybatis-config.xml的配置文件,这个配置文件是mybatis的全局配置文件,用来进行相关的全局配置,在任何操作下都生效的配置。下面我们要针对其中的属性做详细的解释,方便大家在后续使用的时候更加熟练。
官方说明:
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 配置文档的顶层结构如下:
configuration(配置)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
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>
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>
更多详细内容见下边的第三章节
在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>
<!-- 参数传递的处理: 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} -->
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>
<!--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>
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); } }
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>
<!-- 嵌套结果: 一对多 查询部门及所有员工 --> <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); }
在上述逻辑的查询中,是由我们自己来完成sql语句的关联查询的,那么,我们能让mybatis帮我们实现自动的关联查询吗?
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>
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); }
当我们在进行表关联的时候,有可能在查询结果的时候不需要关联对象的属性值(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>
三种关联关系都有两种关联查询的方式: 嵌套查询,嵌套结果
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 是 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片段
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>
动态 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>
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,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/>
当添加上该标签之后,会有如下效果:
映射语句文件中的所有 select 语句的结果将会被缓存。
映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
缓存不会定时进行刷新(也就是说,没有刷新间隔)。
缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
在进行配置的时候还会分为一级缓存和二级缓存:
一级缓存:线程级别的缓存,是本地缓存,sqlSession级别的缓存
二级缓存:全局范围的缓存,不止局限于当前会话
一级缓存是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,查询语句一样,执行手动清除缓存
二级缓存是全局作用域缓存,默认是不开启的,需要手动进行配置。
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"/>
整合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 通过提供插件机制,让我们可以根据自己的需要去增强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 从而达到一个分页的效果。
关键类总结:
引入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是什么及怎么使用”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。