XML映射文件
mybatis的真正实力是在于它的映射语句。这也是很神奇的地方。针对所有的特点来说,xml映射文件是相对简单的。当然,如果你要把它们与等价的JDBC代码相比较,你可能会立马发现可以少些95%的代码。mybatis主要是注重SQL的,尽量保持你的方式。
XML映射文件只有几个第一类的元素(它们被按照顺序定义):
cache-用一个命名空间来配置缓存。
cache-ref-从另一个命名空间中引用缓存。
resultMap-最复杂和最强悍的元素就是resultMap,它描述了如何从你的数据库结果集中加载你的对象。
sql-一个可重用的部分,可以被其他SQL语句引用
insert –映射的插入语句
update – 映射的更新语句
delete – 映射的删除语句
select – 映射的查找语句
下面模块将会详细的描述这些元素,用相应的SQL语句来进行描述。
select
在mybatis中你将会使用最多的就是select语句。数据放在数据库中,直到你取出来的时候才是非常重要的。所以大多数应用查询数据比修改数据要多很多。对于每个insert,update,delete来说,这里或许有很多select。这也是mybatis的一个基本原则,而且,正是这个原因,mybatis在查询和结果映射方面做了很大的努力和注意。select元素是相当简单的,下面有个简单的例子:
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
This statement is called selectPerson, takes a parameter of type int (or Integer), and returns a HashMap keyed by column names mapped to row values.
这个语句叫selectPerson,有一个int类型的参数,通过映射列名到行对应的值,返回一个hashmap的key。
注意该参数的记号:
#{id}
它告诉mybatis来创建一个PreparedStatement参数,在JDBC里面,像这样的参数将会在SQL中被定义为一个"?",然后传递到一个新的PreparedStatement,就像下面这样:
// Similar JDBC code, NOT MyBatis…
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
当然,还有更多的代码通过JDBC需要单独提取结果并将其映射到对象的实例,但是mybatis可以帮助你这样做。这里你需要知道更多的参数和结果集映射。这些细节保证了它们自己的部分,在后面的部分会说明。
select元素有很多属性来允许你配置每个语句想详细行为。
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
select的属性
insert, update and delete
数据的修改语句有insert,update和delete,它们的实现很相似:
<insert
id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20">
<update
id="updateAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
<delete
id="deleteAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
Insert, Update和Delete属性
下面是insert,update和delete语句的一些例子:
<insert id="insertAuthor">
insert into Author (id,username,password,email,bio)
values (#{id},#{username},#{password},#{email},#{bio})
</insert>
<update id="updateAuthor">
update Author set
username = #{username},
password = #{password},
email = #{email},
bio = #{bio}
where id = #{id}
</update>
<delete id="deleteAuthor">
delete from Author where id = #{id}
</delete>
正如所提到的。insert的属性更丰富点,它有一些额外的属性和父元素允许你用很多的方式来处理key的生成。
首先,如果你的数据库支持自动生成key属性(例如:mysql和sql server),你就可以直接这样写:useGeneratedKeys="true"
,然后设置keyProperty 到目标属性。例如:如果Author 表使用了一个自动生成列类型的id,这个语句会如下一样的被修改:
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>
如果你的数据库支持多列插入,你可以传递一个Authors 的list集合或者一个数组,并且可以取回自动生成的key。
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username, password, email, bio) values
<foreach item="item" collection="list" separator=",">
(#{item.username}, #{item.password}, #{item.email}, #{item.bio})
</foreach>
</insert>
对数据库来说,mybatis有另外一种来处理生成key的方式,不支持自动生成列类型,或者不支持JDBC驱动所支持的自动生成key。
这里是一个简单的例子来生成一个任意的ID(你会不会做,但这体现了灵活性和MyBatis真的不介意)
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>
在上面的例子中,selectKey 元素会先运行,Author 的id会被设置,接着insert语句会被调用。这个例子就是给你一个在你的数据库中没有复杂的java代码来生成key的相似的行为。
下面就是描述selectKey 元素的例子:
<selectKey
keyProperty="id"
resultType="int"
order="BEFORE"
statementType="PREPARED">
selectKey 属性
sql
这个元素可以被用来定义SQL中一个可重用的部分代码,它可以在其他的语句中被包含。(在加载阶段)它可以被静态的参数化。不同的属性值可以在包含实例中有所不同。例如:
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </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 some_table t1
cross join some_table t2
</select>
属性值可以被使用在包含refid属性或者属性值在内部的inclue子句中,例如:
<sql id="sometable">
${prefix}Table
</sql>
<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
<include refid="someinclude">
<property name="prefix" value="Some"/>
<property name="include_target" value="sometable"/>
</include>
</select>
Parameters
在以前的语句中,你看到过简单的parameters的例子。在mybatis中parameters是很强大的元素。对于一个简单的情景,大概90%的例子,这里对他们没有多少,例如:
<select id="selectUsers" resultType="User">
select id, username, password
from users
where id = #{id}
</select>
上面的例子论证了一个非常简单的parameter 的映射。parameterType 被设置为int,因此参数可以被命名为任何的。原始或者简单的数据类型如integer何string没有相关的属性,因此将会完全替代参数的所有值。然而,如果你在一个复杂对象中传递,它的行为就会有点不同。例如:
<insert id="insertUser" parameterType="User">
insert into users (id, username, password)
values (#{id}, #{username}, #{password})
</insert>
如果一个User 类型的参数对象被传递到语句中,id,username和password属性将会被查找,并且它们的值被传递到了PreparedStatement 的参数中。对于把参数传递到语句中是很好很简单的。但是在parameter maps中还有很多其他的特性。
首先:类似于mybatis的其它部分,参数可被指定更多的数据类型。
#{property,javaType=int,jdbcType=NUMERIC}
类似于mybatis的其他部分,从参数对象中决定了javaType,除非对象是HashMap。javaType 应该被具体化来确定正确的TypeHandler 被使用。
NOTE The JDBC Type is required by JDBC for all nullable columns, if null is passed as a value. You can investigate this yourself by reading the JavaDocs for the PreparedStatement.setNull() method.
注意:通过JDBC所有可为空的列 对于JDBC类型是必需的,如果把空当做一个值传递。你可以研究这个通过阅读的JavaDocs中的PreparedStatement.setnull()方法。
来进一步自定义类型处理,你也可以指定一个特定的TypeHandler类(或别名),例如
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
所以它看起来很啰嗦,但是实际你很少设置它们。
对于数字类型,这里也有一个numericScale 来决定由多少相关的小数被保留。
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
最后,模型的属性允许你指定in,out或者inout参数,如果一个参数是out或者inout,参数对象属性的实际值将会被改变,正如你期望的如果你调用了一个输出参数。如果mode=out(或者inout),并且jdbcType=CURSOR(例如:oracle中是REFCURSOR),你必须指定一个resultMap 到部署ResultSet 的参数类型。注意,javaType属性是一个可选的,如果把CURSOR当做一个jdbcType且留有空格,它将会自动的设置ResultSet 。
#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}
mybatis也支持大多的更高级的数据类型,比如structs,但是当你输出参数的时候,你必须告诉语句类型的名字。例如
#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}
尽管这些都可以选择,大多时间你简化了指定属性名字,mybatis会解决其余的部分。最多,你会把jdbcType指定为空列。
#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}
String Substitution(替代)
默认使用#{}
可以让mybatis生成PreparedStatement 的属性,并且通过PreparedStatement 参数设置安全的值。当这个更安全,更快,并且基本都更好的时候,有时你只想直接注入一个没有修改的string插入到SQL语句中。例如:order by,你或许可以按照下面这样使用:
ORDER BY ${columnName}
mybatis是不会遗漏或者修改字符串的。
注意:接受用户提供的一个没有修改的输入的语句时不安全的方式。这可以导致有可能发生SQL注入的攻击,因此你应该不允许用户输入这些属性,或者总是执行你自己的检查。
Result Maps
resultMap 元素是mybatis中最强大和重要的元素。它可以允许你删除90%的JDBC要求从结果集取回的数据的代码,而且,在一些场景允许你做一些JDBC所不支持的事情。实际上,为了给一个复杂的语句join映射写等价的代码或许跨越了很多行代码。ResultMap的设计就是如此简单,一点也不要求明确结果映射的语句,并且大多复杂的语句要求只不过是绝对的必要来描述关系。
你已经在前面看过简单的映射语句的例子,它们没有明确的resultMap。例如:
<select id="selectUsers" resultType="map">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
如此一个语句在所有列简化了结果,自动映射到hashmap的key中,正如resultType 属性一样具体。在很多情景下都是很有用的,一个hashmao不可以成为一个好的domain 模型。那意味着你的应用将很可能使用JavaBeans或者POJOs (Plain Old Java Objects)来当做domain模型。mybatis两者都支持。考虑下面的javaBean:
package com.someapp.model;
public class User {
private int id;
private String username;
private String hashedPassword;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getHashedPassword() {
return hashedPassword;
}
public void setHashedPassword(String hashedPassword) {
this.hashedPassword = hashedPassword;
}
}
基于JavaBean的规格,上面的类有三个属性:id, username, 和hashedPassword。在查询语句中他们刚好匹配对应的列名。
这样的一个JavaBean 可以被映射到一个ResultSet 就像hashmap一样容易。
<select id="selectUsers" resultType="com.someapp.model.User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
记着TypeAliases 是你的好伙伴。使用它们可以让你不需要一直写入你的类的完全限定路径。例如:
<!-- In Config XML file -->
<typeAlias type="com.someapp.model.User" alias="User"/>
<!-- In SQL Mapping XML file -->
<select id="selectUsers" resultType="User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
在这些例子中,mybatis在基于自动部署列到javaBean属性的名字的情况下会自动的创建ResultMap。如果列名不匹配,你可以利用select子句来aliases(标准SQL的特点) 列的名字使其标签匹配。例如:
<select id="selectUsers" resultType="User">
select
user_id as "id",
user_name as "userName",
hashed_password as "hashedPassword"
from some_table
where id = #{id}
</select>
你已经学到了一些关于ResultMap的知识,但是你还没有看到过一个关于它的例子。这些简单的例子是不会在这里出现的。仅仅是例子的原因,让我们一起来看一下作为内部resultMap的最后一个例子,它会是解决列名错误匹配的另一个方式。
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
SQL语句引用它来使用属性resultMap这样做(注意我们移除了resultType属性)。例如:
<select id="selectUsers" resultMap="userResultMap">
select user_id, user_name, hashed_password
from some_table
where id = #{id}
</select>
高等的 Result Maps
mybatis被创建是因为心里一个想法的产生:数据库并不总是你想要的或者需要它们成为什么状态。我们希望任何的数据库都遵循三范式或者BCNF范式,它们不都会遵循。并且如果有可能有一个单独的数据库映射于所有应用都使用就更好了,但是不会存在这样的数据库。Result Maps是解决这个问题的答案,mybatis提供了解决这个问题的方法。
例如,我们应该怎么映射语句?
<!-- Very Complex Statement -->
<select id="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>
或许你想把它映射到一个智能对象模型,它被Author所写并且由Blog组成,而且有很多邮件,每个都有零个或者很多的评论和标签。下面是一个复杂的resultMap的完整的例子(假设aliases有这些Author, Blog, Post, Comments and Tags类型)。看一看这个,但是不要过于担忧,我们将经历每个步骤。虽然它可能第一次看到的时候很困难,但是其实很简单。
<!-- Very Complex Result Map -->
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
<result property="favouriteSection" column="author_favourite_section"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>
resultMap 元素有大量的父元素和一些值得讨论的结构。以下是resultmap元素的概念视图。
resultMap
构造器:使用来将一个类的实例化注入结果集到构造器中。
idArg参数:ID参数;把结果集当做ID来帮助改善全部的执行。
arg:一个正常的结果被注入到了构造器中。
id:一个ID结果;把结果集当做ID来帮助改善全部的执行。
result:一个正常的结果被注入到一个属性或者javaBean的相关属性,一个复杂类型的联系;大多的结果集会回滚到这个类型中。
嵌套结果映射: resultMap自己的联系或者可以引用其中某个。
collection-一个复杂类型的集合。
嵌套结果映射: 集合都是resultMap类型自己或者引用其中的。
discriminator :使用一个结果值来决定使用哪个resultMap。
case:一个是结果集映射基于一些值的情况
嵌套结果集映射:结果集自身映射的情况,因此可以包含很多相同的元素,或者他可以引用一个内部的resultMap。
你最好的实践就是逐渐的建立ResultMaps。此处可以使用单元测试解决这个问题。如果你尝试一次创建一个巨大的resultMap,你可能会出错,而且很难很好的运行。从简单的开始,每次都进行一点的提升。使用框架的下降趋势就是他们有时候有黑盒测试。你最好的赌注,以确保你写一个单元测试来确保实现你打算的代码操作。当有bug出现的时候它可以帮助你解决这个问题。
接下来的模块将会带你看看每个元素的更多细节。
id & result
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
这些都是结果映射最基础的东西。id和result映射一个单独的列值到一个单独的属性或者域是一个简单的数据类型。
两者之间仅有的不同就是当比较对象实例的时候,id会把结果当做一个标识属性来应用。这个可以帮助提高一般的执行,但是当缓存执行,且结果集映射的时候。
每个都有大量的属性:
Id and Result Attributes
支持的JDBC类型
对于将来的引用,mybatis支持下面JDBC类型,下面列举类JDBC的枚举类型:
构造器
<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
</constructor>
当属性对大多的数据传输对象类型的类起作用时,大多数的domain模型,你想要使用不可变的类的地方或许会有一些例子。通常表包含引用或者查找数据,这种方式很少的或者从来不可以改变那些适用的不变的类。在没有在外面暴露公共方法的时候,构造器注入允许你在类中设置值,以此来实例化。mybatis也支持私有属性和私有的JavaBean属性来实现,但是一些人更愿意使用构造器注射。constructor元素可以做到这点。
考虑下面的构造器:
public class User {
//...
public User(int id, String username) {
//...
}
//...
}
为了在构造器中注入结果集,mybatis需要通过参数的类型来识别构造器。Java没有办法来内部查看()或者反射)到参数的名字。所以,当创建一个constructor元素的时候,确定参数是有序的,数据类型是具体的。
<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
</constructor>
属性的其余部分和规则对于规则id和result元素都是相同的:
association
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>
association 元素处理带有"has-one"类型的关系。例如,在我们的例子中,Blog有一个Author,关联映射主要像其他结果一样.。如果是必要的,你可以指定目标属性和属性的javaType(mybatis在大多时间都是可以理解的),而且如果你想重写取回的结果集的值,你需要 一个类型处理类。
联系的不同之处在于你需要告诉mybatis怎么加载该联系。mybatis可以用两种方式来处理:
(1)嵌套查询:通过执行另一个映射SQL语句,返回希望得到的复杂类型。
(2)嵌套结果:通过使用嵌套结果映射来处理结果集连接的重复设置。
首先,让我们来检查一下元素的属性。正如你将看到的,它与正常的结果集映射是有区别的,是通过select和resultMap来体现的。
aaaa
嵌套查询的联系
例如:
<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>
<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>
我们有两条查询语句:一个用来加载Blog,另外一个用来加载Author。 Blog’s的resultMap描述了查询Author的语句,该语句应该被用来加载它的author 属性。
假设当他们列和属性的名字匹配的时候,所有其它的属性将会被自动加载。
然而这个方法是简单的,对于大数据集合或者列表它是不可以很好的执行的。这个问题以"n+1查询问题"而著称。
一言以蔽之n+1的查询会造成类似的问题:
你执行一个单独的SQL语句来取记录列表的值(+1)。
对于每个返回的记录,你可以执行一条查询语句来为每个加载详细的(称之为N)。
这个问题可以导致数以百计或者数以千计的SQL语句被执行。这不总是我们想要的结果。
好的一面就是mybatis可以进行懒加载这样的查询,因此你一次或许会执行这些语句。然而,如果你加载了这样的一个list,接着立即迭代它去访问内部的数据,你将会借助所有的懒加载,因此执行情况会很糟糕。
正因为如此,所以这里有另外一种方式。
And so, there is another way.
嵌套结果集的联系
你在上面已经看到过一个嵌套联系的复杂例子。下面是一个更加简单的例子来验证它是如何工作的。而不是执行一个分离的语句,我们会把blog和author表连接在一起,像下面这样:
<select id="selectBlog" resultMap="blogResult">
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
from Blog B left outer join Author A on B.author_id = A.id
where B.id = #{id}
</select>
注意连接,确保所有的结果都用了应独一无二且清晰的别名。这会使得映射更加简单。现在我们可以映射结果集了:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" resultMap="authorResult" />
</resultMap>
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>
在上面的例子中你可以看到Blog’s “author” 联系代理到 “authorResult” 的resultMap来加载Author 实例。
非常重要:id元素在嵌套结果映射中有着重要的角色。你应该一直指定一个或者更多可以被用来独一无二的识别结果集的属性。事实的真相就是如果你忽略了,mybatis会仍然起作用,但是执行会处于一种严重的状态消耗。
尽可能选择一些可以独立识别结果的属性。最原始的key就是明显的选择(尽管是综合的)。
现在,上面的例子使用了一个内部的resultMap元素来映射联系。这使得Author 的resultMap 可重用。然而,如果你不需要重用它,或者你更喜欢简化且让你的结果映射位于一处到单独的描述性的resultMap,你可以嵌套结果集映射的联系。下面是使用了这种方法的同一个例子:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</association>
</resultMap>
如果blog有一个co-author会怎么样?select语句会像这样:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
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,
CA.id as co_author_id,
CA.username as co_author_username,
CA.password as co_author_password,
CA.email as co_author_email,
CA.bio as co_author_bio
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Author CA on B.co_author_id = CA.id
where B.id = #{id}
</select>
为author重新调用resultMap
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>
因为在结果集中列的名字和在resultMap中列名字定义是不同的,为了映射co-author的结果集,你需要制定一个columnPrefix 来使用resultMap 。
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author"
resultMap="authorResult" />
<association property="coAuthor"
resultMap="authorResult"
columnPrefix="co_" />
</resultMap>
多个结果集列表的联系
MyBatis 3.2.3版本开始提供了另一种解决n+1问题的方式。
一些数据库允许你存储存储过程来返回更多额结果集或者一次执行更多的语句,每次返回一个结果集。这个会被用来冲击数据库,再没有使用连接时,返回相关的数据。
在例子中,存储过程执行了下面的查询,返回两条结果。首先会包含Blogs,其次会包含Authors
SELECT * FROM BLOG WHERE ID = #{id}
SELECT * FROM AUTHOR WHERE ID = #{id}
通过添加一个resultSe属性来用逗号分离名字的列表映射语句的名字必须给每个结果一个名字。
<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
{call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>
现在我们可以指定数据来填充与“author”有联系的结果集。
<resultMap id="blogResult" type="Blog">
<id property="id" column="id" />
<result property="title" column="title"/>
<association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="email" column="email"/>
<result property="bio" column="bio"/>
</association>
</resultMap>
你已经知道了如何处理带有“has-one”类型的关系。但是“has-many”的呢?下面的模块会是这个主题
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>
collection元素几乎与该关联相同。实际上,它是如此相似,记录的相似性将是多余的。所以,让我们把重点放到不同上。
继续上面的例子,一个blog只有一个author,但是一个blog可以有很多个post。在blog这个类中,它会被一些类似于这样的所代表:
private List<Post> posts;
将一组嵌套结果映射到这样的列表中,我们使用collection元素。就像元素之间的联系,我们可以使用一个嵌套select或者通过join来嵌套的结果集
嵌套查询的collection
First, let’s look at using a nested select to load the Posts for the Blog.
首先,让我们看一下使用嵌套查询来为blog加载posts。
<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>
<select id="selectPostsForBlog" resultType="Post">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>
你需要注意到这里有很多的事情,但是对于大多数部分它和相关联的元素看起来是很相似的。首先,你会注意到我们正在使用collection元素。接着你会注意到,这里有一个ofType的属性。这个属性对于区别JavaBean属性和类型是很重要的。collection是包含这些的。所以你可以阅读下面的映射代码:
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
posts的集合是Post类型的一个ArrayList 。
javaType 属性是真的不是必要的,作为mybatis将会在大多的情况解决这个问题。所以你可以简化的缩短它。
<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>
嵌套的collection结果集
在这点,你或许可以猜测嵌套结果集如何在collection中运行,因为它和这个关联相同,但在ofType属性相同时添加的方法。
首先,我们看一下SQL:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
P.id as post_id,
P.subject as post_subject,
P.body as post_body,
from Blog B
left outer join Post P on B.id = P.blog_id
where B.id = #{id}
</select>
我们再次把Blog 和Post 表做了连接,确保简单映射结果列的标签。现在用Post 映射的collection映射Blog,如下所示:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
</resultMap>
在这里我们再次想到了元素id的重要性,如果你没有了解过你需要阅读一下上面相关的章节。
如果你喜欢更长的形式,那会允许你的result map有更多的重用,你可以选择使用下面的映射:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>
<resultMap id="blogPostResult" type="Post">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
</resultMap>
集合有多个resultSet
正如我们所做的联系,我们可以调用一个存储过程来执行两个查询并返回两个结果集,一个是Blog的,另外一个是Post的。
SELECT * FROM BLOG WHERE ID = #{id}
SELECT * FROM POST WHERE BLOG_ID = #{id}
通过添加一个resultSet属性,用逗号分隔集合的名字来映射语句,它们的名称必须指定到每一个result set。
<select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
{call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
</select>
我们指定了"posts"的collection,它会被结果集中叫做"posts"的数据所填充。
<resultMap id="blogResult" type="Blog">
<id property="id" column="id" />
<result property="title" column="title"/>
<collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
</collection>
</resultMap>
这里对于你映射的深度、宽度或者你集合之间联系的结合都是没有限制的。当映射的时候,你应该在意识上执行它们。单元测试和你应用程序的执行测试经历很长来发现你应用的最好的方法。最好的事情就是mybatis让你随后改变你的想法,对你的代码基本上没有影响。
高级联系和集合映射是一个深度主题。这篇文档只可以带你到这里。伴随着一点练习,它将会很快的变得清晰明白。
discriminator
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
有时候一个单独的数据库查询可能返回很多不同数据类型的结果集(但是希望有所涉及)。元素discriminator 被设计来处理这种情形和其他的情景,包含一个类的继承机制。该元素理解起来很简单,因为它与java中的switch语句很相似。
一个discriminator 定义column 的种类和属性javaType。该列就是mybatis查找值用来比较的。javaType 是需要用来进行相等性的测验。(虽然String将会可能对于任何情景都起作用)。例如:
<resultMap id="vehicleResult" type="Vehicle">
<id property="id" column="id" />
<result property="vin" column="vin"/>
<result property="year" column="year"/>
<result property="make" column="make"/>
<result property="model" column="model"/>
<result property="color" column="color"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultMap="carResult"/>
<case value="2" resultMap="truckResult"/>
<case value="3" resultMap="vanResult"/>
<case value="4" resultMap="suvResult"/>
</discriminator>
</resultMap>
在这个例子中,mybatis将会从结果集和比较它的传播类型值中取回每个记录。如果它与discriminator 的任何例子都匹配,那么它就会使用resultMap来指定。这个会被唯一的执行,所以换句话说,resultMap 的其余部分就会被忽略(除非它继承了,我们在第二个板块讨论过的)。如果没有任何实例相匹配,mybatis仅仅会使用resultMap 来定义discriminator 块外部的一些。因此,如果carResult 被按照下面这样声明:
<resultMap id="carResult" type="Car">
<result property="doorCount" column="door_count" />
</resultMap>
只有doorCount 属性会被加载。这样做是允许discriminator 实例,甚至和父resultMap没有关系的完全独立的分组。在这个例子中我们做一些练习来了解在cars 和vehicles之间存在一种关系。因此,我们想属性的其余部分也被加载了。resultMap的一个简单改变:
<resultMap id="carResult" type="Car" extends="vehicleResult">
<result property="doorCount" column="door_count" />
</resultMap>
现在vehicleResult 和carResult 的所有属性都会被加载。
再一次,一些人可能会发现这种在外部定义的映射稍微有点无趣。这个对于那些喜欢更简洁的映射风格的来说这里有一个可选择的语法。例如:
<resultMap id="vehicleResult" type="Vehicle">
<id property="id" column="id" />
<result property="vin" column="vin"/>
<result property="year" column="year"/>
<result property="make" column="make"/>
<result property="model" column="model"/>
<result property="color" column="color"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultType="carResult">
<result property="doorCount" column="door_count" />
</case>
<case value="2" resultType="truckResult">
<result property="boxSize" column="box_size" />
<result property="extendedCab" column="extended_cab" />
</case>
<case value="3" resultType="vanResult">
<result property="powerSlidingDoor" column="power_sliding_door" />
</case>
<case value="4" resultType="suvResult">
<result property="allWheelDrive" column="all_wheel_drive" />
</case>
</discriminator>
</resultMap>
注意:如果你压根不指定任何结果,那么这些都是Result Map。接着mybatis会为你自动匹配它们的列和属性。所以这些例子的大多数比它们实际需要的要更加冗余。也就是说,大多数数据库有点复杂,而且我们可以依赖所有的例子来说是不可能的。
自动映射
你在前面的章节中也看到过,在简单的例子中mybatis可以自动为你映射结果,其他的都需要你自己建立一个结果映射。但是你将会在这部分看到你也可以混合两种方式。让我们更加深入了解一下自动映射如何运行。
当自动映射结果集时,mybatis会取得列的名字而且在忽略实例的情况下寻找有相同名字的属性。那也就意味着如果一个列的名字是ID,属性也是id都被找到,mybatis会把ID列的值设置给id属性。
通常数据库的列使用大写字母来命名,强调单词和java属性之间往往遵循驼峰命名约定。为了让它们之间可以做到自动映射,需要设置 mapUnderscoreToCamelCase=true。
当有一个具体的结果集时,自动映射就会起作用。当这个事情发生的时候,对每一个结果映射来说,所有的列在resultMap中都是会出现,而且没有一个人工映射将会自动映射,接着手工映射将会被处理。下面的例子中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 进行配置:
<resultMap id="userResultMap" type="User" autoMapping="false">
<result property="password" column="hashed_password"/>
</resultMap>
缓存
mybatis包含一个强大的事务查询缓存特点,它是可以进行配置的。在mybatis 3中已经有很多缓存实现的改变来达到这个更加强大和更容易配置的目标。
只有本地session缓存可以被唯一的使用来为session存活期间缓存数据。为了达到一个全局第二层次的缓存,你仅仅需要在你的SQL映射文件中添加一行:
<cache/>
这个简单语句的作用如下所示:
在映射语句文件中所有来自select语句的结果都会被缓存
在映射语句文件中的所有insert,update和delete语句都会被清除缓存。
缓存会使用一个LRU算法来回收。
基于目录的任何时间的缓存不会被刷新(例如:无刷新间隔)
在list或者object中缓存会存储1024个引用。(无论查询方法返回什么)
该缓存会被当做读/写缓存,意味着再没有其他调用或者线程可选修改的干扰下,调用者对于对象取值并不会被共享,而且可以安全的修改。
这些所有的属性都是可以被通过cache元素来进行修改的,比如:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
更加高级的配置是创建一个FIFO缓存,它可以每隔60秒刷新一次,存储多大512隔对象或者集合的引用,并且对象返回只读的,因此修改它们可以造成在调用者在不同线程之间的冲突。
现有的回收的策略有:
LRU(Least Recently Used):移除长期不使用的对象。
FIFO(First In First Out):按照输入缓存的顺序移除对象。
SOFT (Soft Reference软引用):移除基于垃圾收集器状态和软引用的对象。
WEAK (Weak Reference弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认的是LRU回收。
刷新时间间隔可以被设置为任意的正整数,而且应该代表一个合理的时间量在毫秒级别。默认是不设置的,因此时间刷新间隔被使用,而且缓存仅仅可以被调用的语句刷新。
大小可以被设置为任意的正整数,记住你对象缓存的大小和你环境中可以被利用的存储资源。默认为1024。
只读属性可以被设为true或者false。一个只读缓存将会返回相同的缓存对象的实例给所有的调用者。尽管它提供了一个有意义的执行的特点,但是这种对象不应该被修改。一个读写缓存将会返回一个缓存对象的复制。这个虽然更慢,但是更加安全,因此read-only属性默认为false。
注意:二级缓存是事务。那也意味着当一个SqlSession以commit或者以rollback结束,但是在执行的时候,flushCache=true,且没有insert,delete,update,表示它是可更新的。
使用一个传统的缓存
为了以这些方式定制缓存,你可以通过实现你自己的缓存来完全重写缓存的行为,或者创建一个适配器来解决第三级缓存。
<cache type="com.domain.something.MyCustomCache"/>
这个例子示范了如何使用一个传统的缓存实现。在type属性中class必须实现org.apache.ibatis.cache.Cache接口,并且提供一个带有String类型参数id的构造器。这个接口是mybatis框架中更加复杂中的一个,但是下面讲的很简单。
public interface Cache {
String getId();
int getSize();
void putObject(Object key, Object value);
Object getObject(Object key);
boolean hasKey(Object key);
Object removeObject(Object key);
void clear();
}
为了配置你的缓存,仅仅添加共有的JavaBean属性到你的缓存实现中,传递属性通过cache元素,下面的将会在你的缓存实现中调用一个setCacheFile(String file)方法:
<cache type="com.domain.something.MyCustomCache">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
你可以使用JavaBean属性的所有简单的类型,mybatis将会完成。你在配置属性中可指定一个占位符(例如:${cache.file})来替换你定义的值。
从3.4.2版本开始,mybatis一直支持在设置好所有属性后调用初始化方法。如果你想使用这个特性,你就需要在你传统的cache类中实现 org.apache.ibatis.builder.InitializingObject接口。
public interface InitializingObject {
void initialize() throws Exception;
}
注意:在这部分缓存的设置(类似缓存策略,读写等等)并不会被应用当你使用传统缓存的时候。
记住一个缓存的配置和缓存实例和SQL映射文件的命名空间有联系是很重要的。因此,所有的语句在相同的命名空间有相同的缓存。语句可以被修改,它们与缓存互动,或者通过使用两个简单的属性在statement-by-statement 的基础上完全的排除它们自己。语句会被像下面这样配置:
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
既然那个是默认的,你明显的不可以按照那个方式来配置语句。相反,如果你想改变默认的就仅仅需要设置flushCache 和useCache 属性。例如,在一些情况下,你或许想排除一个来自缓存的特殊查询结果,或者你想一个查询语句来刷新缓存。类似的,你或许有一些update语句,而且不需要刷新缓存来执行。
cache-ref
重新回顾前面的章节,对于语句,有相同命名空间的来说,这种特别的命名空间的缓存将被使用或者刷新。这里可能会有当你想在命名空间之间共享一个相同的缓存配置和实例的时候。在这种情况下你可以引用另一个缓存通过使用元素cache-ref。
<cache-ref namespace="com.someone.application.data.SomeMapper"/>