MyBatis+SpringMVC+SpringBoot

这下够清楚了吧!详解Mybatis的Mapper映射文件

2020-12-21  本文已影响0人  程序花生

前言

上节我们介绍了 《 Mybatis配置文件 XML 全貌详解》,内容很详细( 也很枯燥),由于篇幅实在过于冗长,我预计大家想看完得花上两段上班地铁公交车的时间 。。。

不过应该有让大家了解到 Mybatis 的核心配置文件 config.xml 全貌,其中的 元素即是我们本节准备登场介绍的 SQL 映射器,上节有介绍了三种引入 SQL 映射器的方式,本节我们就主要聊聊它的几个顶级元素用法。

Mybatis 真正强大就在于它的语句映射,这是它的魔力所在,也是基石。由于它异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码( 95% 是Mybatis 官网的说法 ,我也就引入一下 ),MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。

一、mapper 映射器顶级元素全貌

与其它 ORM 框架如 Hibernate 不同,Mybatis 的框架思想希望开发者能够直接操作数据库编写 SQL,而不是隐藏起来,让开发者独自面对 Java 对象,为此 Mybatis 设计了 SQL 映射器,任你五招十二式。

映射器有九大顶级元素 ,基本技能介绍

其中,增删改查操作拼接 SQL 时使用到的 动态SQL( if、where、foreach啥的),以及封装结果集时使用到的 复杂映射 (1对1 ,1对多,多对多啥的),这两部分我们后面单立文章再详细介绍,本文中我们简单点过。

九大顶级元素 ,功能归类:

其中顶一元素 parameterMap 已建议弃用了 。

无论你有多么复杂的 SQL 操作,最根本的思路都逃不出以上 4 部分。

二、namespace 命名空间

一个完整的 Mapper 映射文件,需要有约束头 xml 与 !DOCTYPE ,其次才是 mapper 根元素,最后再是顶级元素,而其中,namespace 属性作为 mapper 的唯一标识,试回忆:

每一段 SQL 语句都是唯一定义的,我们在 Mybatis 中用「 命名空间标识 + 语句块 ID 」作为唯一的标识,组合之后在 Mybatis 二级缓存中可以作为本地 map 集合 缓存 的唯一Key ,也可以用于 Dao 接口的 映射 绑定,还能作为唯一 代理 标识。总之,我们希望避免命名冲突和重复定义,所以,拥有这么一个唯一标识 ,它就至少有一亿个利好。

三、select 查询

select 查询语句,几乎是我们最高频的使用元素,所以 Mybatis 在这块没少下功夫,目的就是通过提供尽可能多的便利,让我们的查询操作变得简单。 一个查询用户 User 的查询语句可以这么编写:

<select id="selectUser" parameterType="int" resultType="hashmap">
  select * from t_user where id = #{id}
</select>

当然如果你不希望通过 hashmap 来接收查询结果,允许你自由指定返回类型。Mybatis 是支持自动绑定 JavaBean 的,我们只要让查询返回的字段名和 JavaBean 的属性名保持一致(或者采用驼峰式命名),便可以自动映射结果集,例如你创建一个 Java 类 User.java ,包含两个属性 id 和 name , 那么结果集可以指定为 com.vo.User ,就完成了。

<select id="selectUser" 
    parameterType="int" resultType="com.vo.User">
  select * from t_user where id = #{id}
</select>

注意参数符号:

#{id}

“#”在编辑器中是标题字体,为了不影响阅读,下面的#号开头的段落中#都是用“”引起来的

“#”{} 告诉 MyBatis 创建一个预编译语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个 “ ? ” 来标识,并被传递到一个新的预编译语句中,就像这样:

// 近似的 JDBC 代码,非 MyBatis 代码...
String selectUser = " select * from t_user where id = ? ";
PreparedStatement ps = conn.prepareStatement(selectUser);
ps.setInt(1,id);

“#”{} 作为占位符,${} 作为替换符,两者没有孰轻孰重,只不过应用场景不同,适当取舍即可。

我们希望完成类似 JDBC 中的 PrepareStatement 预编译处理 ,可以使用 #{} ,它会在替换占位符时首尾添加上单引号 '' ,能有效防止 SQL 注入 风险。

