至此,你可能想到这一节要讲什么了。我提到了其他的数据库,也谈到各个数据库中会以不同的方式实现特性。除了一些只读应用外,我的观点是:要构建一个完全数据库独立的应用,而且是高度可扩缩的应用,是极其困难的。实际上,这几乎不可能,除非你真正了解每个数据库具体如何工作。另外,如果你清楚每个数据库工作的具体细节,就会知道,数据库独立性可能并不是你真正想要的(这个说法有点绕!)。
例如,再来看最早提到的资源调度例子(增加FOR UPDATE子句之前)。假设在另一个数据库上开发这个应用,这个数据库有着与Oracle完全不同的锁定/并发模型。我想说的是,如果把应用从一个数据库移植到另一个数据库,就必须验证它在完全不同的环境下还能正常地工作,而且为此我们要做大幅修改!
假设把这个资源调度应用部署在这样一个数据库上,它采用了阻塞读机制(读会被写阻塞)。现在业务规则通过一个数据库触发器实现(在INSERT之后,但在事务提交之前,我们要验证表中对应特定时间片的记录只有一行,也就是刚插入的记录)。在阻塞读系统中,由于有这种新插入的数据,所以表的插入要串行完成。第一个人插入他(她)的请求,要在星期五的下午2:00到下午3:00预订“房间A”,然后运行一个查询查看有没有重叠的预订。下一个人想插入一个重叠的请求,查找重叠情况时,这个请求会被阻塞(它发现有新插入的数据,但要等待直到这些数据确实可以读取)。在这个采用阻塞读机制的数据库中,我们的应用显然可以正常工作(不过如果两个人都插入自己的行,然后试图读对方的数据,就有可能得到一个死锁,这个概念将在第6章讨论),但不能并发工作,因为我们是一个接一个地检查是否存在重叠的资源分配。
如果把这个应用移植到Oracle,并简单地认为它也能同样地工作,结果可能让人震惊。由于Oracle会在行级锁定,并提供了非阻塞读,所以看上去一切都乱七八糟。如前所示,必须使用FOR UPDATE子句来完成串行访问。如果没有这个子句,两个用户就可能同时调度同一个资源。如果不了解所用数据库在多用户环境中如何工作,就会导致这样的直接后果。
将应用从数据库A移植到数据库B时,我时常遇到这种问题:应用在数据库A上原本无懈可击,到了数据库B上却不能工作,或者表现得很离奇。看到这种情况,我们的第一个想法往往是,数据库B是一个“不好的”数据库。而真正的原因其实是数据库B的工作方式完全不同。没有哪个数据库是错的或“不好的”,它们只是有所不同而已。应当了解并理解它们如何工作,这对于处理这些问题有很大的帮助。将应用从Oracle移植到SQL Server时,也会暴露SQL Server的阻塞读和死锁问题,换句话说,不论从哪个方向移植都可能存在问题。
例如,有人请我帮忙将一些Transact-SQL(T-SQL,SQL Server的存储过程语言)转换为PL/SQL。做这个转换的开发人员一直在抱怨Oracle中SQL查询返回的结果是“错的”。查询如下所示:

![]()
这个查询的目标是:在T表中,如果不满足某个条件,则找出x为NULL的所有行;如果满足某个条件,就找出x等于某个特定值的所有行。
开发人员抱怨说,在Oracle中,如果L_SOME_VARIABLE未设置为一个特定的值(仍为NULL),这个查询居然不返回任何数据。但是在Sybase或SQL Server中不是这样的,查询会找到将x设置为NULL值的所有行。从Sybase或SQL Server到Oracle的转换中,几乎都能发现这个问题。SQL采用一种三值逻辑来操作,Oracle则是按ANSI SQL的要求来实现NULL值。基于这些规则的要求,x与NULL的比较结果既不为true也不为false,也就是说,实际上,它是未知的(unknown)。从以下代码可以看出我的意思:

第一次看到这些结果可能会被搞糊涂。这说明,在Oracle中,NULL与NULL既不相等,也不完全不相等。默认情况下,SQL Server则不是这样处理;在SQL Server和Sybase中,NULL就等于NULL。不能说Oracle的SQL处理是错的,也不能说Sybase或SQL Server的处理不对,它们只是方式不同罢了。实际上,所有这些数据库都符合ANSI,但是它们的具体做法还是有差异。有许多二义性、向后兼容性等问题需要解决。例如, SQL Server也支持ANSI方法的NULL比较,但这不是默认的方式(如果改成ANSI方法的NULL比较,基于SQL Server构建的数千个遗留应用就会出问题)。
在这种情况下,一种解决方案是编写以下查询:

不过,这又会带来另一个问题。在SQL Server中,这个查询会使用x上的索引。Oracle中却不会这样,因为B*树索引不会对一个完全为NULL的项加索引(索引技术将在第12章介绍)。因此,如果需要查找NULL值,B*树索引就没有什么用处。
这里,为了尽量减少对代码的影响,我们的做法是赋给x某个值,不过这个值并没有实际意义。在此,根据定义可知,x的正常值是正数,所以可以选择 –1。这样一来,查询就变成:
![]()
由此创建一个基于函数的索引:
![]()
只需做最少的修改,就能在Oracle中得到与SQL Server同样的结果。从这个例子可以总结出以下几个要点:
q 数据库是不同的。在一个数据库上取得的经验也许可以部分应用于另一个数据库,但是你必须有心理准备,二者之间可能存在一些基本差别,可能还有一些细微的差别。
q 细微的差别(如对NULL的处理)与基本差别(如并发控制机制)可能有同样显著的影响。
q 应当了解数据库,知道它是如何工作的,它的特性如何实现,这是解决这些问题的惟一途径。
常有开发人员问我如何在数据库中做某件特定的事情(通常这样的问题一天不止一个),例如“如何在一个存储过程中创建临时表?”对于这些问题,我并不直接回答,而是反过来问他们“你为什么想那么做?”给我的回答常常是:“我们在SQL Server中就是用存储过程创建临时表,所以在Oracle中也要这么做。”这不出我所料,所以我的回答很简单:“你根本不是想在Oracle中用存储过程创建临时表,你只是以为自己想那么做。”实际上,在Oracle中这样做是很不好的。在Oracle中,如果在存储过程中创建表,你会发现存在以下问题:
q DDL操作会阻碍可扩缩性。
q DDL操作的速度往往不快。
q DDL操作会提交事务。
q 必须在所有存储过程中使用动态SQL而不是静态SQL来访问这个表。
q PL/SQL的动态SQL没有静态SQL速度快,或者说没有静态SQL优化。
关键是,即使真的需要在Oracle中创建临时表,你也不愿意像在SQL Server 中那样在过程中创建临时表。你希望在Oracle中能以最佳方式工作。反过来也一样,在Oracle中,你会为所有用户创建一个表来共享临时数据;但是从Oracle移植到SQL Server时,可能不希望这样做,这会影响SQL Server的可扩缩性和并发性。所有数据库创建得都不一样,它们存在很大的差异。
1. 标准的影响
如果所有数据库都符合SQL99,那它们肯定一样。至少我们经常做这个假设。在这一节中,我将揭开它的神秘面纱。
SQL99是数据库的一个ANSI/ISO标准。这个标准的前身是SQL92 ANSI/ISO标准,而SQL92之前还有一个SQL89 ANSI/ISO标准。它定义了一种语言(SQL)以及数据库的行为(事务、隔离级别等)。你知道许多商业数据库至少在某种程度上是符合SQL99的吗?不过,这对于查询和应用的可移植性没有多大的意义,这一点你也清楚吗?
SQL92标准有4个层次:
q 入门级(Entry level)。这是大多数开发商符合的级别。这一级只是对前一个标准SQL89稍做修改。所有数据库开发商都不会有更高的级别,实际上,美国国家标准和技术协会NIST(National Institute of Standards and Technology,这是一家专门检验SQL合规性的机构)除了验证入门级外,甚至不做其他的验证。Oracle 7.0于1993年通过了NIST的SQL92入门级合规性验证,那时我也是小组中的一个成员。如果一个数据库符合入门级,它的特性集则是Oracle 7.0的一个功能子集。
q 过渡级。这一级在特性集方面大致介于入门级和中间级之间。
q 中间级。这一级增加了许多特性,包括(以下所列并不完整):
n 动态SQL
n 级联DELETE以保证引用完整性
n DATE和TIME数据类型
n 域
n 变长字符串
n CASE表达式
n 数据类型之间的CAST函数
q 完备级。增加了以下特性(同样,这个列表也不完整):
n 连接管理
n BIT串数据类型
n 可延迟的完整性约束
n FROM子句中的导出表
n CHECK子句中的子查询
n 临时表
入门级标准不包括诸如外联结(outer join)、新的内联结(inner join)语法等特性。过渡级则指定了外联结语法和内联结语法。中间级增加了更多的特性,当然,完备级就是SQL92全部。有关SQL92的大多数书都没有区别这些级别,这就会带来混淆。这些书只是说明了一个完整实现SQL92的理论数据库会是什么样子。所以无论你拿起哪一本书,都无法将书中所学直接应用到任何SQL92数据库上。关键是,SQL92最多只达到入门级,如果你使用了中间级或更高级里的特性,就存在无法“移植”应用的风险。
SQL99只定义了两级一致性:核心(core)一致性和增强(enhanced)一致性。SQL99力图远远超越传统的“SQL”,并引入了一些对象—关系构造(数组、集合等)。它包括SQL MM(多媒体,multimedia)类型、对象—关系类型等。还没有哪个开发商的数据库经认证符合SQL99核心级或增强级,实际上,据我所知,甚至没有哪个开发商声称他们的产品完全达到了某级一致性。
对于不同的数据库来说,SQL语法可能存在差异,实现有所不同,同一个查询在不同数据库中的性能也不一样,不仅如此,还存在并发控制、隔离级别、查询一致性等问题。我们将在第7章详细讨论这些问题,并介绍不同数据库的差异对你会有什么影响。
SQL92/SQL99试图对事务应如何工作以及隔离级别如何实现给出一个明确的定义,但最终,不同的数据库还是有不同的结果。这都是具体实现所致。在一个数据库中,某个应用可能会死锁并完全阻塞。但在另一个数据库中,同样是这个应用,这些问题却有可能不会发生,应用能平稳地运行。在一个数据库中,你可能利用了阻塞(物理串行化),但在另一个数据库上部署时,由于这个数据库不会阻塞,你就会得到错误的答案。要将一个应用部署在另一个数据库上,需要花费大量的精力,付出艰辛的劳动,即使你100%地遵循标准也不例外。
关键是,不要害怕使用开发商特有的特性,毕竟,你为这些特性花了钱。每个数据库都有自己的一套“技巧”,在每个数据库中总能找到一种完成操作的好办法。要使用最适合当前数据库的做法,移植到其他数据库时再重新实现。要使用合适的编程技术,从而与这些修改隔离,我把这称为防御式编程(defensive programming)。
2. 防御式编程
我推崇采用防御式编程技术来构建真正可移植的数据库应用,实际上,编写操作系统可移植的应用时也采用了这种技术。防御式编程的目标是充分利用可用的工具,但是确保能够根据具体情况逐一修改实现。
可以对照来看,Oracle是一个可移植的应用。它能在许多操作系统上运行。不过,在Windows上,它就以Windows方式运行,使用线程和其他Windows特有的工具。在UNIX上,Oracle则作为一个多进程服务器运行,使用进程来完成Windows上线程完成的工作,也就是采用UNIX的方式运行。两个平台都提供了“核心Oracle”功能,但是在底层却以完全不同的方式来实现。如果你的数据库应用要在多个数据库上运行,道理也是一样的。
例如,许多数据库应用都有一个功能,即为每一行生成一个惟一的键。插入行时,系统应自动生成一个键。为此,Oracle实现了一个名为SEQUENCE的数据库对象。Informix有一个SERIAL数据类型。Sybase和SQL Server有一个IDENTITY类型。每个数据库都有一个解决办法。不过,不论从做法上讲,还是从输出来看,各个数据库的方法都有所不同。所以,有见识的开发人员有两条路可走:
q 开发一个完全独立于数据库的方法来生成惟一的键。
q 在各个数据库中实现键时,提供不同的实现,并使用不同的技术。
从理论上讲,第一种方法的好处是从一个数据库转向另一个数据库时无需执行任何修改。我把它称为“理论上” 的好处,这是因为这种实现实在太庞大了,所以这种方案根本不可行。要开发一个完全独立于数据库的进程,你必须创建如下所示的一个表:

