MyBatis的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的XML文件就显得相对简单。如果拿它跟具有相同功能的JDBC代码进行对比,你会立即发现省掉了将近95%的代码。MyBatis就是针对SQL构建的,并且比普通的方法做的更好。
SQL映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):
cache–给定命名空间的缓存配置。cache-ref–其他命名空间缓存配置的引用。resultMap–是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。parameterMap–已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。sql–可被其他语句引用的可重用语句块。insert–映射插入语句update–映射更新语句delete–映射删除语句select–映射查询语句
下一部分将从语句本身开始来描述每个元素的细节。
select
查询语句是MyBatis中最常用的元素之一,光能把数据存到数据库中价值并不大,如果还能重新取出来才有用,多数应用也都是查询比修改要频繁。对每个插入、更新或删除操作,通常对应多个查询操作。这是MyBatis的基本原则之一,也是将焦点和努力放到查询和结果映射的原因。简单查询的select元素是非常简单的。比如:
这个语句被称作selectPerson,接受一个int(或Integer)类型的参数,并返回一个HashMap类型的对象,其中的键是列名,值便是结果行中的对应值。
注意参数符号:#{id}
这就告诉MyBatis创建一个预处理语句参数,通过JDBC,这样的一个参数在SQL中会由一个“?”来标识,并被传递到一个新的预处理语句中,就像这样:
当然,这需要很多单独的JDBC的代码来提取结果并将它们映射到对象实例中,这就是MyBatis节省你时间的地方。我们需要深入了解参数和结果映射,细节部分我们下面来了解。
select元素有很多属性允许你配置,来决定每条语句的作用细节。
insert,update和delete
数据变更语句insert,update和delete的实现非常接近:
下面就是insert,update和delete语句的示例:
如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,而且有多种生成方式。
首先,如果你的数据库支持自动生成主键的字段(比如MySQL和SQLServer),那么你可以设置useGeneratedKeys=”true”,然后再把keyProperty设置到目标属性上就OK了。例如,如果上面的Author表已经对id使用了自动生成的列类型,那么语句可以修改为:
如果你的数据库还支持多行插入,你也可以传入一个Authors数组或集合,并返回自动生成的主键。
对于不支持自动生成类型的数据库或可能不支持自动生成主键的JDBC驱动,MyBatis有另外一种方法来生成主键。
这里有一个简单(甚至很傻)的示例,它可以生成一个随机ID(你最好不要这么做,但这里展示了MyBatis处理问题的灵活性及其所关心的广度):
在上面的示例中,selectKey元素将会首先运行,Author的id会被设置,然后插入语句会被调用。这给你了一个和数据库中来处理自动生成的主键类似的行为,避免了使Java代码变得复杂。
selectKey元素描述如下:
sql
这个元素可以被用来定义可重用的SQL代码段,可以包含在其他语句中。它可以被静态地(在加载参数)参数化.不同的属性值通过包含的实例变化.比如:
这个SQL片段可以被包含在其他语句中,例如:
属性值也可以被用在include元素的refid属性里(
)或include内部语句中(
),例如:
参数(Parameters)
前面的所有语句中你所见到的都是简单参数的例子,实际上参数是MyBatis非常强大的元素,对于简单的做法,大概90%的情况参数都很少,比如:
上面的这个示例说明了一个非常简单的命名参数映射。参数类型被设置为int,这样这个参数就可以被设置成任何内容。原生的类型或简单数据类型(比如整型和字符串)因为没有相关属性,它会完全用参数值来替代。然而,如果传入一个复杂的对象,行为就会有一点不同了。比如:
如果User类型的参数对象传递到了语句中,id、username和password属性将会被查找,然后将它们的值传入预处理语句的参数中。
这点相对于向语句中传参是比较好的,而且又简单,不过参数映射的功能远不止于此。
首先,像MyBatis的其他部分一样,参数也可以指定一个特殊的数据类型。
像MyBatis的剩余部分一样,javaType通常可以由参数对象确定,除非该对象是一个HashMap。这时所使用的TypeHandler应该明确指明javaType。
NOTE如果一个列允许null值,并且会传递值null的参数,就必须要指定JDBCType。阅读PreparedStatement.setNull()的JavaDocs文档来获取更多信息。
为了以后定制类型处理方式,你也可以指定一个特殊的类型处理器类(或别名),比如:
尽管看起来配置变得越来越繁琐,但实际上,很少需要去设置它们。
对于数值类型,还有一个小数保留位数的设置,来确定小数点后保留的位数。
最后,mode属性允许你指定IN,OUT或INOUT参数。如果参数为OUT或INOUT,参数对象属性的真实值将会被改变,就像你在获取输出参数时所期望的那样。如果mode为OUT(或INOUT),而且jdbcType为CURSOR(也就是Oracle的REFCURSOR),你必须指定一个resultMap来映射结果集ResultMap到参数类型。要注意这里的javaType属性是可选的,如果留空并且jdbcType是CURSOR,它会被自动地被设为ResultMap。
MyBatis也支持很多高级的数据类型,比如结构体,但是当注册out参数时你必须告诉它语句类型名称。比如(再次提示,在实际中要像这样不能换行):
尽管所有这些选项很强大,但大多时候你只须简单地指定属性名,其他的事情MyBatis会自己去推断,顶多要为可能为空的列指定jdbcType。
字符串替换
默认情况下,使用#{}格式的语法会导致MyBatis创建PreparedStatement参数并安全地设置参数(就像使用?一样)。这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在SQL语句中插入一个不转义的字符串。比如,像ORDERBY,你可以这样来使用:
这里MyBatis不会修改或转义字符串。
NOTE用这种方式接受用户的输入,并将其用于语句中的参数是不安全的,会导致潜在的SQL注入攻击,因此要么不允许用户输入这些字段,要么自行转义并检验。
ResultMaps
resultMap元素是MyBatis中最重要最强大的元素。它可以让你从90%的JDBCResultSets数据提取代码中解放出来,并在一些情形下允许你做一些JDBC不支持的事情。实际上,在对复杂语句进行联合映射的时候,它很可能可以代替数千行的同等功能的代码。ResultMap的设计思想是,简单的语句不需要明确的结果映射,而复杂一点的语句只需要描述它们的关系就行了。
你已经见过简单映射语句的事例了,但没有明确的resultMap。比如:
上述语句只是简单地将所有的列映射到HashMap的键上,这由resultType属性指定。 虽然在大部分情况下都够用,但是HashMap不是一个很好的领域模型。 你的程序更可能会使用JavaBean或POJO(PlainOldJavaObjects,普通Java对象)作为领域模型。 MyBatis对两者都支持。看看下面这个JavaBean:
基于JavaBean的规范,上面这个类有3个属性:id,username和hashedPassword。这些属性会对应到select语句中的列名。
这样的一个JavaBean可以被映射到ResultSet,就像映射到HashMap一样简单。
类型别名是你的好帮手。使用它们,你就可以不用输入类的完全限定名称了。比如:
这些情况下,MyBatis会在幕后自动创建一个ResultMap,再基于属性名来映射列到JavaBean的属性上。如果列名和属性名没有精确匹配,可以在SELECT语句中对列使用别名(这是一个基本的SQL特性)来匹配标签。比如:
ResultMap最优秀的地方在于,虽然你已经对它相当了解了,但是根本就不需要显式地用到他们。上面这些简单的示例根本不需要下面这些繁琐的配置。出于示范的原因,让我们来看看最后一个实例中,如果使用外部的resultMap会怎样,这也是解决列名不匹配的另外一种方式。
引用它的语句使用resultMap属性就行了(注意我们去掉了resultType属性)。比如:
如果世界总是这么简单就好了。
高级结果映射
MyBatis创建的一个想法是:数据库不可能永远是你所想或所需的那个样子。我们希望每个数据库都具备良好的第三范式或BCNF范式,可惜它们不总都是这样。如果有一个独立且完美的数据库映射模式,所有应用程序都可以使用它,那就太好了,但可惜也没有。ResultMap就是MyBatis对这个问题的答案。
比如,我们如何映射下面这个语句?
你可能想把它映射到一个智能的对象模型,这个对象表示了一篇博客,它由某位作者所写,有很多的博文,每篇博文有零或多条的评论和标签。我们来看看下面这个完整的例子,它是一个非常复杂的ResultMap(假设作者,博客,博文,评论和标签都是类型的别名)。不用紧张,我们会一步一步来说明。虽然它看起来令人望而生畏,但其实非常简单。
resultMap元素有很多子元素和一个值得讨论的结构。下面是resultMap元素的概念视图。
resultMap
constructor-用于在实例化类时,注入结果到构造方法中idArg-ID参数;标记出作为ID的结果可以帮助提高整体性能arg-将被注入到构造方法的一个普通结果id–一个ID结果;标记出作为ID的结果可以帮助提高整体性能result–注入到字段或JavaBean属性的普通结果association–一个复杂类型的关联;许多结果将包装成这种类型嵌套结果映射–关联可以指定为一个resultMap元素,或者引用一个collection–一个复杂类型的集合嵌套结果映射–集合可以指定为一个resultMap元素,或者引用一个discriminator–使用结果值来决定使用哪个resultMap嵌套结果映射–一个case也是一个映射它本身的结果,因此可以包含很多相 同的元素,或者它可以参照一个外部的resultMap。case–基于某些值的结果映射
最佳实践最好一步步地建立结果映射。单元测试可以在这个过程中起到很大帮助。如果你尝试一次创建一个像上面示例那样的巨大的结果映射,那么很可能会出现错误而且很难去使用它来完成工作。从最简单的形态开始,逐步进化。而且别忘了单元测试!使用框架的缺点是有时候它们看上去像黑盒子(无论源代码是否可见)。为了确保你实现的行为和想要的一致,最好的选择是编写单元测试。提交bug的时候它也能起到很大的作用。
下一部分将详细说明每个元素。
idresult
这些是结果映射最基本的内容。id和result都将一个列的值映射到一个简单数据类型(字符串,整型,双精度浮点数,日期等)的属性或字段。
这两者之间的唯一不同是,id表示的结果将是对象的标识属性,这会在比较对象实例时用到。这样可以提高整体的性能,尤其是缓存和嵌套结果映射(也就是联合映射)的时候。
两个元素都有一些属性:
支持的JDBC类型
为了未来的参考,MyBatis通过包含的jdbcType枚举型,支持下面的JDBC类型。
构造方法
通过修改对象属性的方式,可以满足大多数的数据传输对象(DataTransferObject,DTO)以及绝大部分领域模型的要求。但有些情况下你想使用不可变类。通常来说,很少或基本不变的、包含引用或查询数据的表,很适合使用不可变类。构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis也支持私有属性和私有JavaBeans属性来达到这个目的,但有一些人更青睐于构造方法注入。constructor元素就是为此而生的。
看看下面这个构造方法:
为了将结果注入构造方法,MyBatis需要通过某种方式定位相应的构造方法。在下面的例子中,MyBatis搜索一个声明了三个形参的的构造方法,以java.lang.Integer,java.lang.Stringandint的顺序排列。
当你在处理一个带有多个形参的构造方法时,很容易在保证arg元素的正确顺序上出错。从版本3.4.3开始,可以在指定参数名称的前提下,以任意顺序编写arg元素。为了通过名称来引用构造方法参数,你可以添加
Param注解,或者使用-parameters编译选项并启用useActualParamName选项(默认开启)来编译项目。下面的例子对于同一个构造方法依然是有效的,尽管第二和第三个形参顺序与构造方法中声明的顺序不匹配。如果类中存在名称和类型相同的属性,那么可以省略javaType。
剩余的属性和规则和普通的id和result元素是一样的。
关联
关联元素处理“有一个”类型的关系。比如,在我们的示例中,一个博客有一个用户。关联映射就工作于这种结果之上。你指定了目标属性,来获取值的列,属性的java类型(很多情况下MyBatis可以自己算出来),如果需要的话还有jdbc类型,如果你想覆盖或获取的结果值还需要类型控制器。
关联中不同的是你需要告诉MyBatis如何加载关联。MyBatis在这方面会有两种不同的方式:
嵌套查询:通过执行另外一个SQL映射语句来返回预期的复杂类型。嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集。首先,然让我们来查看这个元素的属性。所有的你都会看到,它和普通的只由select和
resultMap属性的结果映射不同。
关联的嵌套查询
示例:
我们有两个查询语句:一个来加载博客,另外一个来加载作者,而且博客的结果映射描述了“selectAuthor”语句应该被用来加载它的author属性。
其他所有的属性将会被自动加载,假设它们的列和属性名相匹配。
这种方式很简单,但是对于大型数据集合和列表将不会表现很好。问题就是我们熟知的“N+1查询问题”。概括地讲,N+1查询问题可以是这样引起的:
你执行了一个单独的SQL语句来获取结果列表(就是“+1”)。对返回的每条记录,你执行了一个查询语句来为每个加载细节(就是“N”)。
这个问题会导致成百上千的SQL语句被执行。这通常不是期望的。
MyBatis能延迟加载这样的查询就是一个好处,因此你可以分散这些语句同时运行的消耗。然而,如果你加载一个列表,之后迅速迭代来访问嵌套的数据,你会调用所有的延迟加载,这样的行为可能是很糟糕的。
所以还有另外一种方法。
关联的嵌套结果
在上面你已经看到了一个非常复杂的嵌套关联的示例。下面这个是一个非常简单的示例来说明它如何工作。代替了执行一个分离的语句,我们联合博客表和作者表在一起,就像:
注意这个联合查询,以及采取保护来确保所有结果被唯一而且清晰的名字来重命名。这使得映射非常简单。现在我们可以映射这个结果:
在上面的示例中你可以看到博客的作者关联代表着“authorResult”结果映射来加载作者实例。
非常重要:id元素在嵌套结果映射中扮演着非常重要的角色。你应该总是指定一个或多个可以唯一标识结果的属性。实际上如果你不指定它的话,MyBatis仍然可以工作,但是会有严重的性能问题。在可以唯一标识结果的情况下,尽可能少的选择属性。主键是一个显而易见的选择(即使是复合主键)。
现在,上面的示例用了外部的结果映射元素来映射关联。这使得Author结果映射可以重用。然而,如果你不需要重用它的话,或者你仅仅引用你所有的结果映射合到一个单独描述的结果映射中。你可以嵌套结果映射。这里给出使用这种方式的相同实例:
如果blog有一个co-author怎么办? select语句将看起来这个样子:
再次调用Author的resultMap将定义如下:
因为结果中的列名与resultMap中的列名不同。 你需要指定columnPrefix去重用映射co-author结果的resultMap。
上面你已经看到了如何处理“有一个”类型关联。但是“有很多个”是怎样的?下面这个部分就是来讨论这个主题的。
集合
集合元素的作用几乎和关联是相同的。实际上,它们也很相似,文档的异同是多余的。所以我们更多