例如使用 ${} 操作删除 ( 就很有问题!)

// 1、使用 ${} 有注入风险
delete from t_user where id = ${id}

// 2、正常传值,id 传入 1  
delete from t_user where id = 1
// 结果删除了id=1 的记录

// 3、注入风险,id 传入 1 or 1=1 
delete from t_user where id = 1 or 1=1
// 全表删除了

再看看 #{} 是如何规避 SQL 注入 的:

// 1、使用 #{} 有效防止注入风险
delete from t_user where id = #{id}

// 2、正常传值,id 传入 1   
delete from t_user where id = '1'
// 结果删除了id=1 的记录

// 3、注入风险,id 传入 1 or 1=1 
delete from t_user where id = '1 or 1=1'
// SQL 语句报错,表数据安全

虽然在防止 SQL 注入方面,确实无能为力,不过我们{} 确实无能为力,不过我们 确实无能为力,不过我们{} 在其它方面可不容小觑,例如它允许你灵活地进行 动态表和动态列名的替换操作,例如:

// 1、灵活查询指定表数据
select * from ${tableName} 

// 传入 tableName参数 = t_user , 结果
select * from t_user  

// 2、灵活查询不同列条件数据
select * from t_user where ${colunmName} = ${value}

// 传入 colunmName参数 = name , value参数 = '潘潘', 结果
select * from t_user where name = '潘潘'

// 传入 colunmName参数 = id , value参数 = 1, 结果
select * from t_user where id = 1

以上的 {} 替换列名与表名的方式非常灵活,不过确实存在 SQL 注入风险,所以在考虑使用 #{} 或 {} 前,需要评估风险,避免风险,允许的情况下,我建议使用 #{} 。

当然,select 元素允许你配置很多属性来配置每条语句的行为细节。

<select     
  id="selectUser" 
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY"
  databaseId="mysql"
  resultOrdered="false"
  resultSets="rs1,rs2,rs3">
  select * from t_user
</select>

下面详细介绍一下,略微冗长,一口气看完吧:

我们知道 JDBC 通过 ResultSet 来对查询结果进行封装,ResultSet 对象本身包含了一个由查询语句返回的一个结果集合。例如你经常在 JDBC 见过的结果集读取:

// 允许滚动游标索引结果集
while( rs.next() ){
    rs.getString("name");
}
// 当然也支持游标定位到最后一个位置
rs.last();
// 向后滚动
rs.previous();

四、insert / update / delete 增删改

数据变更语句 insert,update 和 delete 的实现非常接近,而且相对于 select 元素而言要简单许多。

<insert
  id="insertUser"
  parameterType="domain.vo.User"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateUser"
  parameterType="domain.vo.User"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteUser"
  parameterType="domain.vo.User"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

其中大部分属性和 select 元素相同,我们介绍 3 个不同的属性:

我们先看看 insert,update 和 delete 语句的示例:

<insert id="insertUser">
  insert into t_user (id,name) 
  values (#{id},#{name})
</insert>

<update id="updateUser">
  update t_user set name = #{name} where id = #{id}
</update>

<delete id="deleteUser">
  delete from t_user where id = #{id}
</delete>

如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,并且提供了多种生成方式。

首先,如果你的数据库支持 自动生成主键 的字段(比如 MySQL 和 SQL Server),那么你可以设置 useGeneratedKeys=”true”,然后再把 keyProperty 设置为目标属性就 OK 了。例如,如果上面的 t_user 表已经在 id 列上使用了自动生成,那么语句可以修改为:

<insert id="insertUser" useGeneratedKeys="true"
    keyProperty="id">
  insert into t_user (name) values (#{name})
</insert>

如果你的数据库还支持多行插入, 你也可以传入一个 User 数组或集合,并返回自动生成的主键。

<insert id="insertUser" useGeneratedKeys="true"
    keyProperty="id">

  insert into t_user (name) values  

  <foreach item="item" collection="list" separator=",">
    (#{item.name})
  </foreach>
</insert>

对于不支持自动生成主键列的数据库和可能不支持自动生成主键的 JDBC 驱动,MyBatis 有另外一种方法来生成主键

这里有一个简单(也很傻)的示例,它可以生成一个随机 ID(不建议实际使用,这里只是为了展示 MyBatis 处理问题的灵活性和宽容度):

<insert id="insertUser">

  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>

  insert into t_user (id, name)
  values  (#{id}, #{name})
</insert>

在上面的示例中,首先会运行 selectKey 元素中的语句,并设置 User 的 id,然后才会调用插入语句。这样就实现了数据库自动生成主键类似的行为,同时保持了 Java 代码的简洁。

selectKey 元素描述如下:

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">

selectKey 中的 order 属性有2个选择:BEFORE 和 AFTER 。

五、cache 缓存

缓存对于互联网系统来说特别常见,其特点就是将数据保存在内存中。MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。

默认情况下,只启用了本地的会话缓存(即一级缓存,sqlSession级别 ),它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,首先在全局配置文件config.xml文件中加入如下代码:

<!--开启二级缓存--> 
<settings> 
    <setting name="cacheEnabled" value="true"/> </settings>

其次在UserMapper.xml文件中开启缓存:

<!--开启二级缓存--> 
<cache></cache>

基本上就是这样。这个简单语句的效果如下:

缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

这些属性可以通过 cache 元素的属性来修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

上面表示了一套更高级的缓存配置,首先创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,然后返回的对象被设置成只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

缓存可用的清除策略有:

默认的清除策略是 LRU

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

二级缓存是事务性的。这意味着,当 SqlSession 完成并提交 ( commit ) 时,或是完成并回滚 ( close ) 时,二级缓存都会被刷新。不管是否配置了 flushCache=true 。

Mybatis 的缓存包括一级缓存(sqlSession 级别)和二级缓存(mapper 级别),所以 mapper 映射器中配置的是二级缓存,我们先大概知道有这个概念,因为后续我们会针对这两种缓存进行详细介绍,而且还会讲解如何自定义缓存,因为 Mybatis 的缓存默认都是以 map 的数据结构存储在本地,所以自定义缓存可以把存储介质拓展到磁盘或数据库redis等;而且一级缓存是默认开启的,二级缓存需要我们手工开启,这些后续都会详细讲解,提前预告。

缓存获取顺序:二级缓存 > 一级缓存 > 数据库

六、cache-ref 引用缓存

回想一下 cache 的内容,对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。

<cache-ref namespace="com.vo.UserMapper"/>

七、sql 语句块

这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。 参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。比如:

<sql id="userColumns"> 
    ${alias}.id,${alias}.name 
</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 t_user t1 cross join t_user t2
</select>

也可以在 include 元素的 refid 属性或多层内部语句中使用属性值,例如:

<sql id="sql1">
  ${prefix}_user
</sql>

<sql id="sql2">
  from
    <include refid="${include_target}"/>
</sql>

<select id="select" resultType="map">
  select
    id, name
  <include refid="sql2">
    <property name="prefix" value="t"/>
    <property name="include_target" value="sql1"/>
  </include>
</select>

八、parameterMap 参数映射

parameterMap 元素官方已经不建议使用,并且再后续版本会退出舞台。首先对于我们 Java 来说,特别不希望在代码中通过传递 map 来传参,这样对于后续维护或者参数查找都是极不负责任的,我们推荐使用 JavaBean 来传值参数,这是 parameterMap 被抛弃的其中一个原因;另外也由于 parameterType 属性的诞生就能很好的代替 parameterMap ,并且还能自定义 JavaBean 类型的传参,所以 parameterMap 退出舞台,实属正常。

九、总结

我一直来都希望自己只输出观点,而不是输出字典,但其中有些知识点又是极其冗杂,知识输出真是个难搞的差事,如何既能把知识脉络梳理的完整,又能讲得浅显易懂,言简意赅,确实是后续文章分解输出的研究方向。

作者:潘潘和他的朋友们
链接:https://juejin.cn/post/6907274537113485325
来源:掘金

上一篇下一篇

猜你喜欢

热点阅读