然后,为了得到一个新的键,必须执行以下代码:

看上去很简单,但是有以下结果(注意结果不止一项):
q 一次只能有一个用户处理事务行。需要更新这一行来递增计数器,这会导致程序必须串行完成这个操作。在最好的情况下,一次只有一个人生成一个新的键值。
q 在Oracle中(其他数据库中的行为可能有所不同),倘若隔离级别为SERIALIZABLE,除第一个用户外,试图并发完成此操作的其他用户都会接到这样一个错误:“ORA-08177: can't serialize access for this transaction”(ORA-08177:无法串行访问这个事务)。
例如,使用一个可串行化的事务(在J2EE环境中比较常见,其中许多工具都自动将SERIALIZABLE用作默认的隔离模式,但开发人员通常并不知道),你会观察到以下行为。注意SQL提示符(使用SET SQLPROMPT SQL*Plus命令)包含了活动会话的有关信息:


下面,再到另一个SQL*Plus会话完成同样的操作,并发地请求惟一的ID:

此时它会阻塞,因为一次只有一个事务可以更新这一行。这展示了第一种可能的结果,即这个会话会阻塞,并等待该行提交。但是由于我们使用的是Oracle,而且隔离级别是SERIALIZABLE,提交第一个会话的事务时会观察到以下行为:
![]()
第二个会话会立即显示以下错误:

