写点什么

Mybatis 系列全解(五):全网最全!详解 Mybatis 的 Mapper 映射文件

发布于: 2021 年 01 月 26 日
Mybatis系列全解(五):全网最全!详解Mybatis的Mapper映射文件

封面:洛小汐

作者:潘潘



若不是生活所迫,谁愿意背负一身才华。



前言


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



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


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


Mybatis 系列全解脑图分享,持续更新中




Mybaits 系列全解 (持续更新)


  • Mybatis 系列全解(一):手写一套持久层框架

  • Mybatis 系列全解(二):Mybatis 简介与环境搭建

  • Mybatis 系列全解(三):Mybatis 简单 CRUD 使用介绍

  • Mybatis 系列全解(四):全网最全!Mybatis 配置文件 XML 全貌详解

  • Mybatis 系列全解(五):全网最全!详解 Mybatis 的 Mapper 映射文件

  • Mybatis 系列全解(六):Mybatis 最硬核的 API 你知道几个?

  • Mybatis 系列全解(七):全息视角看 Dao 层两种实现方式之传统方式与代理方式

  • Mybatis 系列全解(八):Mybatis 的动态 SQL

  • Mybatis 系列全解(九):Mybatis 的复杂映射

  • Mybatis 系列全解(十):Mybatis 注解开发

  • Mybatis 系列全解(十一):Mybatis 缓存全解

  • Mybatis 系列全解(十二):Mybatis 插件开发

  • Mybatis 系列全解(十三):Mybatis 代码生成器

  • Mybatis 系列全解(十四):Spring 集成 Mybatis

  • Mybatis 系列全解(十五):SpringBoot 集成 Mybatis

  • Mybatis 系列全解(十六):Mybatis 源码剖析




目录


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

2、namespace 命名空间

3、select 查询

4、insert / update / delete 增删改

5、cache 缓存

6、cache-ref 缓存引用

7、sql 语句块

8、parameterMap 参数映射

9、总结



mapper 映射器顶级元素全貌


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


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


  • select : 用于查询,支持传参,返回指定结果集;

  • insert : 用于新增,支持传参,返回指定结果集;

  • update : 用于更新,支持传参,返回指定结果集;

  • delete : 用于删除,支持传参,返回指定结果集;

  • sql : 被其它语句引用的 可复用 语句块;

  • cache : 当前命名空间缓存配置;

  • cache-ref : 引用其它命名空间的缓存配置;

  • parameterMap : 参数映射,已弃用,是它不够好;

  • resultMap : 结果集映射,它就很好;


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


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


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



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



namespace 命名空间


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


  • 上学时,6 年级一班 23 号,能代表唯一的你。

  • 编写 Java 类时,包名 + 类名,能代表唯一的类。

  • 而如今,我们在 Mybatis 中写的每一段 SQL 语句,同样有唯一的代表方式,那就是「 命名空间标识 + 语句 id 」,无论是为了区分业务也好,还是为了拆分服务也好,反正 Mybatis 让每一个 mapper.xml 配备一个唯一命名空间标识。



每一段 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>
复制代码


  • id 属性:在当前 mapper.xml 命名空间下,它的 id 值是唯一的( 不过如果在不同的 mapper.xml 命名空间下,则允许有相同的的 id 值 )

  • parameterType 属性:代表传入的参数类型,这里是 int (或 Integer)类型

  • resultType 属性:代表返回结果类型,这里指定返回一个 hashMap 类型的对象,mybatis 会把查询出来的数据表记录对应的 ' 字段列名 - 字段值 ',自动映射为 map 集合的 key - value


当然如果你不希望通过 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>
复制代码



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


  • id 必填项,在命名空间下的唯一标识,可被 Mybatis 引用,如果存在相同的 “ 命名空间 + 语句 id ” 组合,Mybatis 将抛出异常;

  • parameterType 可选项,传入语句的参数的类全限定名或别名,可以是基本类型、map 或 JavaBean 等复杂的参数类型传递给 SQL;

  • parameterMap 用于引用外部 parameterMap 的属性块,目前已被废弃。以后请使用行内参数映射和 parameterType 属性。

  • resultType 可选项,定义类的全路径,在允许自动匹配的情况下,结果集将通过 Javaben 的规范映射,或定义为 int 、double、float 等参数;也可以使用别名,但是要符合别名规范和定义。 resultType 和 resultMap 之间只能同时使用一个。(日常中,比如我们统计结果总条数的时候可以设置为 int );

  • resultMap 可选项,对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解,后面一对一、一对多、多对多我们会有一篇文章单独讲解。 resultType 和 resultMap 之间只能同时使用一个。

  • flushCache 可选项,清空缓存,将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。

  • useCache 可选项,使用缓存,将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。

  • timeout 可选项,这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。

  • fetchSize 可选项,获取记录的总条数设定。这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。由于性能问题,建议在 sql 做分页处理。

  • statementType 可选项,可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。

  • resultSetType 可选项,FORWARDONLY,SCROLLSENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动)。

- FORWARD_ONLY,只允许游标向前访问;

- SCROLL_SENSITIVE,允许游标双向滚动,但不会及时更新数据,也就是说如果数据库中的数据被修改过,并不会在 resultSet 中及时更新出来;

- SCROLL_INSENSITIVE ,允许游标双向滚动,如果数据库中的数据被修改过,会及时更新到 resultSet;

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


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


  • databaseId 可选项,如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。

  • resultOrdered 可选项,这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false。

  • resultSets 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。


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 个不同的属性:


  • useGeneratedKeys : (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。

  • keyProperty : (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。

  • keyColumn : (仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。


我们先看看 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 。


  • BEFORE:表示先执行 selectKey 的语句,然后将查询到的值设置到 JavaBean 对应属性上,然后再执行 insert 语句。

  • AFTER:表示先执行 AFTER 语句,然后再执行 selectKey 语句,并将 selectKey 得到的值设置到 JavaBean 中的属性。上面示例中如果改成 AFTER,那么插入的 id 就会是空值,但是返回的 JavaBean 属性内会有值。



cache 缓存


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


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


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


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


<!--开启二级缓存--> <cache></cache>
复制代码


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


  • 映射语句文件中的所有 select 语句的结果将会被缓存。

  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。

  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。

  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。

  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。

  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。


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


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


<cache  eviction="FIFO"  flushInterval="60000"  size="512"  readOnly="true"/>
复制代码


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


缓存可用的清除策略有:


  • LRU – 最近最少使用:移除最长时间不被使用的对象。

  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。

  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。


默认的清除策略是 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 退出舞台,实属正常。




总结


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


本篇完,本系列下一篇我们讲《 Mybatis 系列全解(六):Mybatis 最硬核的 API 你知道几个? 》。





BIU ~ 文章持续更新,微信搜索「潘潘和他的朋友们」第一时间阅读,随时有惊喜。本文会在 GitHub https://github.com/JavaWorld 收录,热腾腾的技术、框架、面经、解决方案,我们都会以最美的姿势第一时间送达,欢迎 Star ~ 我们未来 不止文章!想进读者群的伙伴欢迎撩我个人号:panshenlian,备注「加群」我们群里欢聊吧 ~


用户头像

有点特别的架构师 2021.01.25 加入

微信搜索:潘潘和他的朋友们

评论

发布
暂无评论
Mybatis系列全解(五):全网最全!详解Mybatis的Mapper映射文件