一、mybatis简介
1.1 框架概念
软件的半成品,完成软件开发过程中的通用操作,实现特定的功能,从而简化开发人员在软件开发中的步骤,提升开发效率。
1.2 常用框架
- MVC框架:简化servlet的开发步骤,与前端交互
- struct2
- springMVC
- 持久层框架:完成数据库操作的框架,与数据库交互
- mybatis
- hibernate
- 胶水框架:Spring --- 负责协同MVC和持久层
1.3 mybatis介绍
mybatis是半自动的ORM框架,提供了实体类和数据表的映射关系,通过映射文件配置,实现对象的持久化。
- 半自动:封装度不高,SQL语句还需要自己写,比较适合处理复杂的SQL,多表联查等(hibernate是全自动)
- ORM(Object Relaitional Mapping):对象关系映射,将Java中的一个对象与表中的一行记录一一对应。
1.4 mybatis 的特点
- 支持自定义SQL,存储过程
- 对原有的JDBC进行封装,几乎消除了所有的JDBC代码,开发者只需要关注SQL本身
- 支持XML和注解配置的方式自动完成ORM操作,实现结果映射。
二、mybatis框架部署
- 创建一个maven项目
- 在pom.xml中添加依赖(mybatis依赖,mysql driver依赖)
- 创建mybatis主配置文件
- resource目录下新建mybatis-config.xml配置文件
- mybatis-config.xml文件中配置数据库连接信息
pom中的依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.9</version>
</dependency>
</dependencies>
<?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>
<!--配置 mybatis 环境-->
<environments default="mysql">
<!--id:数据源的名称-->
<environment id="mysql">
<!--配置事务类型:使用 JDBC 事务(使用 Connection 的提交和回滚)-->
<transactionManager type="JDBC"/>
<!--数据源 dataSource:创建数据库 Connection 对象 type: POOLED 使用数据库的连接池 -->
<dataSource type="POOLED">
<!--连接数据库的四个要素-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/ssm"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--告诉 mybatis 要执行的 sql 语句的位置,建议多个映射文件写在一个文件夹下面-->
<mapper resource="com/bjpowernode/dao/StudentDao.xml"/>
</mappers>
</configuration>
支持中文的 url
jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=utf-8
三、mybatis框架使用
- 创建表结构信息
- domain包中创建实体类,属性和表属性对应
- dao包中创建接口,定义操作数据库的方法
- resource包下创建一个mappers文件夹,创建dao包中接口的映射文件
- 映射文件添加到主配置文件中,告诉mybatis要执行的SQL的位置。
- 测试用例
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
namespace:必须有值,自定义的唯一字符串
推荐使用:dao 接口的全限定名称
-->
<mapper namespace="com.bjpowernode.dao.StudentDao">
<!--
<select>: 查询数据, 标签中必须是 select 语句
id: sql 语句的自定义名称,推荐使用 dao 接口中方法名称,
使用名称表示要执行的 sql 语句
resultType: 查询语句的返回结果数据类型,使用全限定类名
-->
<select id="selectStudents" resultType="com.bjpowernode.domain.Student">
<!--要执行的 sql 语句-->
select id,name,email,age from student
</select>
</mapper>
四、mybatis 的动态SQL
动态SQL:根据查询条件动态完成SQL的拼接
4.1 动态SQL <if>
对于该标签的执行,当 test 的值为 true 时,会将其包含的 SQL 片断拼接到其所在的 SQL 语句中。
语法:
<if test="条件"> sql 语句的部分 </if>
<select id="selectStudentIf" resultType="com.bjpowernode.domain.Student">
select id,name,email,age from student
where 1=1
<if test="name != null and name !='' ">
and name = #{name}
</if>
<if test="age > 0 ">
and age > #{age}
</if>
</select>
# 注意where条件后面的1=1
4.2 动态SQL <where>
if标签的中存在一个比较麻烦的地方:需要在 where 后手工添加 1=1 的子句。因为,若 where 后 的所有条件均为 false,而 where 后若又没有 1=1 子句,则 SQL 中就会只剩下一个空的 where,SQL 出错。但当数据量很大时,会 严重影响查询效率。
使用<where/>标签,在有条件查询时,会自动加上where子句,没有条件查询也不会添加where子句,需要注意的是,第一个<if/>标签中的SQL片段,既可以加and,也可以不加and,而其他<if/>标签中的SQL片段必须加上and,但是建议都加上。
语法:
<where> 其他动态SQL </where>
<select id="selectStudentWhere" resultType="com.bjpowernode.domain.Student">
select id,name,email,age from student
<where>
<if test="name != null and name !='' ">
and name = #{name}
</if>
<if test="age > 0 ">
and age > #{age}
</if>
</where>
</select>
4.3 动态SQL之 <trim>
<select id="selectStudentWhere" resultType="com.bjpowernode.domain.Student">
select id,name,email,age from student
<trim prefix="where" prefixOverrides="and | or" suffix="order by age">
<if test="name != null and name !='' ">
and name = #{name}
</if>
<if test="age > 0 ">
and age > #{age}
</if>
</trim>
</select>
# prefix 前缀
# prefixOverrides 第一个是and或者or,就自动去掉
# suffix 后缀
4.4 动态SQL之 foreach
语法
<foreach collection="集合类型" open="开始的字符" close="结束的字符"
item="集合中的成员" separator="分隔符">
#{stuid}
</foreach>
<select id="selectStudentForList" resultType="com.bjpowernode.domain.Student">
select id,name,email,age from student
<if test="list !=null and list.size > 0 ">
where id in
<foreach collection="list" open="(" close=")"
item="stuid" separator=",">
#{stuid}
</foreach>
</if>
</select>
五、一些常见的问题
5.1 mybatis的优缺点
- 优点:
- 半ORM的框架,封装了JDBC代码,开发者只需要关注如何编写SQL,减少了开发难度
- SQL写在XML里面,保证了SQL和代码的解耦,便于SQL的统一管理。支持编写动态SQL语句,保证SQL的可重用性
- 很好的与各种数据库兼容
- 能够与spring很好的集成
- 缺点
- SQL编写工作量大,尤其是多表联查的时候,SQL的编写较为复杂
- SQL语句依赖数据库,移植性差,不能随意更换数据库。
5.2 #{}和${}的区别是什么?
- 编译过程
- #{} 是占位符,SQL会在执行前进行预编译,将#{}对应的内容替换为 ? 调用prepareSatement的set方法进行赋值
- ${}是拼接符,字符的替换,相当于对应的内容append
- 是否自动加引号
- #{}对应的变量会自动加上引号
- ${}对应的变量不会自动加引号,因此编写的时候要注意手动加引号
- 安全性
- #{} 因为是预编译的占位符,因此能防止SQL注入
- ${}则不能防止SQL注入
5.3 mybatis中 dao接口和mapper.xml文件中的SQL是如何建立关联的?
mapper.xml其实是对dao接口的实现,mapper.xml中的namespace对应的是dao接口的全限定名。
1、解析XML: mybatis初始化SqlSessionFactoryBean的时候,会找到mapperLocations路径去解析里面所有的XML文件。
- 创建SqlSource:把每个SQL封装成SqlSource对象(分为静态SQL--字符串和动态SQL---SQLNode)
- 创建MappedStatement:每个SQL标签就对应一个MappedStatement对象,包括两个属性。创建完MappedStatement对象,就把他缓存到Configuration对象中,基本所有的配置信息都维护在Configuration
- id: 全限定类名加方法名组成的id
- sqlSource:当前SQL标签对应的SqlSource对象
2、Dao接口代理
- dao的初始化
- SqlSessionFactoryBuilder 的build方法去解析配置文件,得到一个SqlSessionFactory工厂对象,传入的参数是一个inputStream流,其实就是去读取mybatis的主配置文件和mapper.xml ---- 同时将所有的配置信息都缓存在Configuration对象中
- factory.openSession() 建立连接,获取事务管理配置,创建事务管理对象
- sqlSession.getMapper(dao.class) ---- 通过jdk动态代理产生dao代理对象。
5.4 Dao接口里面的方法,参数不同的时候可以重载吗?
Mapper接口里的方法,是不能重载的,因为mybatis是使用 全限名+方法名 的保存和寻找策略;当调用接口方法时,通过 “接口全限名+方法名”拼接字符串作为key值,可唯一定位一个MapperStatement,因为在Mybatis中,每一个SQL标签,都会被解析为一个MapperStatement对象。如果重载,方法名一致,就会导致key value的覆盖。
5.5 Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?
记住mybatis是使用 全限名+方法名 的保存和寻找策略,如果不同的xml的namespace不同,那么id是可以重复的。
5.6 mybatis中的分页?
- 原生的limit分页:
- 需要在SQL中编写limit关键字,然后传入分页参数进行分页
- 缺点是编写SQL复杂
- RowBounds分页:
- 是mybatis中内置的一个处理分页的类,可以不需要在映射SQL中写limit关键字,自动给我们拼接,使用方便
- 不适用于处理数据量较大的场景。
- 拦截器插件分页(如pageHelper)
5.7 mybatis插件运行原理
mybatis框架下,我们可以基于插件机制实现分页,分表和监控等功能。
mybatis的四大组件:Executor、StatementHandler、ParameterHandler、ResultSetHandler,mybatis支持用插件对四大核心对象进行拦截,其实,插件就是拦截器,用来增强核心对象的功能,增强功能的本质是借助动态代理实现的。
Mybatis编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
插件运行原理:
- 每个对象创建出来不是直接返回,而是调用了interceptorChain.pluginAll(parameterHandler);
- 拦截器链是mybatis初始化时创建出来的,保存了所有的拦截器
- 遍历所有拦截器,调用intercept.plugin(target)返回封装后的target对象
- interceptor.plugin(target)中调用了Plugin.warp(target,this)
- Plugin类实现了InvocationHandler接口,也就是说调用Plugin类的方法最终会调用Plugin的invoke()方法的实现
- invoke()方法最终会回调Interceptor的抽象方法intercept()
- intercept()方法具体实现是我们自己
- 我们可以使用插件为目标对象创建一个代理对象,AOP四大组件,然后拦截四大对象的每次执行
5.8 mybatis的延迟加载?
mybatis的延迟加载也称为懒加载,是指在进行表的管理查询时,只查询需要使用的数据,关联的数据不适用则不查询,也称为按需查询。
如进行一对多查询时,只查询出一方,当程序需要多方的数据时,mybatis在发出SQL语句进行查询,这样就可以减少数据库的压力。
mybatis延迟加载的条件:resultMap实现高级映射,如使用association、collection实现一对一以及一对多映射;关联对象的查询时子查询,多表连接查询则不行。
延迟加载好处:先从单表查询,需要时在从关联表查询,大大提高了数据库的性能。
延迟加载的原理:
本质是通过动态代理的方式,创建目标对象的代理对象,调用目标方法时(getting方法),进入拦截器方法,⽐如调⽤ a.getB().getName() ⽅法,进⼊拦截器的 invoke(...) ⽅法,发现 a.getB() 需要延迟加载时(为null时),那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调⽤ a.setB(b) ⽅法,于是 a 对象 b 属性就有值了,接着完成 a.getB().getName() ⽅法的调⽤。这就是延迟加载的基本原理。
5.9 mybatis的缓存
mybatis提供了对缓存的支持 ,分为一级缓存和二级缓存。
- 一级缓存是SqlSession级别的缓存,操作数据库时需要构造SqlSession对象,一级缓存主要是SqlSession内部共享,不同SqlSession之间缓存互不影响
- 二级缓存是namespace级别的缓存,多个SqlSession去操作同一个mapper中的SQL语句时,可以共用二级缓存。
5.9.1 一级缓存
SqlSession是一个接口,提供了一些CRUD的方法,其默认实现类为DefaultSqlSession,DefaultSqlSession 类持有 Executor 接口对象,而 Executor 的默认实现是 BaseExecutor 对象,每个 BaseExecutor 对象都有一个 PerpetualCache 缓存,是一个hashMap。
用户发起查询时,根据当前执行语句生成MappedStatement,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后返回结果给用户。
一级缓存配置:
<setting name="localCacheScope" value="SESSION"/>
value两个选项
SESSION 默认级别 一个 MyBatis 会话中执行的所有语句,都会共享这一个缓存
STATEMENT,可以理解为缓存只对当前执行的这一个 Statement 有效。
一级缓存总结:
- 一级缓存生命周期和SqlSession一致
- 一级缓存结构是一个hashmap
- 一级缓存缓存的是对象(user1 == user2 会打印为true,代码里对user1属性修改,会影响user2,但是不会影响数据库。)
- 一个SqlSession内部的更新操作会导致一级缓存失效
- 一级缓存适用范围为一个SqlSession 内部,分布式环境下或者有多个SqlSession 时,其他SqlSession 的写操作会导致脏数据,建议缓存级别设置为statement
5.9.2 二级缓存
如果多个SqlSession需要共享缓存,则需要使用到二级缓存,二级缓存默认是不开启的,开启之后,会使用CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询
二级缓存数据存储的介质不一定在内存中,也有可能在硬盘中,从硬盘读取的时候需要反序列化,因此相关pojo类都需要实现Serializable 序列化接口
二级缓存总结:
- 二级缓存是 namespace 级别,实现了 SqlSession 之间缓存数据的共享
- 二级缓存缓存的是数据(user1 == user2 会打印为false)
- 其他SqlSession的更新操作,会导致二级缓存失效,从而当前SqlSession查询会走数据库
- 多表查询时,极大可能出现脏数据,安全使用条件苛刻(通常我们会为每个单表创建单独的映射文件,由于 MyBatis 的二级缓存是基于 namespace 的,多表查询语句所在的 namspace 无法感应到其他 namespace 中的语句对多表查询中涉及的表进行的修改,引发脏数据问题)
mybatis缓存总结:
实际生产环境一般都会关闭mybatis的缓存,因为这个缓存真的很鸡肋啊。
对于一级缓存:多个SqlSession或者分布式环境,容易产生脏数据,增删改操作多的服务,清除一级缓存也很麻烦
对于二级缓存:虽然解决了SqlSession之间缓存数据共享问题,但是多表查询情况,不同namespace之间无法感应到变化,引发脏数据,基于cache ref 解决的话导致缓存粒度变粗,并且增加了开发成本。
因此,直接使用redis等分布式缓存成本会更低,安全性也会更高。
5.10 mybatis如何将SQL执行结果封装为目标对象并返回的?有哪些映射方式?
- 使用resultMap标签,column对应表字段,property对应对象的属性名,并将定义的resultMap标签作为select标签的 resultMap 属性赋值进去
- 使用sql列的别名功能,将列的别名书写为对象属性名。
mapper 文件:
<!-- 创建 resultMapid:自定义的唯一名称,在<select>使用type:期望转为的 java 对象的全限定名称或别名 -->
<resultMap id="studentMap" type="com.bjpowernode.domain.Student">
<!-- 主键字段使用 id -->
<!-- column对应表字段,property对应对象的属性名 -->
<id column="id" property="id" />
<!--非主键字段使用 result-->
<result column="name" property="name"/>
<result column="email" property="email" />
<result column="age" property="age" />
</resultMap>
<!--resultMap: resultMap 标签中的 id 属性值-->
<select id="selectUseResultMap" resultMap="studentMap">
select id,name,email,age from student where name=#{queryName} or age=#{queryAge}
</select>
5.11 mybatis动态SQL的用处?
根据查询条件动态完成SQL的拼接,实现SQL的复用
执行原理是根据表达式的值,完成逻辑判断,并拼接SQL。
5.12 如何获取自动生成的主键值?主键回填?
<insert id=”insertname” usegeneratedkeys=”true” keyproperty=”id”>
insert into names (name) values (#{name})
</insert>
5.13 在mapper中如何传递多个参数?
- 用#{arg0}、#{arg1}...传递参数,但是要注意顺序不能错,一般不用
- 用param注解
- 用map
- 用Javabean
public interface usermapper {
user selectuser(@param(“username”) string username,
@param(“hashedpassword”) string hashedpassword);
}
5.14 一对一,一对多查询
1、一对一关联查询 association
<select id="getClass" parameterType="int" resultMap="ClassesResultMap">
select * from class c,teacher t where c.teacher_id=t.t_id and c.c_id=#{id}
</select>
<resultMap type="com.lcb.user.Classes" id="ClassesResultMap">
<!-- 实体类的字段名和数据表的字段名映射 -->
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" javaType="com.lcb.user.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
</association>
</resultMap>
2、一对多关联查询 collection
<resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" javaType="com.lcb.user.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
</association>
<collection property="student" ofType="com.lcb.user.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
</collection>
</resultMap>
5.15 一对一有几种查询方式,怎么操作的
1、联合查询,见5.14,在resultMap中配置association节点
2、子查询,先查一个表,根据这个表的结果的外键id,在去查另外一个表的数据。也是通过association配置,但是另一个表的查询通过select属性配置,而不是写在SQL中。
<association property="teacher" javaType="com.lcb.user.Teacher" select="子查询id"/>
一对多查询,就是把association节点变成collection节点
5.16 什么是mybatis的接口绑定?有哪些实现方式?
接口绑定,就是在mybatis中任意定义接口,然后把接口中的方法和SQL进行绑定,我们直接调用接口方法就可以。
接口绑定有两种实现方式
- 注解绑定,在接口的方法上添加@select,@update等注解,适合简单的SQL
- xml绑定,指定xml映射文件的namespace为接口全路径名,适合较为复杂的SQL
5.17 mybatis 和 hibernate有哪些不同?
- mybatis
- 半ORM框架,需要自己编写SQL
- 直接编写原生态SQL,可以严格控制SQL执行,灵活度高
- 无法做到数据库无关性,实现支持多种数据库软件需要自定义多套SQL映射文件
- hibernate
- 全orm框架,无需编写SQL
- 对象/关系映射能力强
- 数据无关性好,对关系模型要求高的软件,使用hibernate开发可以节省代码提高效率。