所以,尽管这个逻辑原本想做到独立于数据库,但它根本不是数据库独立的。取决于隔离级别,这个逻辑甚至在单个数据库中都无法可靠地完成,更不用说跨数据库了!有时我们会阻塞并等待,但有时却会得到一条错误消息。说得简单些,无论是哪种情况(等待很长时间,或者等待很长时间后得到一个错误),都至少会让最终用户不高兴。
实际上,我们的事务比上面所列的要大得多,所以问题也更为复杂。实际的事务中包含多条语句,上例中的UPDATE和SELECT只是其中的两条而已。我们还要用刚生成的这个键向表中插入行,并完成这个事务所需的其他工作。这种串行化对于应用的扩缩是一个很大的制约因素。如果把这个技术用在处理订单的网站上,而且使用这种方式来生成订单号,可以想想看可能带来的后果。这样一来,多用户并发性就会成为泡影,我们不得不按顺序做所有事情。
对于这个问题,正确的解决方法是针对各个数据库使用最合适的代码。在Oracle中,代码应该如下(假设表T需要所生成的主键):

其效果是为所插入的每一行自动地(而且透明地)指定一个惟一键。还有一种性能更优的方法:
![]()
也就是说,完全没有触发器的开销(这是我的首选方法)。
在第一个例子中,我们特意使用了各个数据库的特性来生成一个非阻塞、高度并发的惟一键,而且未对应用代码带来任何真正的改动,因为在这个例子中所有逻辑都包含在DDL中。
提示 在其他数据库中也可以使用其内置的特性或者生成惟一的数来达到同样的效果。CREATE TABLE语法可能不同,但是最终结果是一样的。
理解了每个数据库会以不同的方式实现特性,再来看一个支持可移植性的防御式编程的例子,这就是必要时将数据库访问分层。例如,假设你在使用JDBC进行编程,如果你用的都是直接的SQL(SELECT、INSERT、UPDATE和DELETE),可能不需要抽象层。你完全可以在应用程序中直接编写SQL,前提是只能用各个数据库都支持的构造,而且经验证,这些构造在不同数据库上会以同样的方式工作(还记得关于NULL=NULL的讨论吧!)。另一种方法的可移植性更好,而且可以提供更好的性能,就是使用存储过程来返回结果集。你会发现,每个开发商的数据库都可以从存储过程返回结果集,但是返回的方式不同。针对不同的数据库,要编写的具体源代码会有所不同。
这里有两个选择,一种做法是不使用存储过程返回结果集,另一种做法是针对不同的数据库实现不同的代码。我就坚持第二种做法,即针对不同的开发商编写不同的代码,而且大量使用存储过程。初看上去,另换一个数据库实现时这好像会增加开发时间。不过你会发现,在多个数据库上实现时,采用这种方法实际上容易得多。你不用寻找适用于所有数据库的最佳SQL(也许在某些数据库上表现好一些,但在另外一些数据库上可能并不理想),而只需实现最适合该数据库的SQL。这些工作可以在应用之外完成,这样对应用调优时就有了更大的灵活性。你可以在数据库自身中修正一个表现很差的查询,并立即部署所做的改动,而无需修改应用。另外,采用这种方法,还可以充分利用开发商提供的SQL扩缩。例如,Oracle在其SQL中提供了CONNECT BY操作,能支持层次查询。这个独有的特性对于处理递归查询很有意义。在Oracle中,你可以自由地使用这个SQL扩缩,因为它在应用“之外”(也就是说,隐藏在数据库中)。在其他数据库中,则可能需要使用一个临时表,并通过存储过程中的过程性代码才能得到同样的结果。既然你花钱购买了这些特性,自然可以充分地加以使用。
应用要在哪个数据库上部署,就针对这个数据库开发一个专用的代码层,这种技术与实现多平台代码所用的开发技术是一样的。例如,Oracle公司在开发Oracle数据库时就使用了这些技术。这一层代码量很大(但相对于数据库的全部代码来讲,还只是很少的一部分),称为操作系统相关(operating system-dependent,OSD)代码,是专门针对各个平台实现的。使用这层抽象,Oracle就能利用许多本地OS特性来提高性能和支持集成,而无需重写数据库本身的很大一部分代码。Oracle能作为一个多线程应用在Windows上运行,也能作为一个多进程应用在UNIX上运行,这就反映出Oracle利用了这种OSD代码。它将进程间通信的机制抽象到这样一个代码层上,可以根据不同的操作系统重新实现,所以允许有完全不同的实现,它们的表现与直接(专门)为各平台编写的应用相差无几。
采用这个方法还有一个原因,要想找到一个样样精通的开发人员,要求他熟知Oracle、SQL Server和DB2之间的细微差别(这里只讨论这3个数据库)几乎是不可能的,更别说找到这样一个开发小组了。我在过去11年间一直在用Oracle(大体如此,但不排除其他软件)。每一天使用Oracle,都会让我学到一些新的东西。但我还是不敢说同时精通这3种数据库,知道它们之间的差别,并且清楚这些差别会对要构建的“泛型代码”层有什么影响。我觉得自己无法准确或高效地实现这样一个“泛型代码”层。再说了,我们指的是一般的开发人员,有多少开发人员能真正理解或充分使用了手上的数据库呢?更别说掌握这3种数据库了!要寻找这样一个“全才”,他能开发安全、可扩缩而且独立于数据库的程序,就像是大海捞针一样。而希望由这样的人员组建一支开发队伍更是绝无可能。反过来,如果去找一个Oracle专家、一个DB2专家和一个SQL Server专家,告诉他们“我们需要事务完成X、Y和Z”,这倒是很容易。只需告诉他们“这是你的输入,这些是我们需要的输出,这是业务过程要做的事情”,根据这些来生成满足要求的事务性API(存储过程)就很简单了。针对特定的数据库,按照数据库特有的一组功能,可以采用最适于该数据库的方式来实现。开发人员可以自由地使用底层数据库平台的强大能力(也可能底层数据库缺乏某种能力,而需要另辟蹊径)。
3. 特性和功能
你不必努力争取数据库独立性,这还有一个很自然的理由:你应当准确地知道特定数据库必须提供什么,并充分加以利用。这一节不会列出Oracle 10g提供的所有特性,光是这些特性本身就需要一本很厚的书才能讲完。Oracle 9i Release 1、9i Release 2和10g Release 1本身的新特性在Oracle文档中已做介绍。Oracle为此提供了大约10 000页的文档,涵盖了每一个有意义的特性和功能。你起码要对数据库提供的特性和功能有一个大致的了解,这一节只是讨论大致了解有什么好处。
前面提到过,我总在http://asktom.oracle.com上回答有关Oracle的问题。我说过,我的答案中80%都只是给出相关文档的URL(这是指我公开提出的那些问题,其中许多答案都只是指向文档,另外还会有几个问题我没有公开提出,因为这些问题的答案几乎都是“读读这本书”)。人们问我怎么在数据库中编写一些复杂的功能(或者在数据库之外编写),我就会告诉他们在文档的哪个地方可以了解到Oracle已经实现了这个功能,并且还说明了应该如何使用这个功能。我时常会遇到一些有关复制的问题。可能有这样一个问题:“我想在每个地方都留有数据的一个副本。我希望这是一个只读的副本,而且每天只在半夜更新一次。我该怎么编写代码来做到呢?”答案很简单,只是一个CREATE MATERIALIZED VIEW命令而已。这是数据库中的一个内置功能。实际上,实现复制还有许多方法,从只读的物化视图到可更新的物化视图,再到对等复制以及基于流的复制,等等。
你当然可以编写你自己的复制,这么做可能很有意思,但是从最后看来,自己编写可能不是最明智的做法。数据库做了很多工作。一般来说,数据库会比我们自己做得更好。例如,Oracle中复制是用C编写的,充分考虑到了国际化。不仅速度快、相当容易,而且很健壮。它允许跨版本和跨平台,并且提供了强大的技术支持,所以倘若你遇到问题,Oracle Support会很乐意提供帮助。如果你要升级,也会同步地提供复制支持,可能还会增加一些新的特性。下面考虑一下如果由你自己来开发会怎么样。你必须为每一个版本都提供支持。老版本和新版本之间的互操作性谁来负责?这个任务会落在你的头上。如果出了“问题”,你没有办法寻求支持,至少在得到一个足够小的测试用例(但足以展示你的主要问题)之前,没有人来帮助你。当新版本的Oracle推出时,也要由你自己将你的复制代码移植到这个新版本。
如果没有充分地了解数据库已经提供了哪些功能,从长远看,其坏影响还会几次三番地出现。我曾经与一些有多年数据库应用开发经验的人共事,不过他们原先是在其他数据库上开发应用。这一次他们在Oracle上构建了一个分析软件(趋势分析、报告和可视化软件),要用于分析临床医学数据(与保健相关)。这些开发人员不知道SQL的一些语法特性,如内联视图、分析功能和标量子查询。他们遇到的一个主要问题是需要分析一个父表及两个子表的数据。相应的实体—关系图(entity-relationship diagram,ERD)如图1-1所示。

