MyBatis
定义
它是一款半自动的ORM持久层框架,具有较高的SQL灵活性,支持高级映射(一对一,一对多),动态SQL,延迟加载和缓存等特性,但它的数据库无关性较低
**ORM: **Object Relation Mapping,对象关系映射。对象指的是Java对象,关系指的是数据库中的关系模型,对象关系映射,指的就是在Java对象和数据库的关系模型之间建立一种对应关系,比如用一个Java的Student类,去对应数据库中的一张student表,类中的属性和表中的列一一对应。Student类就对应student表,一个Student对象就对应student表中的一行数据
半自动: 用mybatis进行开发,需要手动编写SQL语句。而全自动的ORM框架,如hibernate,则不需要编写SQL语句。
优点
-
灵活性高,自由地对sql进行定制
-
但相比JDBC,它提供了输入映射和输出映射,可以很方便地进行SQL参数设置,以及结果集封装。
-
还提供了关联查询和动态SQL等功能,极大地提升了开发的效率。
缺点
当要切换数据库时,SQL语句可能就要重写,因为不同的数据库有不同的方言(Dialect),所以mybatis的数据库无关性低。
占位符
#{}
- #{}在mybatis中,最后会被解析为?,其实就是Jdbc的PreparedStatement中的?占位符,它有预编译的过程
- 预编译
- 它有预编译的过程,会对输入参数进行类型解析(如果入参是String类型,设置参数时会自动加上引号),可以防止SQL注入
- 注意
- 如果parameterType属性指定的入参类型是简单类型的话(简单类型指的是8种java原始类型再加一个String),#{}中的变量名可以任意
- 如果入参类型是pojo,比如是Student类,那么#{name}表示取入参对象Student中的name属性,#{age}表示取age属性
${}
${},一般会用在模糊查询的情景,比如SELECT * FROM student WHERE name like '%${name}%'
;- 处理阶段在#{}之前,它不会做参数类型解析,而仅仅是做了字符串的拼接,
- 入参的Student对象的name属性为zhangsan,则上面那条SQL最终被解析为SELECT * FROM student WHERE name like ‘%zhangsan%’;
- 模糊查询只能用${}
- 对于简单类型(8种java原始类型再加一个String)的入参,${}中参数的名字必须是value
使用过程
原生流程
- 编写mapper.xml,书写SQL,并定义好SQL的输入参数,和输出参数
- 编写全局配置文件,配置数据源,以及要加载的mapper.xml文件
- 通过全局配置文件,创建SqlSessionFactory
- 每次进行CRUD时,通过SqlSessionFactory创建一个SqlSession
- 调用SqlSession上的selectOne,selectList,insert,delete,update等方法,传入mapper.xml中SQL标签的id,以及输入参数
- 提交sqlSession.commit()
- 关闭资源sqlSession.close()
基于Mapper代理
为了简化开发,mybatis提供了mapper接口代理的开发方式,不需要再编写dao类,只需要编写一个mapper接口,一个mapper的接口和一个mapper.xml相对应,只需要调用SqlSession对象上的getMapper(),传入mapper接口的class信息,即可获得一个mapper代理对象,直接调用mapper接口中的方法,即相当于调用mapper.xml中的各个SQL标签,此时就不需要指定SQL标签的id字符串了,mapper接口中的一个方法,就对应了mapper.xml中的一个SQL标签
注意
- mapper接口的全限定名,要和mapper.xml的namespace属性一致
- mapper接口中的方法名要和mapper.xml中的SQL标签的id一致
- mapper接口中的方法入参类型,要和mapper.xml中SQL语句的入参类型一致
- mapper接口中的方法出参类型,要和mapper.xml中SQL语句的返回值类型一致
使用方法
- 全局配置文件
<!-- 普通加载xml -->
<mappers> <mapper resource="StudentMapper.xml"/>
</mappers>
-
mapper.java
-
mapper.xml
-
测试调用
基于注解的开发
还是得有一个全局配置的xml文件,不过mapper.xml就可以省掉了
注意
当使用注解开发时,若需要传入多个参数,可以结合@Param注解
-
@Param标签会被mybatis处理并封装成一个Map对象,比如上面的示例中,实际传入的参数是一个Map对象,@Param标签帮忙向Map中设置了值,即它做了
-
Map<String,Object> map = new HashMap<>(); map.put("name", name); map.put("major",major);
步骤
- 创建一个Mapper接口
<!-- 在mapper接口中使用注解 -->
<mappers> <mapper class="com.yogurt.mapper.PureStudentMapper"/>
</mappers>
- 在全局配置文件中修改标签,直接指定加载这个类
三种加载Mapper的方式
-
<!-- 普通加载xml --> <mappers><mapper resource="StudentMapper.xml"/> </mappers>
-
<!-- 在mapper接口中使用注解 --> <mappers><mapper class="com.yogurt.mapper.PureStudentMapper"/> </mappers>
-
<!-- 使用<package>标签 --> <mappers><package name="com.yogurt.mapper"/> </mappers>
- 自动加载com.yogurt.mapper包下的所有mapper
- 这种方式需要将mapper接口文件和mapper.xml文件都放在com.yogurt.mapper包下,且接口文件和xml文件的文件名要一致。
- 若mapper接口采用注解方式,则不需要xml;若mapper接口没有采用注解方式,则mapper接口和xml文件的名称要相同,且在同一目录
注意
是因为,对于src/main/java 源码目录下的文件,maven打包时只会将该目录下的java文件打包,而其他类型的文件都不会被打包进去,去工程目录的target目录下看看maven构建后生成的文件
解决
需要在pom.xml中的 标签下 添加 标签,指定打包时要将xml文件打包进去
<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes></resource></resources> </build>
运行流程
流程
- 利用Xpath语法解析全局配置文件
- 解析全局配置,
- 是否开启二级缓存
- 是否开启延迟加载
- 是否使用插件(plugin)
- 是否设置类型别名(typeAlias)
- 数据库连接信息
- 解析mapper映射文件,将每个CRUD标签,封装为一个MappedStatement
- 所有的信息封装到Configuration对象(这是一个重量级对象)
- 将Configuration封装到SqlSessionFactory中
- 每次调用SqlSessionFactory开启一个SqlSession
- 每个SqlSession,会持有一个私有的Executor对象,以及共享的Configuration对象
- 每次操作,选取一个MappedStatement,由Executor执行。包括SQL语句组装,参数解析,返回结果解析等操作
图示
关键类
-
Configuration
封装了所有的属性
-
MappedStatement
一个CRUD标签,对应一个MappedStatement
-
Executor体系
mybatis核心执行器,通过Executor来执行数据库操作
-
SqlSource体系
封装CRUD标签的一个SqlSource,用于组装SQL语句
-
SqlNode体系
以树状形式,存储动态SQL标签,一个SqlSource拥有一个rootSqlNode,每个SqlNode有一个apply方法,在Executor执行时用于拼接SQL语句,SqlNode的设计用到的是设计模式中的组合模式。
-
StatementHandler
生成Statement,设置参数,执行查询
-
ParameterHandler
设置参数时,进行参数解析,类型转换等
-
ResultSetHandler
获取结果集,并进行结果封装
MyBatis缓存
定义
mybatis缓存,将数据库的查询结果,保存到内存(或者硬盘),以便下次执行相同查询时,不经过数据库,直接从内存中取出结果
作用
对于重复的查询,它能够提高响应速度,并减轻数据库的访问压力。适用于对响应时间要求高,而数据实时性要求不高的情况下。
分类
一级缓存
作用范围
- SqlSession(默认)
持有者
- BaseExecutor
默认开启
- 其实是无法关闭的,但可以通过一些方法来使一级缓存失效
清除缓存
- 在同一个SqlSession里执行 增 删 改 操作时(不必提交),会清除一级缓存
- SqlSession提交或关闭时(关闭时会自动提交),会清除一级缓存
- 对mapper.xml中的某个CRUD标签设置属性flushCache=true (这样会导致该标签的一级缓存和二级缓存都失效)
- 在全局配置文件中设置 ,这样会使一级缓存失效,二级缓存不受影响
二级缓存
作用范围
- mapper级别(可以跨SqlSession),一个mapper.xml即对应一个二级缓存,每个二级缓存,以mapper文件的namespace作为唯一标识
持有者
- MappedStatement
默认关闭
- (其实全局配置文件中的cacheEnabled默认是true,这个是二级缓存总开关,默认是已经打开了的,然而必须要在mapper.xml中配置 标签,才能开启该mapper.xml的二级缓存)
开启效果
- 映射语句文件中的所有SELECT 语句将会被缓存。
- 映射语句文件中的所有时INSERT 、UPDATE 、DELETE 语句会刷新缓存。
- 缓存会使用Least Rece ntly U sed ( LRU ,最近最少使用的)算法来收回。
- 根据时间表(如no Flush Int erv al ,没有刷新间隔),缓存不会以任何时间顺序来刷新。
- 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024 个引用。
- 缓存会被视为read/write(可读/可写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
注意
- 对于SELECT节点对应的MappedStatement,默认是开启二级缓存,对其他节点(INSERT/DELETE/UPDATE),默认是关闭(这是由MappedStatement里的useCache属性控制的)
- 放入二级缓存的数据,默认要实现Serializable接口,因为二级缓存的存储介质除了内存,还可能存在硬盘,所以存储的对象需要可序列化。
- readOnly=false:通过序列化,返回缓存对象的一份拷贝,速度上会慢一些,但是更加安全。用户取得数据后,执行修改,并不会影响到二级缓存中的对象
- 这些对象可以随意修改,不会影响后续相同查询条件的结果。这会慢一些,但是安全,因此默认是false。
- 当然,若把二级缓存的readOnly属性设为true,则对象数据可以不用实现Serializable接口
- readOnly=true:返回缓存对象的引用。
- 因此返回的这些缓存对象自己没事不要去修改。一旦修改,就会影响下一个相同查询条件的查询结果。这提供了很重要的性能优势。
- readOnly=true意在告诉用户,从缓存中取出数据后,不要对数据进行修改,而不是保证缓存的只读性(cache中存的是对象引用,取出数据后若执行修改,则会改变真正的对象,另外的用户再从cache中取对象,则会发现对象已经被修改)
- 执行一次查询后,需要进行提交,该次查询结果才会保存到二级缓存中
存在问题
- 由于是每个namespace对应一个二级缓存(一个namespace就是一个mapper.xml)
- 若mapperA.xml中全是对user表的操作,而在另一个mapperB.xml中也有少许对user表的操作,这2个mapper的二级缓存是互相独立的,然而mapperB若对user表执行了增删改,并提交,却不会刷新到mapperA的二级缓存,此时用mapperA去做查询,则可能取到脏数据。
应用场景
主键返回
在插入一条记录时,我们不设置其主键id,而让数据库自动生成该条记录的主键id,那么在插入一条记录后,如何得到数据库自动生成的这条记录的主键id呢
两种方式
- 使用useGeneratedKeys和keyProperty属性
<insert id="insert" parameterType="com.yogurt.po.Student" useGeneratedKeys="true" keyProperty="id">INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});
</insert>
- 使用子标签
<insert id="insert" parameterType="com.yogurt.po.Student">INSERT INTO student (name,score,age,gender) VALUES (#{name},#{score},#{age},#{gender});<selectKey keyProperty="id" order="AFTER" resultType="int" >SELECT LAST_INSERT_ID();</selectKey>
</insert>
标签其实就是一条SQL,这条SQL的执行,可以放在主SQL执行之前或之后,并且会将其执行得到的结果封装到入参的Java对象的指定属性上。
注意子标签只能用在和标签中。上面的LAST_INSERT_ID()实际上是MySQL提供的一个函数,可以用来获取最近插入或更新的记录的主键id。
批量查询
主要是动态SQL标签的使用,注意如果parameterType是List的话,则在标签体内引用这个List,只能用变量名list,如果parameterType是数组,则只能用变量名array
mapper定义
<select id="batchFind" resultType="student" parameterType="java.util.List">SELECT * FROM student<where><if test="list != null and list.size() > 0">AND id in<foreach collection="list" item="id" open="(" separator="," close=")">#{id}</foreach></if></where>
</select>
传参
List<Student> students = mapper.batchFind(Arrays.asList(1, 2, 3, 7, 9));
动态sql
可以根据具体的参数条件,来对SQL语句进行动态拼接。
注意
由于不确定查询参数是否存在,许多人会使用类似于where 1 = 1 来作为前缀,然后后面用AND 拼接要查询的参数,这样,就算要查询的参数为空,也能够正确执行查询,如果不加1 = 1,则如果查询参数为空,SQL语句就会变成SELECT * FROM student where ,SQL不合法。
- if
<select id="find" resultType="student" parameterType="student">SELECT * FROM student WHERE age >= 18<if test="name != null and name != ''">AND name like '%${name}%'</if>
</select>
- choose
<select id="findActiveBlogLike"resultType="Blog">SELECT * FROM BLOG WHERE state = ‘ACTIVE’<choose><when test="title != null">AND title like #{title}</when><when test="author != null and author.name != null">AND author_name like #{author.name}</when><otherwise>AND featured = 1</otherwise></choose>
</select>
-
where
标签只会在至少有一个子元素返回了SQL语句时,才会向SQL语句中添加WHERE,并且如果WHERE之后是以AND或OR开头,会自动将其删掉
<select id="findActiveBlogLike"resultType="Blog">SELECT * FROM BLOG<where><if test="state != null">state = #{state}</if><if test="title != null">AND title like #{title}</if><if test="author != null and author.name != null">AND author_name like #{author.name}</if></where>
</select>
-
trim
标签可以用标签代替
<trim prefix="WHERE" prefixOverrides="AND | OR">...
</trim>
-
foreach
用来做迭代拼接的,通常会与SQL语句中的IN查询条件结合使用,注意,到parameterType为List(链表)或者Array(数组),后面在引用时,参数名必须为list或者array。如在foreach标签中,collection属性则为需要迭代的集合,由于入参是个List,所以参数名必须为list
<select id="batchFind" resultType="student" parameterType="list">SELECT * FROM student WHERE id in<foreach collection="list" item="item" open="(" separator="," close=")">#{item}</foreach>
</select>
-
sql
可将重复的SQL片段提取出来,然后在需要的地方,使用标签进行引用
<select id="findUser" parameterType="user" resultType="user">SELECT * FROM user<include refid="whereClause"/>
</select><sql id="whereClause"><where><if test="user != null">AND username like '%${user.name}%'</if></where>
</sql>
-
bind
mybatis的动态SQL都是用OGNL表达式进行解析的,如果需要创建OGNL表达式以外的变量,可以用bind标签
<select id="selectBlogsLike" resultType="Blog"><bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />SELECT * FROM BLOGWHERE title LIKE #{pattern}
</select>
关联查询
使用 标签以及和 子标签,进行关联查询,
延迟加载
定义
在关联查询时,利用延迟加载,先加载主信息。需要关联信息时再去按需加载关联信息
优点
这样会大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
用法
在mybatis中,resultMap标签 的association标签和collection标签具有延迟加载的功能。
原理
开启前
- 第一种方法:我们直接关联查询出所有订单和用户的信息
- select * from orders o ,user u where o.user_id = u.id;
开启后
- 第二种方法:分步查询,首先查询出所有的订单信息,然后如果需要用户的信息,我们在根据查询的订单信息去关联用户信息
- select * from orders;
- select * from user where id=user_id
逆向工程
mybatis官方提供了mapper自动生成工具mybatis-generator-core来针对单表,生成PO类,以及Mapper接口和mapper.xml映射文件。针对单表,可以不需要再手动编写xml配置文件和mapper接口文件了,非常方便。美中不足的是它不支持生成关联查询。一般做关联查询,就自己单独写SQL就好了。