简介
除了配置相关之外,另一个核心就是SQL映射,MyBatis 的真正强大也在于它的映射语句。
Mybatis创建了一套规则以XML为载体映射SQL
之前提到过,
各项配置信息将Mybatis应用的整体框架搭建起来,而映射部分则是准备了一次SQL操作所需的信息
一次SQL执行的主要事件是什么?
输入参数解析,绝大多数SQL都是需要参数的
SQL,通过SQL与数据库交互,所以最根本的是SQL,如果连SQL都没有,还扯个蛋蛋?
结果映射,Mybatis可以帮我们完成字段与Java类型的映射
所以说SQL映射的核心内容为:
- SQL内容指定
- 参数信息设置
- 输出结果设置
当然,每个SQL都需要指定一个ID作为用于执行时的唯一标识符
比如下面示例
<select id="selectPerson"parameterType="int"resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
SELECT * FROM PERSON WHERE ID = #{id} 为SQL内容部分
parameterType="int" 以及SQL中的#{id}为参数信息设置部分
resultType="hashmap" 为输出结果设置部分
概况
如上所述,核心内容为:
- ID
- SQL内容
- 入参设置
- 结果配置
ID用于执行时唯一定位一个映射
对于
SQL内容,也没有什么特别的,就是平常所说的数据库可以执行的SQL语句
对于
SQL内容中的参数,MyBatis 会通过 JDBC创建一个预处理语句参数
这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,类似这样:
// Similar JDBC code, NOT MyBatis…String selectPerson ="SELECT * FROM PERSON WHERE ID=?";PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
输入的类型使用parameterType进行指定(parameterMap – 已废弃!)
输出信息使用resultMap或者resultType进行指定
从包含的信息的角度分析Mybatis 映射文件的核心内容
如下图所示:
而对于数据库的CRUD操作,Mybatis的XML配置中分别使用了 insert、select、update、delete四个标签进行分别处理
所以一个映射(映射文件中的一个)常见的形式如下,parameterType以及resultType | resultMap 会根据SQL的类型需要或者不需要
<select | insert | update | delete id="......" parameterType="......" resultType | resultMap="......">
SQL内容......
</select | insert | update | delete>
核心信息为通过Mybatis执行一次SQL的必备信息,Mybatis还可以提供更多的功能设置
所以对于不同类型的SQL,还会有更多的一些配置条目
比如之前提到过的数据库厂商标识符 databaseId,所有类型的SQL映射都可以设置这一属性
而对于其他的附加辅助属性配置,有些是所有类型共同的,而有些是特有的
databaseId就是共有的,比如用于返回自动生成的键的配置useGeneratedKeys 只有insert与update才拥有
文档结构解析
所以从文档结构的形式角度看SQL映射,有四种类型的映射 select、insert、update、delete
每种类型又都有各自的属性设置,有一些是共同的,有一些是特有的
下图如果不清楚,请到评论区中,右键,新标签查看图片,可以查看到大图
属性角度解析
如果
从属性的角度去看待各自的归属,每种属性都有各自的作用功能
他们自身的功能也决定了那些类型才能拥有他
比如键值的返回相关的useGeneratedKeys,就只可能发生在insert或者update中,只有他们才可能自动生成键
以上为SQL映射文件的核心关键信息以及属性的解读
有些细节还需要注意,
关于flushCache以及userCache,前者是是否清空清空本地缓存和二级缓存,后者是本条语句的结果是否进行二级缓存,含义完全不一样
四种类型都有flushCache属性,对于select默认false,对于insert、update、delete默认是true
而userCache只有select有,默认是true
因为缓存机制,比如update 的时候如果 设置flushCache="false",则当你更新后,查询的数据数据还是老的数据。
额外的馈赠-语法糖
在编程实践中,经常有一些公共的方法或者处理逻辑,我们通常将他们提取单独封装,以便提高代码复用程序
那么,对于SQL的编写呢?
Mybatis也提供了封装提取的手段---
SQL元素标签
<sql id="xxx">
........
</sql>
然后可以使用include,将他包含到指定的位置
<include refid="xxx"></include>
这是一种
静态的织入,通过SQL元素,你可以方便的完成公共SQL片段的提取封装
如果有两个表,都有name、age等字段,我想将他们封装,但是表名却又不一样怎么办?
SQL元素还提供了别名的设置,可以很容易的解决这个问题,请参考官方文档
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
这个 SQL 片段可以被包含在其他语句中,例如:
<select id="selectUsers"resultType="map">
select
<include refid="userColumns"><propertyname="alias"value="t1"/></include>,
<include refid="userColumns"><propertyname="alias"value="t2"/></include>
from some_table t1
cross join some_table t2
</select>
上面示例中包含了两次SQL片段,第一次中alias被替换为t1 ,第二次中的alias被替换为t2,最终的结果形式为:
select
t1.id,
t1.username,
t1.password,
t2.id,
t2.username,
t2.password
from
some_table t1
cross join some_table t2
深入映射
参数(Parameters)细节配置
<selectid="selectPerson"parameterType="int"resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
示例中入参类型通过parameterType指定为int,参数占位符为#{id},这是最简单的一种形式了,入参只是一个Java基本类型(非自定义的对象类型)
对于对象类型Mybatis也可以很好的完成工作,不管是入参时的解析,还是输出结果的映射解析
能够根据属性的名称进行自动的配对
<select id="selectUsers"resultType="User">
select id, username, password
from users
where id = #{id}
</select>
<insert id="insertUser"parameterType="User">
insert into users (id, username, password)
values (#{id}, #{username}, #{password})
</insert>
不仅仅支持对象,而且还
支持map,当parameterType="map"时,map的key会被用来和占位符中的名称进行匹配
也就是说对于: SELECT * FROM PERSON WHERE ID = #{id} ,当parameterType="map"时,你的参数map需要存在 key=id 的元素
parameterType
也支持list,当parameterType="list"时,可以借助于动态SQL的foreach 进行循环
如果是基本数据类型的List,比如List<Integer> 那么直接循环即可;如果是List<User>,可以通过遍历每个元素,然后通过#{item.username}、#{item.password}的形式进行读取
<insert id="..." parameterType="List">
INSERT INTO xxx_table(
username,
password,
createTime
)
values
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.username},
#{item.password},
#{item.createTime}
)
</foreach>
</insert>
可以看得出来,类型的形式很丰富,Mybatis很多时候都可以自动处理,但是你可以对他进行显式的明确指明,比如
<span style="color:#880000">#{property,javaType=int,jdbcType=NUMERIC}</span>
property表示字段名称,javaType为int,jdbcType为NUMERIC
(jdbcType是JDBC对于数据库类型的抽象定义,详见java.sql.JDBCType 或者java.sql.Types,可以简单认为数据库字段类型
javaType 通常可以由参数对象确定,除非该对象是一个 HashMap,是map的时候通常也可以很好的工作,但是建议在入参类型是Map对他进行明确的指定
需要注意的是:如果一个列允许 null 值,并且会传递值 null 的参数,就必须要指定 JDBC Type
当你在插入时,如果需要使用自定义的typeHandler ,也应该在此处进行指定
<span style="color:#880000">#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}</span>
对于数值类型,还可以设置保留小数的位数
<span style="color:#880000">#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}</span>
对于参数的细化配置也很容易理解,他要么是用于使用时确定入参或者数据库字段的具体类型,如javaType或者jdbcType
要么就是在字段处理过程中增加的一些处理所需要的信息,比如是不是需要按照自定义处理器处理后在执行到数据库?是不是将数值的小数位数处理后在去执行数据库?
另外对于存储过程的调用Mybatis也是有支持的,mode 属性允许你指定 IN,OUT 或 INOUT 参数。
通常我们使用#{}的格式进行字符串处理,这样可以安全,是通常的首选,但是如果你就是想直接插入一个字符串到SQL中,可以使用${},不过很显然,$的使用你要非常慎重
ResultMap-别名映射
Mybatis好用的一大神器就是ResultMap,可以让你高效灵活的从结果集映射到你想要的类型中,能够进行很多高级的映射
一般的映射可以借助于resultType就可以解决了,resultType后面的值同parameterType类似
parameterType resultType的值都用于明确类型,可以使用完全限定名
不过你是否还记得入门简介中关于typeAlias中的介绍?
Mybatis内置了Java基础类型的别名,你都可以直接使用
借助于resultType可以完成一些基本的诉求,比如从单表到对应实体类对象的映射,能够自动的根据字段名称和属性名称进行匹配
但是如果名称不对应又该怎么办?
如果你的实体中的属性名称为userName,数据库字段名为name,Mybatis真的敢擅自的将这两者对应起来么?
如下图所示,将之前的第一个示例稍作修改,增加一个StudentAnother,name更改为了userName,并将测试代码稍作修改
从结果可以看得到,实体中的userName是null ,Mybatis肯定不敢擅自映射
一种可行的方式是使用别名,通过数据库字段AS设置别名,就可以成功的完成映射
通过别名,将数据库列名通过别名与属性字段建立映射关系,然后Mybatis就可以进行自动匹配了
但是这种形式如果有多条SQL,每个SQL中都需要有别名,而且,如果后续有原因修改对象的字段名字,怎么办?
另外的方式就是使用ResultMap,ResultMap的基础用法就是相当于设置别名
但是借助于ResultMap,将别名的映射关系,维护在ResultMap中,所有使用到此映射类型的SQL都只需要关联这个ResultMap即可,如果有变更,仅仅需要变更ResultMap中的属性字段对应关系
所有的SQL中的内容并不需要变动
如下图所示,SQL中字段与实体类中不匹配,查询的结果为null
右侧通过ResultMap将userName与列名name进行了映射,就可以成功读取数据
ResultMap最基础的形式如下
<resultMap id="............" type=".................">
<id property="............" column="............"/>
<result property="............" column="............"/>
</resultMap>
ResultMap需要id和type,id用于唯一标识符,type用于指明类型,比如Blog
ResultMap最基础的两个信息是id和result元素
他们的内容均为property="......." column="...........",property(对象的属性字段)和clumn(数据库的列名)
对于基础性的映射借助于id和result就可以完全搞定, id 表示的结果将是对象的标识属性,可以认为对象的唯一标识符用id指定,这对于性能的提高很有作用
小结
对于ResultMap就是做字段到属性的映射,id和result都是这个作用,但是如果是唯一标识符请使用id来指定
另外对于每一个字段,还可以明确的声明javaType和jdbcType,以及typeHandler用于更加细致的解析映射
所以说基本元素为:
ResultMap-高级映射
ResultMap当然不仅仅是像上面那样只是别名的转换,还可以进行更加复杂的映射
对于结果集返回有哪些场景?
“将一行记录映射为一个对象”与“将多行记录映射为对象列表”这两者本质是一样的,因为所需要做的映射是一样的
比如上面数据库列名name到字段userName 的映射,不管是一行记录还是多行记录,他们都是一样的
所以下面就以一个对象为例
单纯的映射
比如上面的例子,数据库列名与实体类中的字段一一对应(尽管名称不完全匹配,但是仍旧是一一对应的)
组合的映射
对于关系型数据库存在着关联关系的说法,一对一,一对多等
这些关联关系最终也是要映射到对象中的, 所以对象中经常也会存在多种对应关系
比如下面官方文档中的示例----查询博客详情
一个博客Blog 对应着一个作者Author ,一个作者可能有多个博文Post,每篇博文有零或多条的评论Post_Tag 和标签Tag
<!-- Very Complex Statement -->
<selectid="selectBlogDetails"resultMap="detailedBlogResultMap">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio,
A.favourite_section as author_favourite_section,
P.id as post_id,
P.blog_id as post_blog_id,
P.author_id as post_author_id,
P.created_on as post_created_on,
P.section as post_section,
P.subject as post_subject,
P.draft as draft,
P.body as post_body,
C.id as comment_id,
C.post_id as comment_post_id,
C.name as comment_name,
C.comment as comment_text,
T.id as tag_id,
T.name as tag_name
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Post P on B.id = P.blog_id
left outer join Comment C on P.id = C.post_id
left outer join Post_Tag PT on PT.post_id = P.id
left outer join Tag T on PT.tag_id = T.id
where B.id = #{id}
</select>
对于实体类,一种可能的形式如下
Blog中有一个Author,有一个List<Post> ,每一个Post中又有List<Comment> 和 List<Tag>
可以看得出来对于组合映射又有一对一以及一对多两种形式
(尽管Blog存在List<Post> postList; 但是在Mybatis中使用时,对于关系是从紧邻的上一层确定的,比如对于Comment看Post,对于Post看Blog,而不是从Blog看Comment )
Mybatis的ResultMap可以完成类似上述SQL与实体类的映射
在Mybatis中只有两种情况,一对一和一对多
一对一Association
对于一对一被称作关联,在ResultMap中使用association元素表示这种关系
含义为:
association中的所有的字段 映射为association元素上property指定的一个属性
比如下面示例,将id和username 映射为author,谁的author?他的直接外层是谁就是谁!
<association property="author"column="blog_author_id"javaType="Author">
<id property="id"column="author_id"/>
<result property="username"column="author_username"/>
</association>
对于association的基本格式如下,相当于在基础的ResultMap中插入了一个“一对一”的对应
<resultMap id="............" type=".................">
<id property="............" column="............"/>
<result property="............" column="............"/>
<association property="............" column="............" javaType="............">
<id property="............" column="............"/>
<result property="............" column="............"/>
</association>
</resultMap>
association中对于字段和属性的映射也是使用id和result,对于唯一标志使用id来表示
关联的嵌套查询
对于一个association还可以对他进行嵌套查询,也就是在查询中进行查询
比如官方示例中
<resultMap id="blogResult"type="Blog">
<association property="author"column="author_id"javaType="Author"select="selectAuthor"/>
</resultMap>
<select id="selectBlog"resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<selectid="selectAuthor"resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>
当执行selectBlog时,会执行 SELECT * FROM BLOG WHERE ID = #{id} ,查询得到的结果映射到blogResult,在这个ResultMap中使用了association元素
这个association元素使用select标签进行了嵌套查询,也就是使用另外的一个映射selectAuthor进行处理
处理流程:
- 先查询selectBlog查询所有的结果
- 对于每一条结果,然后又再一次的select,这就是嵌套查询
这会出现“N+1 查询问题”,查询一次SQL查询出一个列表(这是1)然后对于这个列表的每一个结果都再次的查询(这是N)性能有些时候很不好
嵌套查询使用select,还有一个重要的就是association 上的 column,这个column用于指定嵌套查询的参数
比如上面的例子,将会使用author_id传递给 SELECT * FROM AUTHOR WHERE ID = #{id}中的id,然后进行查询
此处仅仅只是一个参数,如果是多个参数仍旧可以,使用 column= ” {prop1=col1,prop2=col2} ”的形式
比如:
上面就是通过column指定将要传递给嵌套内查询的参数
鉴于ResultMap可以提供很好地映射,所以上面的示例完全可以修改为普通的association形式,通过join将关联查询的结果映射到指定的对象中,而不是借助于select元素进行嵌套查询
一对多collection
对于一对多关系,Mybatis使用collection
collection的逻辑本质上与association是一样的,都是对象字段映射
只不过用于区分,也用于在除了数据时,具体的指定类型
一个collection形式为:
<collection property="posts"ofType="domain.blog.Post">
<id property="id"column="post_id"/>
<result property="subject"column="post_subject"/>
<result property="body"column="post_body"/>
</collection>
内部依然是使用id和result完成字段和属性的映射
但是collection上使用ofType来指定这个属性的类型,而不是之前的javaType
这也很好理解,对于一对一或者检查的查询,他就是一个对象类型,所以使用JavaType
对于集合的映射,我们很清楚的知道他是一个集合,所以集合类型是他的javaType,比如 javaType="ArrayList",Mybatis 在很多情况下会为你算出来,所以可以省略javaType
但是,什么类型的集合?还需要说明,所以使用ofType进行指定,看起来更加清晰
使用collection的基础形式为:
<resultMap id="............" type=".................">
<id property="............" column="............"/>
<result property="............" column="............"/>
<collection property="............" column="............" ofType="............">
<id property="............" column="............"/>
<result property="............" column="............"/>
</collection>
</resultMap>
集合的嵌套查询
对于collection也可以采用类似association中的select元素进行嵌套查询
原理也是类似,当检索出来结果后,借助于select指定的查询语句,循环查询
<resultMap id="blogResult"type="Blog">
<collection property="posts"javaType="ArrayList"column="id"ofType="Post"select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog"resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<selectid="selectPostsForBlog"resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>
ResultMap的嵌套
在前面的叙述中,所有的内部的关联或者集合的属性映射都是直接嵌套在外部ResultMap中的
借助于嵌套查询的形式 select属性,可以进行嵌套查询,通过嵌套查询的方式,相当于经过这个select,内部的字段映射部分被路由到另一个ResultMap(ResultType)中了
而不需要在这个ResultMap中逐个重新的进行字段的映射指定
但是select会有1+N的问题,但是使用select时这种使用外部ResultMap(resultType)的形式却是很有实用意义
因为如果可以进行分离,被剥离的那一部分既可以单独使用,又可以嵌套在其他的ResultMap中,组合成更加强大的形式
Mybatis是支持ResultMap嵌套的
不仅仅association支持ResultMap的嵌套,collection也是支持的
可以看得出来,不管是借助于select的嵌套查询,还是ResultMap的嵌套,都只是在association上或者collection上附加select或者resultMap属性即可
然后就可以省略掉标签内部的字段映射处理了(id和result)
除非开发前对ResultMap的层级结构进行过统一设计布局,否则,内嵌其他人开发的ResultMap,也并不一定总是好事,当内嵌的ResultMap发生变动时,某些情况可能会导致问题
嵌套的ResultMap一定需要是本文件中的吗?当然不是必须的,比如下面示例中借助于:接口的全限定名称进行索引
<association property="courseEntity" column="course_id"
javaType="com.xxx.xxx.domain.CourseEntity" resultMap="com.xxx.xxx.dao.CourseMapper.courseResultMap">
</association>
ResultMap的重用
ResultMap的嵌套也是一种复用,此处说的重用非解耦后的复用
在ResultMap中,我们通过id或者result 将数据库字段和实体类中的属性名进行对应
列名和属性名的对应,以及列名和属性名全部都是固定的了,如下图所示,username就是和author_username对应
在之前的例子中,一个blog有一个作者,但是如果一个博客还有一个联合作者怎么办?就像很多书可能不仅仅只有一个作者
在这种场景下:有两个作者,他们的java类型必然都是Author
而且他们的字段也是相同的,但是你不得不将他们进行区分,如下面SQL中所示,关联了两次Author表,通过前缀进行了区分
一种解决方法就是将映射部分也重写两次,就像关联两次那样,仅仅是列名column前缀不同(可以将这两个ResultMap嵌入到blogResult中或者内容移入到外层ResultMap中,总之是写两遍映射)
还有一种方法就是借助于columnPrefix,如下图所示,Blog中有两个Author的实例,一个是author另一个是coAuthor,关联关系,使用association
他们都是Author类的实例,所以使用同样的ResultMap,通过columnPrefix对其中一个映射添加列前缀
通过这个列前缀,就相当于有了另外的一个ResultMap,这个ResultMap就是指定的ResultMap中的column中每一个值都加上一个前缀
构造方法字段值注入
使用Mybatis的核心就是为了执行SQL以及完成结果映射,结果的映射必然要创建最终需要映射的结果的对象
通过ResultMap中的id和result指定的字段值都是通过setter设置器方法进行值的设置的
既然最终就是要创建一个指定类型并且具有指定属性的对象结果,那么为什么一定非得是通过setter,难道不能在创建对象的时候通过构造方法初始化对象吗?
Mybatis的ResultMap是支持构造方法设置的
对于构造方法的属性值设置,通过constructor进行
将之前的例子稍作修改,增加一个构造方法,复制一个ResultMap,添加constructor,就可以完成映射
借助于constructor与使用id和result映射在业务逻辑上没有什么本质的区别,都是将列名与字段进行映射,变的是形式
因为是借助于构造函数,所以constructor中与ResultMap中的其他字段映射是有区别的,不是使用id和result 使用的是
arg 参数
简言之,使用构造方法需要根据方法签名进行匹配,方法签名就是类型和个数的匹配,所以需要javaType
对于有些场景你可能不希望暴露某些属性的共有setter设置器,就可以使用构造方法的形式
上面的示例中没有通过constructor对id进行映射,如果对id进行映射需要使用 <idArg column="id" javaType="int"/>(没写错 就是idArg
)
对于使用constructor对值进行解析映射,根本就是匹配正确的构造方法,除了使用javaType还有name,通过name指定构造方法参数的名称
从版本 3.4.3 开始,如果指定了名称name,就不需要严格死板的按照顺序对应了,可以打乱顺序。
没有人会刻意的打乱顺序,但是永远的保证映射的顺序不变动是很难得
鉴别器
重新建一个表作为示例,配置信息还是如原来一样,SQL映射文件也是在第一个示例中的XML中编写的
主要的信息如下,表以及数据以及实体类以及映射文件等
定义了三个类,一个Person类作为抽象模型(尽管我这个不是抽象类)
一个成人类Adult和一个儿童类Child
Adult增加了company属性,Child增加了school属性
每个类都有setter和getter方法,并且还重写了toString方法
映射文件
测试类
package third;
import first.StudentAnother;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class Test {
public static void main(String[] args) throws Exception {
/*
* 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。
* SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。
* 而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。
* */
String resource = "config/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream,"development");
/*
* 从 SqlSessionFactory 中获取 SqlSession
* */
SqlSession session = sqlSessionFactory.openSession();
try {
List<Person> personList = session.selectList("mapper.myMapper.selectPerson");
personList.stream().forEach(i->{
System.out.print(i);
System.out.println(i.getClass().getName());
});
} finally {
session.close();
}
}
}
测试结果
Mybatis很神奇的将结果映射为了不同的子类对象
所以说如果一条记录可能会对应多种不同类型的对象,就可以借助于discriminator,通过某个字段的数据鉴别,映射为不同的类
ResultMap中的type对应了父类型,discriminator上的column对应了需要鉴别的列名
每一个case对应着一种类型或者一个ResultMap,通过discriminator就可以根据鉴别的值的不同进行动态的选择
discriminator可以很轻松的处理者中类层次关系中数据的映射
使用discriminator的结果处理步骤
- MyBatis将会从结果集中取出每条记录,然后比较它的指定鉴别字段的值。
- 如果匹配任何discriminator中的case,它将使用由case指定的resultMap(resultType)
- 如果没有匹配到任何case,MyBatis只是简单的使用定义在discriminator块外面的resultMap
如果将映射关系中case后面的值设置为3和4(数据库中只有1和2)
结果如下,仅仅匹配了discriminator外面的部分