关联 (One-To-One)
<!-- ResultMap --> <resultMap id="blogAuthorMap" type="blog"> <id property="id" column="id"/> <result column="name" property="name"/> <result column="title" property="title"/> <result column="content" property="content"/> <result column="author_id" property="authorId"/> <!-- 一对一关系映射,配置封装author的内容 --> <association property="author" javaType="author"> <id property="id" column="id"/> <result property="username" column="username"/> <result property="password" column="password"/> <result property="email" column="email"/> <result property="blo" column="blo"/> </association> </resultMap> <!-- --> <select id="findAll" resultMap="blogAuthorMap"> select b.*, a.username, a.email, a.blo from blog b, author a where b.author_id = a.id </select>
- 关联(association)元素处理“有一个”类型的关系。 比如,在我们的示例中,一个博客有一个用户。关联结果映射和其它类型的映射工作方式差不多。 你需要指定目标属性名以及属性的javaType(很多时候 MyBatis 可以自己推断出来),在必要的情况下你还可以设置 JDBC 类型,如果你想覆盖获取结果值的过程,还可以设置类型处理器。
- 关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:
- 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
- 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。
关联的嵌套 Select 查询
<resultMap id="blogAuthorMap" type="blog"> <id property="id" column="id"/> <result column="name" property="name"/> <result column="title" property="title"/> <result column="content" property="content"/> <result column="author_id" property="authorId"/> <!-- 一对一关系映射,配置封装author的内容 --> <association property="author" column="author_id" javaType="author" select="findAuthorById"/> </resultMap> <select id="findAuthorById" resultType="author"> select * author where where id = #{id} </select> <select id="findBlogAll" resultMap="blogAuthorMap"> select * from blog </select>
- 就是这么简单。我们有两个 select 查询语句:一个用来加载博客(Blog),另外一个用来加载作者(Author),而且博客的结果映射描述了应该使用 selectAuthor 语句加载它的 author 属性。
- 其它所有的属性将会被自动加载,只要它们的列名和属性名相匹配。
- 这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为“N+1 查询问题”。 概括地讲,N+1 查询问题是这样子的:
- 你执行了一个单独的SQL语句来获取一个列表(就是 "+1")
- 对列表返回的每一条记录,你执行了一个select 语句来为每条记录加载详细信息(这就是 "N")
- 这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。
- 好消息是,MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来。 然而,如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕。
- 所以我们建议使用上面中方式“关联的嵌套结果映射”
关联 (One-To-Many)
<!-- ResultMap --> <resultMap id="authorBlogMap" type="author"> <id column="id" property="id"/> <result column="username" property="username"/> <result column="password" property="password"/> <result column="email" property="email"/> <result column="blo" property="blo"/> <!-- 配置author用户中blog集合的映射 --> <collection property="blogList" ofType="blog"> <!-- aid 是blog.id 的一个别名 --> <id column="aid" property="id"/> <result column="name" property="name"/> <result column="title" property="title"/> <result column="content" property="content"/> <result column="author_id" property="authorId"/> </collection> </resultMap> <!-- select --> <select id="findAll" resultMap="authorBlogMap"> select * from author a left outer join blog b on a.id = b.author_id </select>
- 上述我们代码示例中, 一个作者(Author), 有多篇博客。在作者类中可以通过如下方式来表示
/** * 用户博客列表 */ private List<Blog> blogList;
关联 (Many-To-Many)
- 由于MyBatis 并没有提供 多对多的实现方式,那么我们在设计中可以通过一对多来间接实现。如下例:
<!-- 博客关联博客分类 --> <resultMap type="blog" id="resultMap"> <id column="id" property="id"/> <result column="name" property="name"/> <result column="title" property="title"/> <result column="content" property="content"/> <!-- 配置分类集合 --> <collection property="classificationList" ofType="classification"> <id column="c_id" property="id"></id> <result column="c_name" property="name"></result> <result column="remarks" property="remarks"></result> </collection> </resultMap> <!-- 查询博客列表中带有分类 --> <select id="findAll" resultMap="resultMap"> select b.*,c.id as c_id,c.name as c_name,c.remarks from blog b left outer join blog_classification bc on b.id = bc.blog_id left outer join classification c on bc.classification_id = c.id </select>
- 上面的例子中, 一个博客(blog)可能会有多个分类,并且一个分类(blog_classification)下面会有多个博客信息,用博客分类映射表(blog_classification)来表示他们之间的关系,本处只是为了体现多对多的实现,在实际业务中对于这种查询方式并不值得借鉴
- MyBatis 对关联映射,并没有深度,广度或组合上的要求。但是在映射时需要留意性能问题,在探索最佳实践的过程中,应用的单元测试和心梗测试会是你的好帮手,而MyBatis 的好处在于,可以在不对你的代码引入重大变更(如果有)的情况下,允许你之后改变你的想法。
- 高级关联和集合映射是一个深度话题。文档的介绍只能到此为止。配合少许的实践,你会很快了解全部的用法。
自动映射
- 正如你在前面一节看到的,在简单的场景下,MyBatis 可以为你自动映射查询结果。但如果遇到复杂的场景,你需要构建一个结果映射。 但是在本节中,你将看到,你可以混合使用这两种策略。让我们深入了解一下自动映射是怎样工作的。
- 当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。 这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性。
- 通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true。
- 甚至在提供了结果映射后,自动映射也能工作。在这种情况下,对于每一个结果映射,在 ResultSet 出现的列,如果没有设置手动映射,将被自动映射。在自动映射处理完毕后,再处理手动映射。 在下面的例子中,id 和 userName 列将被自动映射,hashed_password 列将根据配置进行映射。
<select id="selectUsers" resultMap="userResultMap"> select user_id as "id", user_name as "userName", hashed_password from some_table where id = #{id} </select>
<resultMap id="userResultMap" type="User"> <result property="password" column="hashed_password"/> </resultMap>
- 三种自动映射等级:
- NONE - 禁用自动映射。仅对手动映射的属性进行映射。
- PARTIAL - 对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射
- FULL - 自动映射所有属性。
- 默认值是 PARTIAL,这是有原因的。当对连接查询的结果使用 FULL 时,连接查询会在同一行中获取多个不同实体的数据,因此可能导致非预期的映射。 下面的例子将展示这种风险:
<select id="selectBlog" resultMap="blogResult"> select B.id, B.title, A.username, from Blog B left outer join Author A on B.author_id = A.id where B.id = #{id} </select>
<resultMap id="blogResult" type="Blog"> <association property="author" resultMap="authorResult"/> </resultMap> <resultMap id="authorResult" type="Author"> <result property="username" column="author_username"/> </resultMap>
- 在该结果映射中,Blog 和 Author 均将被自动映射。但是注意 Author 有一个 id 属性,在 ResultSet 中也有一个名为 id 的列,所以 Author 的 id 将填入 Blog 的 id,这可不是你期望的行为。 所以,要谨慎使用 FULL。
- 无论设置的自动映射等级是哪种,你都可以通过在结果映射上设置 autoMapping 属性来为指定的结果映射设置启用/禁用自动映射。
<resultMap id="userResultMap" type="User" autoMapping="false"> <result property="password" column="hashed_password"/> </resultMap>
缓存
- MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便的配置和定制。为了使它更加强大而且易于配置,MyBatis 3 中的缓存做了许多改进。
- 默认情况下,值启用了本地会话缓存,它仅仅对一个会话中的数据进行缓存。要启用全局的二级缓存,只需要在SQL的配置文件中添加。
<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 . 只读的缓存会给所有的调用者返回缓存对象的相同实例。因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回对象的拷贝。速度上会慢一些,但是更加安全,因此默认值是 flase
- 二级缓存是事务性的, 这意味着,当SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/update/delete 语句时,缓存会获得更新