图1-1 简单的ERD
他们想生成父记录的报告,并提供子表中相应子记录的聚集统计。他们原来使用的数据库不支持子查询分解(WITH子句),也不支持内联视图(所谓内联视图,就是 “查询一个查询”,而不是查询一个表)。由于不知道有这些特性,开发人员们在中间层编写了他们自己的一个数据库。他们的做法是先查询父表,对应返回的每一行,再对各个子表分别运行聚集查询。这样做的后果是:对于最终用户想要运行的每一个查询,他们都要运行数千个查询才能得到所需的结果。或者,他们的另一种做法是在中间层获取完整的聚集子表,再放入内存中的散列表,并完成一个散列联结(hash join)。
简而言之,他们重新开发了一个数据库,自行完成了与嵌套循环联结或散列联结相当的功能,而没有充分利用临时表空间、复杂的查询优化器等所提供的好处。这些开发人员把大量时间都花费在这个软件的开发、设计、调优和改进上,而这个软件只是要做数据库已经做了的事情,要知道他们原本已经花钱买了这些功能!与此同时,最终用户还在要求增加新特性,但是一直没有如愿,因为开发人员总忙于开发报告“引擎”,没有更多的时间来考虑这些新特性,实际上这个报告引擎就是一个伪装的数据库引擎。
我告诉他们,完全可以联结两个聚集来比较用不同方法以不同详细程度存储的数据(见代码清单1-1~代码清单1-3)。
代码清单1-1 内联视图:对“查询”的查询

