第八章、Mybatis高级配置
1. MyBatis的映射文件高级
cache
– 该命名空间的缓存配置。cache-ref
– 引用其它命名空间的缓存配置。resultMap
– 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。sql
– 可被其它语句引用的可重用语句块。
1.1 SQL代码片段
该元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。
1.1.1 基础的SQL片段
通过<sql>
标签定义代码片段
<!--定义所有的列名-->
<sql id="fullColumns">id, title, description, pid, image</sql>
<!--定义一个基础查询-->
<sql id="baseSelect">
select
<include refid="fullColumns"></include>
from section
</sql>
通过<include>
标签引入定义的代码片段
<select id="selectByPrimaryKey" resultType="Section">
<include refid="baseSelect"></include>
where id = #{id}
</select>
<select id="selectAll" resultType="Section">
<include refid="baseSelect"></include>
</select>
1.1.2 带参数的SQL片段(了解)
<sql id="baseSectionSelect">
select
<include refid="fullColumns"></include>
from section where title like '%${key}%'
</sql>
<select id="selectAll" resultType="Section">
<include refid="baseSectionSelect">
<property name="key" value="区"/>
</include>
</select>
1.2 参数设置
参数的类型
原始类型(基本数据类型及包装类)
引用数据类型:简单类型(String,Date)和复杂类型(自定义类型-实体类)
参数的使用
#{参数名,javaType=int,jdbcType=NUMERIC,typeHandler=TypeHandler} 完整形式 #{参数名} 简写形式(最常用)
1.2.1 MyBatis的参数分类
简单类型:直接使用值的数据类型
/**
* 根据主键删除
* @param id
* @return
*/
int deleteByPrimaryKey(int id);
<!--简单的参数数据类型,通常省略掉parameterType由MyBatis的自动推断-->
<delete id="deleteByPrimaryKey" parameterType="int">
delete from section where id = #{id}
</delete>
复杂类型:使用属性值的数据类型
/**
* 添加一个数据到数据库中
* @param section
* @return 返回受影响的行数
*/
int insert(Section section);
<!--在SQL语句的拼接中,直接使用属性名获取值,parameterType可以省略由MyBatis的自动推断 -->
<insert id="insert" parameterType="Section"
useGeneratedKeys="true" keyProperty="id">
insert into section (id, title, description, pid, image)
values (default, #{title}, #{description}, #{pid}, #{image})
</insert>
1.2.2 多个参数
多个参数,mybatis在实现时,会使用list集合保存
参数在使用时,通过有序的名称来获取值
两种方式:
第一种:arg0, arg1,arg2, …
第二种:param1, param2, param3, …
List<Section> selectByTitleAndPid(String title, int pid);
<!--多个参数时,parameterType属性通常省略-->
<select id="selectByTitleAndPid" parameterType="list" resultType="Section">
select * from section where title = #{param1} and pid = #{param2}
</select>
使用注解给参数命名
使用 @Param(“名称”)
List<Section> selectByTitleAndPid(@Param("title") String title, @Param("pid") int pid);
<select id="selectByTitleAndPid" resultType="Section">
select * from section where title = #{title} and pid = #{pid}
</select>
注意事项 :一般情况下如果参数的个数超过3个,请使用对象封装。
1.2.3 字符串替换(面试中常问)
替换SQL语句中的特殊字符。
常见的特殊字符
#{ 参数名 }:会使用 ? 来替换,用于设置SQL语句的参数占位符
${ 参数名 }:会使用参数名对应的内容来替换,通常用于拼接SQL语句。(慎用 - SQL注入式攻击)
1.3 结果映射ResultMap
resultMap
元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets
数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap
能够代替实现同等功能的数千行代码。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
1.3.1 直接映射Map
会把数据库中查询的每一行数据映射成为一个Map,列名作为key,对应列的值作为value
实际开发尽量少用。
Map selectOneMap(int id);
List<Map> selectMap();
<select id="selectOneMap" resultType="map">
select * from section where id = #{id}
</select>
<select id="selectMap" resultType="map">
select * from section
</select>
1.3.2 使用ResultMap映射实体类
当属性名和列名不一致时,需要实现属性名和列名的映射
1.3.2.1 传统方式,使用SQL语句as别名
<select id="selectSections" resultType="Section" >
select
id, title, description, pid, image as image
from section
</select>
1.3.2.1 使用ResultMap标签,实现映射
<resultMap id="BaseMap" type="Section" autoMapping="true">
<!--实现主键映射-->
<id property="id" column="id"></id>
<!--实现其余属性映射(通常映射名称不一样的)-->
<result property="image" column="image"></result>
</resultMap>
<select id="selectSections" resultMap="BaseMap">
select
id, title, description, pid, image
from section
</select>
1.3.3 ResultMap的高级映射
映射有级联关系的对象
package com.xuetang9.mybatis.demo.entity;
import lombok.Data;
@Data
public class Customer {
private String account;
private String password;
private String name;
}
package com.xuetang9.mybatis.demo.entity;
import lombok.Data;
@Data
public class Topic {
private Integer id;
private Integer sid;
private String account;
private String title;
// 关联了一个Customer类型的对象属性
private Customer author;
}
<resultMap id="BaseResultMap" type="Topic" autoMapping="true">
<!--配置一个对象属性-->
<association property="author" javaType="Customer" columnPrefix="cus_">
<id property="account" column="account"></id>
<result property="name" column="name"></result>
<result property="password" column="password"></result>
</association>
</resultMap>
<select id="selectByPrimaryKey" resultMap="BaseResultMap">
select
t.*,
c.account as cus_account,
c.password as cus_password,
c.name as cus_name
from topic t
inner join customer c on t.account = c.account
where t.id = #{id}
</select>
映射有级联关系的集合
package com.xuetang9.mybatis.demo.entity;
import lombok.Data;
import java.util.List;
@Data
public class Section {
private Integer id;
private String title;
private String description;
private Integer pid;
private String image;
// 版块下面关联多个主贴
private List<Topic> topics;
}
package com.xuetang9.mybatis.demo.entity;
import lombok.Data;
public class Topic {
private Integer id;
private Integer sid;
private String account;
private String title;
private Customer author;
}
<resultMap id="BaseMap" type="Section" autoMapping="true">
<!--实现主键映射-->
<id property="id" column="id"></id>
<!--实现其余属性映射(通常映射名称不一样的)-->
<result property="image" column="image"></result>
<!--配置一个集合的信息-->
<collection property="topics" ofType="Topic" columnPrefix="t_">
<id property="id" column="id"></id>
<result property="title" column="title"></result>
</collection>
</resultMap>
<select id="selectByPrimaryKey" resultMap="BaseMap">
select
s.*,
t.id as t_id,
t.title as t_title
from section s
inner join topic t on s.id = t.sid
where s.id = #{id}
</select>
集合中含有复杂类型
注意sql语句的前缀中,需要给两次前缀
t_
表示Topic
,t_cus_
表示Topic中的Customer
<resultMap id="sectionMapTopicList" type="Section" autoMapping="true">
<!-- 需要注意父级对象的id需要显示的写出来。 可以进行唯一数据的区别 -->
<id property="id" column="id"/>
<collection property="topics" ofType="Topic" columnPrefix="t_">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<association property="author" javaType="Customer" columnPrefix="cus_">
<!-- 实际查找的列 cus_id -->
<id property="id" column="id"/>
<result property="account" column="account"/>
<result property="name" column="name"/>
</association>
</collection>
</resultMap>
<!-- 查询板块以及板块下的所有帖子 -->
<select id="selectWithTopicByPrimaryKey" resultMap="sectionMapTopicList">
select s.*,t.id as t_id,
t.title as t_title,
t.content as t_content,
c.id as t_cus_id,
c.account as t_cus_account,
c.name as t_cus_name
from section as s
inner join topic as t on t.section_id = s.id
inner join customer as c on c.id = t.customer_id
where s.id = #{sid}
</select>
1.4 MyBatis缓存
缓存:把物理介质中数据,读取到内存中,以便于后续操作的访问。针对查询。
MyBatis中的缓存分为两种:
第一种:一级缓存,又称为本地缓存,数据是缓存在SqlSession中。只要SqlSession关闭缓存就会清除。
第二种:二级缓存,可以使用 缓存
中间件
, 数据是缓存在SelSessionFactory级别。
1.4.1 一级缓存
List<Topic> selectLocalCache();
<select id="selectLocalCache" resultType="Topic">
select * from topic
</select>
@Test
public void selectLocalCache() {
// 演示一级缓存
List<Topic> topics = topicMapper. selectLocalCache();
// 手动清除缓存
sqlSession.clearCache();
topicMapper. selectLocalCache();
}
1.4.2 二级缓存
在映射文件中添加 <cache/>
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改
<?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">
<mapper namespace="com.xuetang9.mybatis.demo.mapper.TopicMapper">
<!--简单基础缓存-->
<cache/>
<resultMap id="BaseResultMap" type="Topic" autoMapping="true">
<!--配置一个对象属性-->
<association property="author" javaType="Customer" columnPrefix="cus_">
<id property="account" column="account"></id>
<result property="name" column="name"></result>
<result property="password" column="password"></result>
</association>
</resultMap>
<select id="selectByPrimaryKey" resultMap="BaseResultMap">
select
t.*,
c.account as cus_account,
c.password as cus_password,
c.name as cus_name
from topic t
inner join customer c on t.account = c.account
where t.id = #{id}
</select>
<select id="selectLocalCache" resultType="Topic">
select * from topic
</select>
</mapper>
1.4.2.1 自定义二级缓存
package com.xuetang9.mybatis.demo.cache;
import org.apache.ibatis.cache.Cache;
import java.util.HashMap;
import java.util.Map;
public class MyCache implements Cache {
/**
* 使用一个Map用来保存需要缓存的数据(可以换成 缓存数据库 Redis)
*/
private Map<Object,Object> cacheMap = new HashMap<>();
private String id;
public MyCache(String id){
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
System.out.println("缓存数据..............");
System.out.println(key);
System.out.println(value);
System.out.println("缓存数据..............");
// 保存对象到缓存中
cacheMap.put(key,value);
}
@Override
public Object getObject(Object key) {
System.out.println("取出数据");
// 从缓存中取出数据
return cacheMap.get(key);
}
@Override
public Object removeObject(Object key) {
// 从缓存中移除数据
return cacheMap.remove(key);
}
@Override
public void clear() {
// 清空缓存
cacheMap.clear();
}
@Override
public int getSize() {
// 获取缓存数据的条数
return cacheMap.size();
}
}
<cache type="com.xuetang9.mybatis.demo.cache.MyCache"/>
1.4.2.2 使用Redis实现自定义二级缓存
package com.xuetang9.framework.mybatisadvanceconfig.cache;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class CacheRedis implements Cache {
/**
* 大师兄外网IP
*/
private String host = "192.168.5.134";
/**
* Redis默认端口
*/
private int port = 6379;
private Jedis conn = new Jedis(host,port);
private String id;
/**
* 所有的键
*/
private List<Object> keys = new ArrayList<>();
public CacheRedis(String id){
this.id = id;
}
@Override
public String getId() {
return id;
}
/**
* 对象转化为字节数组
* @param obj
* @return
*/
public byte[] toByteArray(Object obj){
try(ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(byteOut);
){
objOut.writeObject(obj);
objOut.flush();
return byteOut.toByteArray();
}catch (IOException e){
e.printStackTrace();
}
return null;
}
/**
* 将字节数组转化为对象
* @param arr
* @return
*/
public Object byteArrayToObj(byte [] arr){
try(ByteArrayInputStream in = new ByteArrayInputStream(arr);
ObjectInputStream objIn = new ObjectInputStream(in);
){
return objIn.readObject();
}catch (IOException|ClassNotFoundException e){
e.printStackTrace();
}
return null;
}
@Override
public void putObject(Object key, Object value) {
if(key instanceof String && value instanceof String ){
conn.set((String)key,(String)value);
keys.add(key);
}else{
conn.set(toByteArray(key),toByteArray(value));
keys.add(toByteArray(key));
}
}
@Override
public Object getObject(Object key) {
if(key instanceof String){
return conn.get((String)key);
}else{
byte[] bytes = conn.get(toByteArray(key));
return byteArrayToObj(bytes);
}
}
@Override
public Object removeObject(Object key) {
if(key instanceof String){
conn.del((String)key);
keys.remove(key);
}else{
conn.del(toByteArray(key));
keys.remove(toByteArray(key));
}
return null;
}
@Override
public void clear() {
keys.forEach(key->{
if(key instanceof String){
conn.del((String)key);
}else{
conn.del((byte[])key);
}
});
}
@Override
public int getSize() {
return keys.size();
}
}
<cache type="com.xuetang9.framework.mybatisadvanceconfig.cache.CacheRedis" />
1.4.3 cache标签的属性
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
- eviction:缓存清除策略
LRU
– 最近最少使用:移除最长时间不被使用的对象。FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
- flushInterval:缓存刷新的间隔时间
- size:缓存的对象引用(变量地址)的个数,默认是1024
- readOnly:是否是只读缓存。
1.5 动态SQL语句
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。
1.5.1 动态SQL元素
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
- bind
1.5.1.1 if标签
/**
* 根据条件查询
* @param section
* @return
*/
List<Section> selectByCondition(Section section);
<select id="selectByCondition" parameterType="Section" resultMap="BaseMap">
select * from section where 1 = 1
<if test="id != null">
and id = #{id}
</if>
<if test="title != null and title != ''">
and title = #{title}
</if>
<if test="description != null">
and description = #{description}
</if>
<if test="pid != null">
and pid = #{pid}
</if>
<if test="image != null">
and image = #{image}
</if>
</select>
1.5.1.2 choose、when、otherwise
List<Section> selectByProperty(@Param("prop") String prop,@Param("value") Object value);
<select id="selectByProperty" resultType="Section">
select
id, title, description, pid, image
from section where 1 = 1
<choose>
<when test="prop == 'id'">
and id = #{value}
</when>
<when test="prop == 'title'">
and title = #{value}
</when>
<when test="prop == 'pid'">
and pid = #{value}
</when>
</choose>
</select>
1.5.1.3 trim、where、set
解决条件拼接的固定部分
<select id="selectByProperty" resultMap="BaseMap">
select
id, title, description, pid, image
from section
<where>
<choose>
<when test="prop == 'id'">
and id = #{value}
</when>
<when test="prop == 'title'">
and title = #{value}
</when>
<when test="prop == 'pid'">
and pid = #{value}
</when>
</choose>
</where>
</select>
<update id="update" parameterType="Section">
update section
<set>
<if test="title != null">
title = #{title},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="pid != null">
pid = #{pid},
</if>
<if test="image != null">
image = #{image},
</if>
</set>
where id = #{id}
</update>
<select id="selectByCondition" parameterType="Section" resultMap="BaseMap">
select * from section
<trim prefix="where" prefixOverrides="and | or">
<if test="id != null">
and id = #{id}
</if>
<if test="title != null and title != ''">
and title = #{title}
</if>
<if test="description != null">
and description = #{description}
</if>
<if test="pid != null">
and pid = #{pid}
</if>
<if test="image != null">
and image = #{image}
</if>
</trim>
</select>
1.5.1.4 foreach
collection属性,填写的是需要遍历的参数的名称
默认情况下:
- 数组类型的参数:名称是 array, arg0
- 集合类型的参数:名称是 list, param1
- 填写@Param中的名称
foreach在循环遍历大量的数据是:使用 ${} 比 #{} 速度要快。
使用${}是,只解析一次参数,就完成了SQL的生成。
使用#{}时,会先把#{}换成?号,然后再执行参数的设置。
<select id="selectByPrimarykeys" resultMap="BaseMap">
select * from section
where id in
<foreach collection="list" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</select>
@Test
public void selectByPrimarykeys() {
long count = 1000000;
List<Integer> list = new ArrayList<>();
for (int i = 0; i < count; i++) {
list.add(i);
}
long start = System.currentTimeMillis();
System.out.println("执行查询:" + start);
List<Section> sections = sectionMapper.selectByPrimarykeys(list);
long end = System.currentTimeMillis();
System.out.println("执行查询结束:" + end);
System.out.println("时间差:" + (end - start));
}
1.5.1.5 bind标签
创建一个变量,并将其绑定到当前的上下文
原始方式的模糊查询
@Test
public void selectLikeCondition() {
Section sectionCondition =new Section();
// 在代码中构建模糊查询的条件
sectionCondition.setTitle("%区%");
List<Section> sections = sectionMapper.selectLikeCondition(sectionCondition);
}
<select id="selectLikeCondition" parameterType="Section" resultMap="BaseMap">
select * from section
<where>
<if test="id != null">
and id = #{id}
</if>
<if test="title != null and title != ''">
and title like #{title}
</if>
<if test="description != null">
and description like #{description}
</if>
<if test="pid != null">
and pid = #{pid}
</if>
<if test="image != null">
and image like #{image}
</if>
</where>
</select>
使用bind标签改造
@Test
public void selectLikeCondition() {
Section sectionCondition =new Section();
sectionCondition.setTitle("区");
List<Section> sections = sectionMapper.selectLikeCondition(sectionCondition);
}
<select id="selectLikeCondition" parameterType="Section" resultMap="BaseMap">
select * from section
<where>
<if test="id != null">
and id = #{id}
</if>
<if test="title != null and title != ''">
<bind name="titlePattern" value=" '%'+title+'%' "/>
and title like #{titlePattern}
</if>
<if test="description != null">
and description like #{description}
</if>
<if test="pid != null">
and pid = #{pid}
</if>
<if test="image != null">
and image like #{image}
</if>
</where>
</select>
2. MyBatis的注解开发
使用Java API简化配置文件
常用注解
- @Insert
- @Delete
- @Update
- @Select
/**
* 查询全部
* @return
*/
@Select("select * from section")
@ResultMap("BaseMap")
List<Section> selectAll();
/**
* 根据条件查询
* @param section
* @return
*/
@Select({"<script>",
"select * from section",
" <where>",
" <if test='title != null'>and title=#{title}</if>",
" <if test='pid != null'>and pid=#{pid}</if>",
" <if test='id != null'>and id=#{id}</if>",
" <if test='image != null'>and image=#{image}</if>",
" </where>",
"</script>"})
@Results(id="BaseMap",value = {
@Result(id=true,property = "id", column = "id"),
@Result(property = "image", column = "image"),
})
List<Section> selectByCondition(Section section);