写在前面
上一篇公众号《梳理 Mybatis(一)》大致介绍了Mybatis的概念、顺带回顾了Jdbc的操作、开发Dao的方法、SqlSessionFactory、以及mybatis全局配置文件主要的参数使用。有兴趣的小伙伴们可以点进去看一看。
今天主要来说说Mybatis具体映射,日志以及在实际开发中用需要注意的点。
目录结构
XML 映射文件
MyBatis 的真正强大在于它的映射语句,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 为聚焦于 SQL 而构建,以尽可能地为你减少麻烦。
cache – 对给定命名空间的缓存配置。 cache-ref – 对其他命名空间缓存配置的引用。 resultMap– 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。 sql – 可被其他语句引用的可重用语句块。 insert – 映射插入语句 update – 映射更新语句 delete – 映射删除语句 select – 映射查询语句 |
自动生成主键
如果你的数据库支持自动生成主键的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把keyProperty设置到目标属性上就 OK 了。
<insert id="insertAuthor" useGeneratedKeys="true"keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>
对于不支持自动生成类型的数据库或可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方法来生成主键。
--------->keyProperty:将查询到主键设置到parameterType指定对象的哪个属性中---->User-->id将插入数据的主键返回,返回到user对象中。
//(数据表中的 自动递增 勾上)
<insert id="insertUser" parameterType="mydata.User">
<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
select last_insert_id()
</selectKey>
//获取 最后一次设置的id
Insert into user(name,birthday,sex,address) values(#{name},#{birthday},#{sex},#{address})
</insert>
输出映射
resultType
使用resultType进行输出映射,只有查询出来的列名和pojo中的属性名一致,该列才可以映射成功。
<!-- mybatis-config.xml 中 -->
<typeAlias type="com.someapp.model.User" alias="User"/>
<!-- SQL 映射 XML 中 -->
<select id="selectUsers" resultType="User">
select id, username, hashedPassword from some_table where id = #{id}
</select>
MyBatis会在幕后自动创建一个 ResultMap,再基于属性名来映射列到 JavaBean 的属性上。
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
而在引用它的语句中使用 resultMap 属性就行了。
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table where id = #{id}
</select>
2. resultMap
mybatis中使用resultMap完成高级输出结果映射。如果查询出来的列名和pojo的属性名不一致,通过定义一个resultMap对列名和pojo属性名之间作一个映射关系。
<resultMap type="my.UserOrders" id="uq1">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="birthday" property="birthday"/>
<result column="sex" property="sex"/>
<result column="address" property="address"/>
<!-- 集合映射 一对多 “多” -->
<collection property="orders" ofType="my.Orders">
<id column="oid" property="id"/>
<result column="user_id" property="user_id"/>
<result column="number" property="number"/>
<result column="createtime" property="createtime"/>
<result column="note" property="note"/>
</collection>
<!-- 联合查询 一对一 的映射-->
<association property="items" javaType="pojo.Items">
<id column="iid" property="id"/>
<result column="name" property="name"/>
<result column="price" property="price"/>
<result column="detail" property="detail"/>
<result column="pic" property="pic"/>
<result column="createtime" property="createtime"/>
</association>
</resultMap>
<select id="UserOrders" resultMap="uq1">
Select user.*,orders.id oid,orders.user_id,orders.number,orders.createtime,orders.note
from user,orders
where user.id=orders.user_id
</select>
3. resultMap元素的概念视图。
constructor - 用于在实例化类时,注入结果到构造方法中 idArg- ID 参数;标记出作为 ID 的结果可以帮助提高整体性能 arg- 将被注入到构造方法的一个普通结果 id– 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能 result– 注入到字段或 JavaBean 属性的普通结果 association – 一个复杂类型的关联;许多结果将包装成这种类型 嵌套结果映射 – 关联本身可以是一个resultMap 元素,或者从别处引用一个 collection – 一个复杂类型的集合 嵌套结果映射 – 集合本身可以是一个 resultMap 元素,或者从别处引用一个 discriminator – 使用结果值来决定使用哪个resultMap case – 基于某些值的结果映射 嵌套结果映射 –case 本身可以是一个 resultMap 元素,因此可以具有相同的结构和元素,或者从别处引用一个 |
构造方法
构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入。constructor 元素就是为此而生的。
public class User {
//...
public User(Integer id, String username, int age) {
//...
}
//...
}
为了将结果注入构造方法,MyBatis 需要通过某种方式定位相应的构造方法。
<constructor>
<idArg column="id" javaType="int" name="id" />
<arg column="age" javaType="_int" name="age" />
<arg column="username" javaType="String" name="username" />
</constructor>
discriminator鉴别器
它很像Java 语言中的 switch 语句。
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultMap="carResult"/>
<case value="2" resultMap="truckResult"/>
<case value="3" resultMap="vanResult"/>
<case value="4" resultMap="suvResult"/>
</discriminator>
MyBatis 会从结果集中得到每条记录,然后比较它的 vehicle_type值。如果它匹配任意一个鉴别器的 case,就会使用这个 case 指定的结果映射。cars 和 vehicles 之间有关系,也就是 Car 是一个 Vehicle。因此,我们希望剩余的属性也能被加载。而这只需要一个小修改。
//extends="vehicleResult"
<resultMap id="carResult" type="Car" extends="vehicleResult">
<result property="doorCount" column="door_count" />
</resultMap>
缓存
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行。<cache/>
映射语句文件中的所有 select 语句的结果将会被缓存。
映射语句文件中的所有 insert、update和 delete 语句会刷新(清空)缓存。
缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
缓存不会定时进行刷新(也就是说,没有刷新间隔)。
缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用@CacheNamespaceRef 注解指定缓存作用域。
为什么需要使用缓存
如果在极短时间内,做相同的查询,那么它们的结果很可能是相同,而访问一次数据库是非常消耗资源的。如果有一层缓存,将极大减少资源的消耗。
Mybatis的一级缓存
mybatis的一级缓存是SqlSession,其内部是通过一个hashmap来实现的,hashmap的键是数据库记录的主键。每个SqlSession之间是相互独立的。
Mybatis的一级缓存的生命周期
MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
√如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
√如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用。
√SqlSession中执行了任何一个update操作(update()、delete()、insert()),都会清空PerpetualCache对象的数据,但是该对象可以继续使用。Ps:数据进行了增删改,那么这个map缓存就会被清空了,避免下次查询的时候脏读(就是数据被改掉了,你再从缓存中读的话,数据是不对的)
Mybatis的一级缓存的查询流程
①对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。
②判断从Cache中根据特定的key值取的数据是否为空,即是否命中。
③如果命中,则直接将缓存结果返回。
③如果没命中:
1.去数据库中查询数据,得到查询结果
2.将key和查询到的结果分别作为key,value对存储到Cache中
3.将查询结果返回
Mybatis的二级缓存
如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:
二级缓存 ———> 一级缓存——> 数据库
如果二级缓存中没有,查询一级缓存,如果在一级缓存中有,将查询的结果存入二级缓存,并换回给用户,如果没有查询数据库,并一级一级的返回存入缓存,在返回给用户。 Mybatis二级缓存的配置
MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。
MyBatis要求返回的POJO必须是可序列化的。也就是要求实现Serializable接口。
①MyBatis支持二级缓存的总开关:全局配置变量参数 cacheEnabled=true
<configuration> <settings> <!--这个配置使全局的映射器(二级缓存)启用或禁用缓存--> <setting name="cacheEnabled" value="true" /> ..... </settings> .... </configuration>
②该select语句所在的Mapper,配置了<cache> 或<cached-ref>节点,才有效。
<!--开启本mapper的namespace下的二级缓存--> <!-- eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。 (1) LRU,最近最少使用的,一处最长时间不用的对象 (2) FIFO,先进先出,按对象进入缓存的顺序来移除他们 (3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象 (4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,移除最长时间不用的对形象 flushInterval:刷新间隔时间,单位为毫秒,这里配置的是100000秒刷新,如果你不配置它,那么当SQL被执行的时候才会去刷新缓存。 size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。 这里配置的是1024个对象 readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有 办法修改缓存,他的默认值是false,不允许我们修改 --> <cache eviction="LRU" flushInterval="100000" size="1024" readOnly="true"></cache> <resultMap type="result3.OrdersOrderDetail" id="uq3"> <id column="oid" property="id"/> <result column="username" property="name"/> <collection property="details" ofType="result3.DetailItems"> <id column="odid" property="id"/> <result column="orders_id" property="orders_id"/> <result column="items_id" property="items_id"/> <result column="items_num" property="items_num"/> <association property="items" javaType="pojo.Items"> <id column="iid" property="id"/> <result column="name" property="name"/> <result column="price" property="price"/> <result column="detail" property="detail"/> <result column="pic" property="pic"/> <result column="createtime" property="createtime"/> </association> </collection> </resultMap> <!--可以通过设置useCache来规定这个sql是否开启缓存,ture是开启,false是关闭 刷新二级缓存 flushCache="true"--> <select id="ordersItems" resultMap="uq3" useCache="true" flushCache="true"> select user.username, orders.id oid, orderdetail.id odid,orderdetail.orders_id,orderdetail.items_id,orderdetail.items_num, items.id iid,items.name,items.price,items.detail,items.pic,items.createtime from orders,user,orderdetail,items where user.id=orders.user_id and orders.id=orderdetail.orders_id and orderdetail.items_id=items.id <!-- <where> <if test="value!=null and value!=''"> and orders.id=#{value} and user.id=orders.user_id and orders.id=orderdetail.orders_id and orderdetail.items_id=items.id </if> </where> --> </select>
动态sql
if
choose (when, otherwise)
trim (where, set)
foreach
if
这个元素可以被用来定义可重用的 SQL代码段,这些 SQL 代码可以被包含在其他语句中对sql进行灵活拼接、组装。
choose, when, otherwise
MyBatis 提供了 choose 元素,它有点像Java 中的 switch语句。
提供了“title”就按“title”查找,提供了“author”就按“author”查找的情形,若两者都没有提供,就返回所有符合条件的 BLOG。
<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>
trim(where, set)
①where:查找
至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句。若语句的开头为“AND”或“OR”,where 元素也会将它们去除。(若只有一个满足 则把满足地那一个最前面的符号去除)
避免:
SELECT * FROM BLOG
WHERE
AND title like‘someTitle’
<where>
//test里面 name sex都是来自包装好的类里面 的字段
//来自 parameterType="my.Q1"里的成员变量
<if test="name!=null andname!=''">
and user.id=orders.user_id
and username like '%${name}%'
</if>
<if test="sex!=null andsex!=''">
and sex=#{sex}
</if>
</where>
②trim:添加
通过自定义 trim 元素来定制 where 元素的功能。和 where 元素等价。
//prefixOverrides=”AND” 去除多余sql前面的AND
//suffixOverrides=”,” 去除sql语句结尾多余的逗号.
prefix="(" 前缀的内容suffix=")"后缀的内容
===========和上面where 元素等价==================
<trimprefix="WHERE" prefixOverrides="AND">
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null andauthor.name != null">
AND author_name like #{author.name}
</if>
</trim>
③Set:更新
用于动态更新语句的解决方案叫做 set。set 元素可以用于动态包含需要更新的列,而舍去其它的。
<updateid="updateAuthorIfNecessary">
update Author
<set>
<if test="username !=null">username=#{username},</if>
<if test="password !=null">password=#{password},</if>
<if test="email !=null">email=#{email},</if>
<if test="bio !=null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
这里,set 元素会动态前置 SET 关键字,同时也会删掉无关的逗号。
foreach
向sql传递数组或List,mybatis使用foreach解析
<foreach collection="集合名或数组名"
item="本次迭代获取的元素-->值"
index="迭代次数(索引)-->键"
open="and id IN("
close=")"
separator=",分隔符">
</foreach>
*script
对于在带注释的映射器类中使用动态sql,可以使用script元素。
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);
bind
bind 元素可以从 OGNL 表达式中创建一个变量并将其绑定到上下文。
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
日志
日志工具
SLF4J
ApacheCommons Logging
Log4j2
Log4j
JDKlogging
它会使用第一个查找得到的工具(按上面列举的顺序查找)。如果一个都未找到,日志功能就会被禁用。如果你的应用部署在一个类路径已经包含 Commons Logging 的环境中,而你又想使用其它日志工具,你可以通过在MyBatis 配置文件 mybatis-config.xml里面添加一项 setting 来选择别的日志工具。
<configuration>
<settings>
...
<setting name="logImpl" value="LOG4J"/>
...
</settings>
</configuration>
logImpl可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING,或者是实现了接口org.apache.ibatis.logging.Log 的,且构造方法是以字符串为参数的类的完全限定名。
日志配置
再次说明下,具体怎么做,由使用的日志工具决定,这里以 Log4J 为例。配置日志功能非常简单。
步骤 1:添加 Log4J 的 jar 包
步骤 2:配置 Log4J
在应用的类路径中创建一个名称为 log4j.properties的文件,文件的具体内容如下:
# Global logging configuration
log4j.rootLogger=ERROR, stdout
# MyBatis logging configuration...
log4j.logger.org.mybatis.example.BlogMapper=TRACE
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
添加以上配置后,Log4J 就会记录 org.mybatis.example.BlogMapper的详细执行操作。
//将日志的记录方式从接口级别切换到语句级别(只对 selectBlog 语句记录日志)
log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE
//对一组映射器接口记录日志
log4j.logger.org.mybatis.example=TRACE
//Mybatis 中 SQL 语句的日志级别被设为DEBUG
log4j.logger.org.mybatis.example=DEBUG
。。。