代码清单1-2 标量子查询:每行运行另一个查询

代码清单1-3 WITH子查询分解
![]()


更何况他们还可以使用LAG、LEAD、ROW_NUMBER之类的分析函数、分级函数等。我们没有再花时间去考虑如何对他们的中间层数据库引擎进行调优,而是把余下的时间都用来学习SQL Reference Guide,我们把它投影在屏幕上,另外还打开一个SQL*Plus实际演示到底如何工作。最终目标不是对中间层调优,而是尽快地把中间层去掉。
我曾经见过许多人在Oracle数据库中建立后台进程从管道(一种数据库IPC机制)读消息。这些后台进程执行管道消息中包含的SQL,并提交工作。这样做是为了在事务中执行审计,即使更大的事务(父事务)回滚了,这个事务(子事务)也不会回滚。通常,如果使用触发器之类的工具来审计对某数据的访问,但是后来有一条语句失败,那么所有工作都会回滚。所以,通过向另一个进程发送消息,就可以有一个单独的事务来完成审计工作并提交。即使父事务回滚,审计记录仍然保留。在Oracle8i以前的版本中,这是实现此功能的一个合适的方法(可能也是惟一的方法)。我告诉他们,数据库还有一个称为自治事务(autonomous transaction)的特性,他们听后很是郁闷。自治事务的实现只需一行代码,就完全可以做到他们一直在做的事情。好的一面是,这说明他们可以丢掉原来的大量代码,不用再维护了。另外,系统总的来讲运行得更快,而且更容易理解。不过,他们还在为“重新发明”浪费了那么多时间而懊恼不已。特别是那个写后台进程的开发人员更是沮丧,因为他写了一大堆的代码。
还是我反复重申的那句话:针对某个问题,开发人员力图提供复杂的大型解决方案,但数据库本身早已解决了这个问题。在这个方面,我自己也有些心虚。我还记得,有一天我的Oracle销售顾问走进我的办公室(那时我还只是一个客户),看见我被成堆的Oracle文档包围着。我抬起头,问他“这是真的吗?”接下来的几天我一直在深入研究这些文档。此前我落入一个陷阱,自以为“完全了解数据库”,因为我用过SQL/DS、DB2、Ingress、Sybase、Informix、SQLBase、Oracle,还有其他一些数据库。我没有花时间去了解每个数据库提供了什么,而只是把从其他数据库学到的经验简单地应用到当时正在使用的数据库上(移植到Sybase/SQL Server时对我的触动最大,它与其他数据库的工作根本不一样)。等到我真正发现Oracle(以及其他数据库)能做什么之后,我才开始充分利用它,不仅能更快地开发,而且写的代码更少。我认识到这一点的时候是1993年。请仔细想想你能用手头的软件做些什么,不过与我相比,你已经晚了十多年了。
除非你花些时间来了解已经有些什么,否则你肯定会在某个时候犯同样的错误。在这本书中,我们会深入地分析数据库提供的一些功能。我选择的是人们经常使用的特性和功能,或者是本应更多地使用但事实上没有得到充分利用的功能。不过,这里涵盖的内容只是冰山一角。Oracle的知识太多了,单用一本书来讲清楚是做不到的。
重申一遍:每天我都会学到Oracle的一些新知识。这需要“与时俱进”,时刻跟踪最新动态。我自己就常常阅读文档(不错,我还在看文档)。即使如此,每天还是会有人指出一些我不知道的知识。
4. 简单地解决问题
通常解决问题的途径有两种:容易的方法和困难的方法。我总是看到人们在选择后者。这并不一定是故意的,更多的情况下,这么做只是出于无知。他们没想到数据库能“做那个工作”。而我则相反,我总是希望数据库什么都能做,只有当我发现它确实做不了某件事时才会选择困难的办法(自己来编写)。
例如,人们经常问我,“怎么确保最终用户在数据库中只有一个会话?”(其实类似这样的例子还有很多,我只是随便选了一个)。可能许多应用都有这个需求,但是我参与的应用都没有这样做,我不知道有什么必要以这种方式限制用户。不过,如果确实想这样做,人们往往选择困难的方法来实现。例如,他们可能建立一个由操作系统运行的批作业,这个批作业将查看V$SESSION表;如果用户有多个会话,就坚决地关闭这些会话。还有一种办法,他们可能会创建自己的表,用户登录时由应用在这个表中插入一行,用户注销时删除相应行。这种实现无疑会带来许多问题,于是咨询台的铃声大作,因为应用“崩溃”时不会将该行删除。为了解决这个问题,我见过许多“有创意的”方法,不过哪一个也没有下面这种方法简单:

