iBATIS In Action (四)使用映射语句
本章和下一章(执行非查询语句)将详细讨论在Data Map文件中创建和使用映射语句的相关内容。在本章中,我们将首先浏览映射语句的大体内容以及它们的前提知识。然后我们将解释如何使用映射语句从数据库中获取类型化的对象(typed object),以及如何传入参数来限制返回的数据(比如添加查询条件)。在第五章中,您将学习到如何使用映射语句来更新数据库。
4.1 使用基础
4.1.1 创建JavaBeans
译注:原书中本节的内容主要是关于JavaBeans的,相关术语与.NET中差别较大,因此略过,这里只给出一些基本的术语。
JavaBeans:按Sun的定义,JavaBeans是一种Java语言编写的可复用的组件。JavaBeans是一个Java类,它定义了一些属性,可以通过事件与其它Beans进行通信。
POJO:即Plain Old Java Object,引用Martin Fowler的话来说明POJO的定义:
“I've come to the conclusion that people forget about regular Java objects because they haven't got a fancy name - so while preparing for a talk Rebecca Parsons, Josh Mackenzie and I gave them one: POJO (Plain Old Java Object). A POJO domain model is easier to put together, quick to build, can run and test outside of an EJB container, and isn't dependent on EJB (maybe that's why EJB vendors don't encourage you to use them.)”
POCO:即Plain Old CLR Object。在.NET中,POJO就不再适用了,那么把Java替换为CLR,就是POCO了(我只在某篇文章中看到过,在网上搜不到的…)。
Domain Object:领域对象,这是一个很有争议的词。前段时间园子里曾就此发生过激烈的争论,另外在javaeye上也有一篇关于它的讨论,相信很多人都看过了。
通过Martin Fowler的话,我们可以看到POJO的来由,它提供了一个”标准”术语,使得人们对此类对象有了清楚的认识,并且开发人员在讨论问题时找到了”共同语言”。.NET中也需要这样的术语,所以在翻译过程中,我将POJO都对应于POCO。
另外,在使用iBATIS操作数据库时,对象的属性(Property)显得尤为重要。这里提一点,注意属性的命名规范,建议使用Pascal case(Java中使用Camel case)。
4.1.2 SqlMap的API
在对JavaBeans有所理解之后,我们将开始查看iBATIS提供的API。ISqlMapper接口有30多个方法,我们将在后面的章节中一一了解。现在,先来看看本章将要用到的几个。
QueryForObject()方法
object QueryForObject(string statementName, object parameterObject, object resultObject);
T QueryForObject<T>(string statementName, object parameterObject);
T QueryForObject<T>(string statementName, object parameterObject, T instanceObject);
首先可以把它们分为两种:泛型和非泛型。每一种又分别有两个版本,第一个版本更为常用,它会使用默认构造函数创建一个新的对象返回(否则会在运行时抛出异常);第二个版本接受一个对象将其作为返回值——在执行映射语句后,会设置它的相应属性,而不是创建新的对象。
如果我们要使用的对象不容易创建,比如缺少缺省的构造函数,或者构造函数是受保护的(protected),那么第二种版本(接受三个参数)会很有用。
需要注意的是,调用QueryForObject()方法时,如果查询结果多于一行,那么iBATIS只会接受第一行记录,其它的记录则忽略不计。
QueryForList() 方法
void QueryForList(string statementName, object parameterObject, IList resultObject);
IList QueryForList(string statementName, object parameterObject, int skipResults, int maxResults);
IList<T> QueryForList<T>(string statementName, object parameterObject);
void QueryForList<T>(string statementName, object parameterObject, IList<T> resultObject);
IList<T> QueryForList<T>(string statementName, object parameterObject, int skipResults, int maxResults);
QueryFoList()方法用于数据库中获取一行或多行数据,将其转化为POCO的列表返回,与QueryForObject()方法类似,QueryFoList()方法也分泛型和非泛型之分。每一种有三个版本,这里以泛型的三个版本进行说明。
第一个版本将映射语句的所有结果转化为对象返回;第二个版本与第一个类似,只是将结果存放在参数resultObject中返回;第三个版本返回查询结果的一个子集——跳过skip参数指定数目的记录,然后返回max参数指定数目的结果。因此,如果映射语句本来返回的是100行记录,但您只需要11-20行,那么只需要将skip和max参数分别设置为10、10即可。
译注:上面说的第三个版本在开发中很有用,结合ObjectDataSource会更强大,请参看我曾写过的一篇小文。
QueryForMap() 方法
QueryForMap()方法从数据库获取一行或多行记录,将结果转换为C#对象,然后放在一个IDictionary(而不是IList)中返回。它有两个版本:
IDictionary QueryForMap(string statementName, object parameterObject, string keyProperty, string valueProperty);
第一个版本会执行一个查询,返回一个包含对象的IDictionary(键值对),IDictionary的key由keyProperty指定,其value则是由对应的记录行转换来的对象。第二个版本与第一个类似,但它的value是由valueProperty指定的,而不是整个对象。
该方法值得一说。来考虑一种情况:我们有一条映射语句,它返回一组账户(account)信息。使用第一个方法,使用第一个版本,我们将得到一个IDictionary,它的accountId为key,而整个account对象则作为value。使用第二个版本,则可以得到另一个IDictionary,它的accountId为key,只将accountName作为value:
accounts = sqlMapper.QueryForMap("Account.getAll", null, "accountId", "accountName");
译注:”Map”这个名称是Java里的,按.NET的精神,应该是Dictionary,所以iBATIS.NET提供了该方法的别名方法:QueryForDictionary。
现在,您已经了解了使用iBATIS所需要的各个API,接下来再看看创建映射语句的不同方式。
4.1.3 映射语句的类型
前面我们讨论了ISqlMapper的API的用法,别急,再使用这些API之前,还得了解一下如何创建各种映射语句。在上个例子中,我们执行了一条名称为Account.getAll的映射语句,但却没看到它是什么样子的(不过在第二章中,我们的示例中曾有过映射语句的例子),它实际上是一条<select>语句。
iBATIS中共有几种类型的映射语句,每一种都有自己的作用以及一组特性(attribute)和子元素。这似乎是很简单的,但一般情况下,我们最好使用最匹配的语句类型(比如,使用<insert>来插入数据,而不是使用<update>语句,也不要使用更通用<statement>语句),因为精确的语句类型更容易理解,在某些情况下还能提供额外的功能(就像使用<insert>时,它有一个<selectKey>子元素——我们将在5.2节中讨论)。
表4.1包含了各种类型语句的描述。
语句类型 |
特性 |
子元素 |
用途 |
详细内容参考 |
<select> |
id parameterClass resultClass listClass parameterMap resultMap cacheModel extends |
所有动态元素 |
查询数据 |
4.2节; |
<insert> |
id parameterClass parameterMap |
所有动态元素<selectKey> <generate> |
插入数据 |
5.2节; |
<update> |
id |
所有动态元素 |
更新数据 |
5.3节; |
<delete> |
id |
所有动态元素 |
删除数据 |
5.3节; |
<procedure> |
id |
所有动态元素 |
执行存储过程 |
5.5节; |
<statement> |
id parameterClass resultClass listClass parameterMap resultMap cacheModel |
所有动态元素 |
可以包含任意类型的语句,几乎无所不能 |
6.3.1节; |
在本章中,我们会只关注<select>类型的语句。
4.2 使用<select>语句
从数据库查询数据是应用程序最基本的用途之一。iBATIS框架使大部分情况下的SELECT语句变得简单,并提供了大量特性,可以满足您对访问数据库的任何需要。
4.2.1 在内联参数中使用#占位符
到目前为止,前面的所以示例都过于简单,我们很少会去执行没有条件的查询。如果要给映射语句添加查询条件,内联参数是一种简单的方法,它在使用时有两种方式。
第一种方式是使用#语法。下面这个简单的例子演示了如何传入一个简单的内联参数,然后通过accountId获取单个的Account对象:
select
accountId,
username,
password,
firstName,
lastName,
address1,
address2,
city,
state,
postalCode,
country
from Account
where accountId = #value#
</select>
这里的#value#字符串告诉iBATIS,该语句接受一个简单参数。这条语句可以如是调用:
Account account = sqlMapper.QueryForObject(“Account.getByIdValue”, 1) as Account;
好,让我们看一下执行这条语句时,iBATIS做了哪些事情。首先,它会查找id为Account.getByIdValue的语句,将#value#占位符转换为ADO.NET中的参数:accountId,
username,
password,
firstName,
lastName,
address1,
address2,
city,
state,
postalCode,
country
from Account
where accountId = @param0
接着,将参数的值设置为1(见上面代码中的QueryForObject()方法的第二个参数);最后执行这条语句。iBATIS获取结果记录,将其映射为一个对象,然后返回。
尽管这些内容看起来是比较底层的信息,但我们应当理解它。
最常遇到的问题之一便是:“我在WHERE子句中如何使用LIKE?”。看看前面那条语句,显然输入参数应当含有通配符(wildchar),而且也不太容易将其插入到SQL语句中。这个问题有三种解决方案:
输入参数的值含有SQL中的通配符(如T-SQL中的%)。
要搜索的文本是可被参数化的SQL表达式(如'%' + #value# + '%')的一部分。
使用文本替换方法(这就是下一个主题的内容了,见4.2.2)。
4.2.2 在内联参数中使用$占位符
内联参数的另一种方式是使用文本替换($)语法,它将一个值直接插入SQL。使用这种方法时要当心,因为它会带来SQL注入的危险,如果滥用,还会带来性能问题。
这也是使用LIKE操作符的一种方法。看这个例子:
select
accountId,
username,
password,
firstName,
lastName,
address1,
address2,
city,
state,
postalCode,
country
from Account
where city like ‘%$value$%’
</select>
这条语句跟前面那个(使用#的)的区别在于iBATIS处理输入参数的方式。执行这条语句所需的代码是一样的:
accountList = sqlMapper.QueryForList(“Account.getByLikeCity”, “burg”);
这一次,iBATIS将语句转化为:
accountId,
username,
password,
firstName,
lastName,
address1,
address2,
city,
state,
postalCode,
country
from Account
where city like‘%$burg$%’
没有设置任何参数,因为语句已经是完整的了。需要再次强调的是,使用这种方法会让我们的程序更易受到SQL注入的攻击。
4.2.3 关于SQL注入
所谓SQL注入攻击,是指一些恶意用户向应用程序输入一些特殊形式的数据,使得程序执行一些意想不到的行为。例如,在上面的语句(4.2.2)中,如果用户输入的值是:
它会将我们的select语句转化为这样“邪恶”的形式:
accountId,
username,
password,
firstName,
lastName,
address1,
address2,
city,
state,
postalCode,
country
from Account
where city like ‘%burg’; drop table Account; --%’
这下好了,我们的那些聪明的用户从数据库中查询出了所有以burg结尾的记录,这还没什么大问题。但他还从数据库删除了一张表(如果只是一个,也算幸运了——如果他真的够聪明,他会知道机会难得,从而会试着删除多张表)。字符串末尾的“--”告诉数据库忽略drop语句后的所有内容,因此该语句不会抛出异常。
如果是由于你的代码问题,让这种事情发生在生产环境中的真实应用程序中,那么这一天你在办公室里就不太好过了。就像我们提到过的,谨慎使用替换($)语法。
4.2.4 自动结果映射
您可能已经注意到,我们在前面的例子中没有定义result map,而是使用了result class。iBATIS的自动结果映射可以让语句正常工作,它会自动创建一个result map,并在执行时将其应用到语句上。
共有三种方式来使用这个特性:单列查询,固定列列表查询以及动态列列表查询。
如果您只想从查询中获取单个列的值,可以使用自动结果映射:
Select accountId from Account
</select>
IList<int> accountIds = sqlMapper.QueryForList<int>(“Account.getAllAccountIdValues”, null);
这条语句会返回Account表中所有的accountId值。
如果您需要多个列的值,使用自动结果映射可以将列名作为POCO的属性名或IDictionary的key。
如果是映射到POCO,需要注意:如果存在一列,它存在于数据库中,但不在对象中(没有与之对应的属性),那么不会有任何的错误、警告和数据——数据将被悄无声息地忽略。如果是映射到IDictionary,问题也是类似的,尽管可以取到数据,却不在期望的地方。
如果您希望使用更好的映射方式,请参看“使用外部结果映射”一节(4.3.1)。
尽管存在这两个潜在的问题,自动映射仍有可用之处。
如果查询所得的字段列表在运行时会发生改变,动态的结果映射也可能会用到。代码清单4.2 演示了这种情况:
select
accountId,
username,
<dynamic>
<isEqual property="includePassword" compareValue="true" >
password,
</isEqual>
</dynamic>
firstName,
lastName
from Account
<dynamic prepend=" where ">
<isNotEmpty property="city">
city like #city#
</isNotEmpty>
<isNotNull property="accountId" prepend=" and ">
accountId = #accountId#
</isNotNull>
</dynamic>
</select>
这里的例子用到了动态SQL(要到第8章才会讲到),属性includePassword决定了结果所包含的列。取决于includePassword的值,结果会包含或不包含password列。要注意的一点是,每次执行都要决定结果映射方式会带来性能损失,因此建议仅在绝对需要的时候才使用这种方法。
4.2.5 连接相关的数据(Joining related data)
有时我们需要连接多张数据表,将结果放入一个扁平(flatten-out)的结构,用作报表或其它目的。iBATIS框架使此过程变得简单,因为它本来就是将SQL语句映射为对象,而不是从表到对象。从字面上来看,对单表的<select>和多表的<select>进行映射没什么不同。
在第7章,我们将了解如何进行更高级的多表操作,将从表的数据映射为对象的子对象——如一个订单(order)的明细列表(order detail)。
现在,我们要简单地重申,从字面上来看,对单表的<select>和多表的<select>进行映射没什么不同。
我们已经讨论了,为何说SQL类似于一个函数(function),它有自己的输入值,基于这些输入值,它能够产生输出值。在下一节中,我们来看看如何提供这些输入值。