仅此而已。现在有ONE_SESSION配置文件的所有用户都只能登录一次。每次我提出这个解决方案时,人们总是拍着自己的脑门,不无惊羡地说:“我不知道居然还能这么做!”正所谓磨刀不误砍柴工,花些时间好好熟悉一下你所用的工具,了解它能做些什么,在开发时这会为你节省大量的时间和精力。
还是这句“力求简单”,它也同样适用于更宽泛的体系结构层。我总是鼓励人们在采用非常复杂的实现之前先要再三思量。系统中不固定的部分越多,出问题的地方就越多。在一个相当复杂的体系结构中,要想准确地跟踪到错误出在哪里不是一件容易的事。实现一个有“无数”层的应用可能看起来很“酷”,但是既然用一个简单的存储过程就能更好、更快地完成任务,而且只利用更少的资源,实现为多层的做法就不是正确的选择。
我见过许多项目的应用开发持续数月之久,好像没有尽头。开发人员都在使用最新、最好的技术和语言,但是开发速度还是不快。应用本身的规模并不大,也许这正是问题所在。如果你在建一个狗窝(这是一个很小的木工活),就不会用到重型机器。你只需要几样小工具就行了,大玩艺是用不上的。另一方面,如果你在建一套公寓楼,就要下大功夫,可能要用到大型机器。与建狗窝相比,解决这个问题所用的工具完全不同。应用开发也是如此。没有一种“万全的体系结构”,没有一种“完美的语言”,也没有一个“无懈可击的方法”。
例如,我就使用了HTML DB来建我的网站。这是一个很小的应用,只有一个(或两个)开发人员参与。它有大约20个界面。这个实现使用PL/SQL和HTML DB是合适的,这里不需要用Java编写大量的代码,不需要建立EJB,等等。这是一个简单的问题,所以应该用简单的方式解决。确实有一些大型应用很复杂、规模很大(如今这些应用大多会直接购买,如人力资源HR系统、ERP系统等),但是小应用更多。我们要选用适当的方法和工具来完成任务。
不论什么时候,我总是提倡用最简单的体系结构来解决问题,而不要采用复杂的体系结构。这样做可能有显著的回报。每种技术都有自己合适的位置。不要把每个问题都当成钉子,高举铁锤随处便砸,我们的工具箱里并非只有铁锤。
5. 开放性
我经常看到,人们选择艰难的道路还有一个原因。这还是与那种观点有关,我们总认为要不遗余力地追求开放性和数据库独立性。开发人员希望避免使用封闭的专有数据库特性,即使像存储过程或序列这样简单的特性也不敢用,因为使用这些专有特性会把他们锁定到某个数据库系统。这么说吧,我的看法是只要你开发一个涉及读/写的应用,就已经在某种程度上被锁定了。一旦开始运行查询和修改,你就会发现数据库间存在着一些微小的差别(有时还可能存在显著差异)。例如,在一个数据库中,你可能发现SELECT COUNT(*) FROM T查询与两行记录的更新发生了死锁。在Oracle中,却发现SELECT COUNT(*)绝对不会阻塞写入器。你可能见过这样的情况,一个数据库看上去能保证某种业务规则,这是由于该数据库锁定模型的副作用造成的,但另一个数据库则不能保证这个业务规则。给定完全相同的事务,在不同数据库中却有可能报告全然不同的答案,原因就在于数据库的实现存在一些基本的差别。你会发现,要想把一个应用轻轻松松地从一个数据库移植到另一个数据库,这种应用少之又少。不同数据库中对于如何解释SQL(例如,NULL=NULL这个例子)以及如何处理SQL往往有不同的做法。
在我最近参与的一个项目中,开发人员在使用Visual Basic、ActiveX控件、IIS服务器和Oracle构建一个基于Web的产品。他们不无担心地告诉我,由于业务逻辑是用PL/SQL编写的,这个产品已经依赖于数据库了。他们问我:“怎么修正这个问题?”
先不谈这个问题,退一步说,针对他们所选的技术,我实在看不出依赖于数据库有什么“不好”:
q 开发人员选择的语言已经把他们与一个开发商提供的一个操作系统锁定(要想独立于操作系统,其实他们更应选择Java)。
q 他们选择的组件技术已经把他们与一个操作系统和一个开发商锁定(选择J2EE更合适)。
q 他们选择的Web服务器已经将他们与一个开发商和一个平台锁定(为什么不用Apache呢?)。
所选择的每一项技术都已经把他们锁定到一个非常特定的配置,实际上,就操作系统而言,惟一能让他们有所选择的技术就是数据库。
暂且不管这些(选择这些技术可能有他们自己的原因),这些开发人员还刻意不去用体系结构中一个重要部件的功能,而美其名曰是为了开放性。在我看来,既然精心地选择了技术,就应该最大限度地加以利用。购买这些技术你已经花了不少钱,难道你想白白地花冤枉钱吗?我认为,他们一直想尽力发挥其他技术的潜能,那么为什么要把数据库另眼相看呢?再者,数据库对于他们的成功至关重要,单凭这一点也说明,不充分利用数据库是说不过去的。
如果从开放性的角度来考虑,可以稍稍换个思路。你把所有数据都放在数据库中。数据库是一个很开放的数据池。它支持通过大量开放的系统协议和访问机制来访问数据。这听起来好像很不错,简直就是世界上最开放的事物。
不过接下来,你把所有应用逻辑还有(更重要的)安全都放在数据库之外。可能放在访问数据的bean中;也可能放在访问数据的JSP中;或者置于在Microsoft事务服务器(Microsoft Transaction Server,MTS)管理之下运行的Visual Basic代码中。最终结果就是,你的数据库被封闭起来,这么一来,数据库已经被你弄得“不开放”了。人们无法再采用现有技术使用这些数据;他们必须使用你的访问方法(或者干脆绕过你的安全防护)。尽管现在看上去还不错,但是你要记住,今天响当当的技术(比如说,EJB)也会成为昨日黄花,到了明天可能就是一个让人厌倦的技术了。在关系领域中(以及大多数对象实现中),过去25年来只有数据库自己傲然屹立。数据前台技术几乎每年一变,如果应用把安全放在内部实现,而不是在数据库中实现,随着前台技术的变革,这些应用就会成为前进道路上的绊脚石。
Oracle数据库提供了一个称为细粒度访问控制(fine-grained access control,FGAC)的特性。简而言之,这种技术允许开发人员把过程嵌入数据库中,向数据库提交查询时可以修改查询。这种查询修改可用于限制客户只能接收或修改某些行。过程在运行查询时能查看是谁在运行查询,他们从哪个终端运行查询,等等,然后能适当地约束对数据的访问。利用FGAC,可以保证以下安全性,例如:
q 某类用户在正常工作时间之外执行的查询将返回0条记录。
q 如果终端在一个安全范围内,可以向其返回所有数据,但是远程客户终端只能得到不敏感的信息。
实质上讲,FGAC允许我们把访问控制放在数据库中,与数据“如影随形”。不论用户从bean、JSP、使用ODBC的Visual Basic应用,还是通过SQL*Plus访问数据,都会执行同样的安全协议。这样你就能很好地应对即将到来的下一种新技术。
现在我再来问你,你想让所有数据访问都通过调用Visual Basic代码和ActiveX控件来完成(如果愿意,也可以把Visual Basic换成Java,把ActiveX换成EJB,我并不是推崇哪一种技术,这里只是泛指这种实现);还是希望能从任何地方访问数据(只要能与数据库通信),而不论协议是SSL、HTTP、Oracle Net,还是其他协议,也不论使用的是ODBC、JDBC、OCI,还是其他API,这两种实现中哪一种更“开放”? 我还没见过哪个报告工具能“查询”Visual Basic代码,但是能查询SQL的工具却有不少。
人们总是不遗余力地去争取数据库独立性和完全的开放性,但我认为这是一个错误的决定。不管你使用的是什么数据库,都应该充分地加以利用,把它的每一个功能都“挤出来”。不论怎样,等到调优阶段你也会这样做的(不过,往往在部署之后才会调优)。如果通过充分利用软件的功能,会让你的应用快上5倍,你会惊讶地发现,居然这么快就把数据库独立性需求抛在脑后了。





