|
我抄一段TOM的书,建议你好好读读:
1.3.4 数据库独立性?
至 此,你可能想到这一节要讲什么了。我提到了其他的数据库,也谈到各个数据库中会以不同的方
式实现特性。除了一些只读应用外,我的观点是:要构建一个完全数 据库独立的应用,而且是高度可扩缩
的应用,是极其困难的。实际上,这几乎不可能,除非你真正了解每个数据库具体如何工作。另外,如果
你清楚每个数据库工作 的具体细节,就会知道,数据库独立性可能并不是你真正想要的(这个说法有点
绕 !)。
例如,再来看最早提到的资源调度例子(增加 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 查询返回的结果是“错的”。查询如下所示:
declare
l_some_variable varchar2(25);
begin
if ( some_condition )
then
l_some_variable := f( ... );
end if;
for C in ( select * from T where x = l_some_variable )
loop
...
这个查询的目标是:在 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)。从以下代码可以看出我的意思:
ops$tkyte@ORA10G> select * from dual where null=null;
no rows selected
ops$tkyte@ORA10G> select * from dual where null <> null;
no rows selected
ops$tkyte@ORA10G> select * from dual where null is null;
D
-X
第一次看到这些结果可能会被搞糊涂。这说明,在 Oracle 中,NULL 与 NULL 既不相等,也不完全不
相等。默认情况下,SQL Server 则不是这样处理;在 SQL Server 和 Sybase 中,NULL 就等于 NULL。不能
说 Oracle 的 SQL 处理是错的,也不能说 Sybase 或 SQL Server 的处理不对,它们只是方式不同罢了。实际
上,所有这些数据库都符合 ANSI,但是它们的具体做法还是有差异。有许多二义性、向后兼容性等问题需
要解决。例如, SQL Server 也支持 ANSI 方法的 NULL 比较,但这不是默认的方式(如果改成 ANSI 方法的
NULL 比较,基于 SQL Server 构建的数千个遗留应用就会出问题)。
在这种情况下,一种解决方案是编写以下查询:
select *
from t
where ( x = l_some_variable OR (x is null and l_some_variable is NULL ))
不过,这又会带来另一个问题。在 SQL Server 中,这个查询会使用 x 上的索引。Oracle 中却不会这
样,因为 B*树索引不会对一个完全为 NULL 的项加索引(索引技术将在第 12 章介绍)。因此,如果需要查
找 NULL 值,B*树索引就没有什么用处。
这里,为了尽量减少对代码的影响,我们的做法是赋给 x 某个值,不过这个值并没有实际意义。在此 ,
根据定义可知,x 的正常值是正数,所以可以选择 –1。这样一来,查询就变成:
select * from t where nvl(x,-1) = nvl(l_some_variable,-1)
由此创建一个基于函数的索引:
create index t_idx on t( nvl(x,-1) );
只需做最少的修改,就能在 Oracle 中得到与 SQL Server 同样的结果。从这个例子可以总结出以下几
个要点:
. 数据库是不同的。在一个数据库上取得的经验也许可以部分应用于另一个数据库,但是你
必须有心理准备,二者之间可能存在一些基本差别,可能还有一些细微的差别。
. 细微的差别(如对 NULL 的处理)与基本差别(如并发控制机制)可能有同样显著的影响。
. 应当了解数据库,知道它是如何工作的,它的特性如何实现,这是解决这些问题的惟一途
径。
常有开发人员问我如何在数据库中做某件特定的事情(通常这样的问题一天不止一个),例如“如何
在一个存储过程中创建临时表?”对于这些问题,我并不直接回答,而是反过来问他们“你为什么想那么
做?”给我的回答常常是: “我们在 SQL Server 中就是用存储过程创建临时表,所以在 Oracle 中也要这么
做 。 ”这不出我所料,所以我的回答很简单: “你根本不是想在 Oracle 中用存储过程创建临时表,你只是以
为自己想那么做。 ”实际上,在 Oracle 中这样做是很不好的。在 Oracle 中,如果在存储过程中创建表, 你
会发现存在以下问题:
. DDL 操作会阻碍可扩缩性。
. DDL 操作的速度往往不快。
. DDL 操作会提交事务。
. 必须在所有存储过程中使用动态 SQL 而不是静态 SQL 来访问这个表。
. 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 个层次:
? 入门级(Entry level)。这是大多数开发商符合的级别。这一级只是对前一个标准 SQL89
稍做修改。所有数据库开发商都不会有更高的级别,实际上,美国国家标准和技术协会
NIST(National Institute of Standards and Technology,这是一家专门检验 SQL 合规性的
机构)除了验证入门级外,甚至不做其他的验证。Oracle 7.0 于 1993 年通过了 NIST 的 SQL92
入门级合规性验证,那时我也是小组中的一个成员。如果一个数据库符合入门级,它的特性集
则是 Oracle 7.0 的一个功能子集。
? 过渡级。这一级在特性集方面大致介于入门级和中间级之间。
? 中间级。这一级增加了许多特性,包括(以下所列并不完整):
? 动态 SQL
级联 DELETE 以保证引用完整性
? DATE 和 TIME 数据类型
? 域
? 变长字符串
? CASE 表达式
? 数据类型之间的 CAST 函数
? 完备级。增加了以下特性(同样,这个列表也不完整):
? 连接管理
? BIT 串数据类型
? 可延迟的完整性约束
? FROM 子句中的导出表
? CHECK 子句中的子查询
? 临时表
入门级标准不包括诸如外联结(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 类型。每个数据库都有一个解决办法。不过,不论从做法上讲,还是从输
出来看,各个数据库的方法都有所不同。所以,有见识的开发人员有两条路可走:
? 开发一个完全独立于数据库的方法来生成惟一的键。
? 在各个数据库中实现键时,提供不同的实现,并使用不同的技术。
从理论上讲,第一种方法的好处是从一个数据库转向另一个数据库时无需执行任何修改。我把它称为
“理论上” 的好处,这是因为这种实现实在太庞大了,所以这种方案根本不可行。要开发一个完全独立于
数据库的进程,你必须创建如下所示的一个表:
ops$tkyte@ORA10G> create table id_table
2 ( id_name varchar2(30) primary key,
3 id_value number );
Table created.
ops$tkyte@ORA10G> insert into id_table values ( 'MY_KEY', 0 );
1 row created.
ops$tkyte@ORA10G> commit;
Commit complete.
然后,为了得到一个新的键,必须执行以下代码:
ops$tkyte@ORA10G> update id_table
2 set id_value = id_value+1
3 where id_name = 'MY_KEY';
1 row updated.
ops$tkyte@ORA10G> select id_value
2 from id_table
3 where id_name = 'MY_KEY';
ID_VALUE
----------
1
看上去很简单,但是有以下结果(注意结果不止一项):
? 一次只能有一个用户处理事务行。需要更新这一行来递增计数器,这会导致程序必须串行
完成这个操作。在最好的情况下,一次只有一个人生成一个新的键值。
? 在 Oracle 中(其他数据库中的行为可能有所不同),倘若隔离级别为 SERIALIZABLE,除第
一个用户外,试图并发完成此操作的其他用户都会接到这样一个错误: “ORA-08177: can't
serialize access for this transaction”(ORA-08177:无法串行访问这个事务)。
例如,使用一个可串行化的事务(在 J2EE 环境中比较常见,其中许多工具都自动将 SERIALIZABLE
用作默认的隔离模式,但开发人员通常并不知道),你会观察到以下行为。注意 SQL 提示符(使用 SET
SQLPROMPT SQL*Plus 命令)包含了活动会话的有关信息:
OPS$TKYTE session(261,2586)> set transaction isolation level serializable;
Transaction set.
OPS$TKYTE session(261,2586)> update id_table
2 set id_value = id_value+1
3 where id_name = 'MY_KEY';
1 row updated.
OPS$TKYTE session(261,2586)> select id_value
2 from id_table
3 where id_name = 'MY_KEY';
ID_VALUE
----------
1
下面,再到另一个 SQL*Plus 会话完成同样的操作,并发地请求惟一的 ID:
OPS$TKYTE session(271,1231)> set transaction isolation level serializable;
Transaction set.
OPS$TKYTE session(271,1231)> update id_table
2 set id_value = id_value+1
3 where id_name = 'MY_KEY';
此时它会阻塞,因为一次只有一个事务可以更新这一行。这展示了第一种可能的结果,即这个会话会
阻塞,并等待该行提交。但是由于我们使用的是 Oracle,而且隔离级别是 SERIALIZABLE,提交第一个会话
的事务时会观察到以下行为:
OPS$TKYTE session(261,2586)> commit;
Commit complete.
第二个会话会立即显示以下错误:
此时它会阻塞,因为一次只有一个事务可以更新这一行。这展示了第一种可能的结果,即这个会话会
阻塞,并等待该行提交。但是由于我们使用的是 Oracle,而且隔离级别是 SERIALIZABLE,提交第一个会话
的事务时会观察到以下行为:
OPS$TKYTE session(271,1231)> update id_table
2 set id_value = id_value+1
3 where id_name = 'MY_KEY';
update id_table
*
ERROR at line 1:
ORA-08177: can't serialize access for this transaction
所 以,尽管这个逻辑原本想做到独立于数据库,但它根本不是数据库独立的。取决于隔离级别,这
个逻辑甚至在单个数据库中都无法可靠地完成,更不用说跨数据库 了!有时我们会阻塞并等待,但有时却
会得到一条错误消息。说得简单些,无论是哪种情况(等待很长时间,或者等待很长时间后得到一个错误),
都至少会让最终 用户不高兴。
实际上,我们的事务比上面所列的要大得多,所以问题也更为复杂。实际的事务中包含多条语句, 上
例中的 UPDATE 和 SELECT 只 是其中的两条而已。我们还要用刚生成的这个键向表中插入行,并完成这个事
务所需的其他工作。这种串行化对于应用的扩缩是一个很大的制约因素。如果把这个技 术用在处理订单的
网站上,而且使用这种方式来生成订单号,可以想想看可能带来的后果。这样一来,多用户并发性就会成
为泡影,我们不得不按顺序做所有事情。
对于这个问题,正确的解决方法是针对各个数据库使用最合适的代码。在 Oracle 中,代码应该如下
(假设表 T 需要所生成的主键):
create table t ( pk number primary key, ... );
create sequence t_seq;
create trigger t_trigger before insert on t for each row
begin
select t_seq.nextval into :new.pk from dual;
end;
其效果是为所插入的每一行自动地(而且透明地)指定一个惟一键。还有一种性能更优的方法:
Insert into t ( pk, ... ) values ( t_seq.NEXTVAL, ... );
也就是说,完全没有触发器的开销(这是我的首选方法)。
在第一个例子中,我们特意使用了各个数据库的特性来生成一个非阻塞、高度并发的惟一键,而且未
对应用代码带来任何真正的改动,因为在这个例子中所有逻辑都包含在 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 所示。
他们想生成父记录的报告,并提供子表中相应子记录的聚集统计。他们原来使用的数据库不支持子查
询分解(WITH 子 句),也不支持内联视图(所谓内联视图,就是 “查询一个查询” ,而不是查询一个表) 。
由于不知道有这些特性,开发人员们在中间层编写了他们自己的一个数据库。他们的做法是先查询父表,
对应返回的每一 行,再对各个子表分别运行聚集查询。这样做的后果是:对于最终用户想要运行的每一个
查询,他们都要运行数千个查询才能得到所需的结果。或者,他们的另一种 做法是在中间层获取完整的聚
集子表,再放入内存中的散列表,并完成一个散列联结(hash join)。
简 而言之,他们重新开发了一个数据库,自行完成了与嵌套循环联结或散列联结相当的功能,而没
有充分利用临时表空间、复杂的查询优化器等所提供的好处。这些开 发人员把大量时间都花费在这个软件
的开发、设计、调优和改进上,而这个软件只是要做数据库已经做了的事情,要知道他们原本已经花钱买
了这些功能!与此同 时,最终用户还在要求增加新特性,但是一直没有如愿,因为开发人员总忙于开发报
告“引擎”,没有更多的时间来考虑这些新特性,实际上这个报告引擎就是一个 伪装的数据库引擎。
我告诉他们,完全可以联结两个聚集来比较用不同方法以不同详细程度存储的数据(见代码清单 1-1~代码清单 1-3)
代码清单 1-1 内联视图:对“查询”的查询
select p.id, c1_sum1, c2_sum2
from p,
(select id, sum(q1) c1_sum1
from c1
group by id) c1,
(select id, sum(q2) c2_sum2
from c2
group by id) c2
where p.id = c1.id
and p.id = c2.id
/
代码清单 1-2 标量子查询:每行运行另一个查询
select p.id,
(select sum(q1) from c1 where c1.id = p.id) c1_sum1,
(select sum(q2) from c2 where c2.id = p.id) c2_sum2
from p
where p.name = '1234'
/
代码清单 1-3 WITH 子查询分解
with c1_vw as
(select id, sum(q1) c1_sum1
from c1
group by id),
c2_vw as
(select id, sum(q2) c2_sum2
from c2
group by id),
c1_c2 as
(select c1.id, c1.c1_sum1, c2.c2_sum2
from c1_vw c1, c2_vw c2
where c1.id = c2.id )
select p.id, c1_sum1, c2_sum2
from p, c1_c2
where p.id = c1_c2.id
/
更何况他们还可以使用 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 表;如果用户有多个会话,就坚
决地关闭这 些会话。还有一种办法,他们可能会创建自己的表,用户登录时由应用在这个表中插入一行,
用户注销时删除相应行。这种实现无疑会带来许多问题,于是咨询台的 铃声大作,因为应用 “崩溃”时不
会将该行删除。为了解决这个问题,我见过许多 “有创意的”方法,不过哪一个也没有下面这种方法简单:
ops$tkyte@ORA10G> create profile one_session limit sessions_per_user 1;
Profile created.
ops$tkyte@ORA10G> alter user scott profile one_session;
User altered.
ops$tkyte@ORA10G> alter system set resource_limit=true;
System altered.
ops$tkyte@ORA10G> connect scott/tiger
Connected.
scott@ORA10G> host sqlplus scott/tiger
SQL*Plus: Release 10.1.0.2.0 - Production on Sun Nov 28 12:49:49 2004
Copyright (c) 1982, 2004, Oracle. All rights reserved.
ERROR:
ORA-02391: exceeded simultaneous SESSIONS_PER_USER limit
Enter user-name:
仅此而已。现在有 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 编写的,这个产品已经依赖
于数据库了。他们问我: “怎么修正这个问题?”
先不谈这个问题,退一步说,针对他们所选的技术,我实在看不出依赖于数据库有什么“不好”:
开发人员选择的语言已经把他们与一个开发商提供的一个操作系统锁定(要想独立于操作
系统,其实他们更应选择 Java)。
? 他们选择的组件技术已经把他们与一个操作系统和一个开发商锁定(选择 J2EE 更合适)。
? 他们选择的 Web 服务器已经将他们与一个开发商和一个平台锁定(为什么不用
Apache 呢?)。
所选择的每一项技术都已经把他们锁定到一个非常特定的配置,实际上,就操作系统而言,惟一能让
他们有所选择的技术就是数据库。
暂 且不管这些(选择这些技术可能有他们自己的原因),这些开发人员还刻意不去用体系结构中一个
重要部件的功能,而美其名曰是为了开放性。在我看来,既然精心 地选择了技术,就应该最大限度地加以
利用。购买这些技术你已经花了不少钱,难道你想白白地花冤枉钱吗?我认为,他们一直想尽力发挥其他
技术的潜能,那么为 什么要把数据库另眼相看呢?再者,数据库对于他们的成功至关重要,单凭这一点也
说明,不充分利用数据库是说不过去的。
如果从开放性的角度来考虑,可以稍稍换个思路。你把所有数据都放在数据库中。数据库是一个很开
放的数据池。它支持通过大量开放的系统协议和访问机制来访问数据。这听起来好像很不错,简直就是世
界上最开放的事物。
不过接下来,你把所有应用逻辑还有(更重要的)安全都放在数据库之外。可能放在访问数据的 bean
中;也可能放在访问数据的 JSP 中;或者置于在 Microsoft 事务服务器(Microsoft Transaction Server,
MTS)管理之下运行的 Visual Basic 代码中。最终结果就是,你的数据库被封闭起来,这么一来,数据库
已经被你弄得“不开放”了。人们无法再采用现有技术使用这些数据;他们必须使用你的访问方法(或者
干脆绕过你的安全防护)。尽管现在看上去还不错,但是你要记住,今天响当当的技术(比如说,EJB)也
会成为昨日黄花,到了明天可能就是一个让人厌倦的技术了。在关系领域中(以及大多数对象实现中), 过
去 25 年来只有数据库自己傲然屹立。数据前台技术几乎每年一变,如果应用把安全放在内部实现,而不是
在数据库中实现,随着前台技术的变革,这些应用就会成为前进道路上的绊脚石。
Oracle 数据库提供了一个称为细粒度访问控制(fine-grained access control,FGAC)的特性。简
而言之,这种技术允许开发人员把过程嵌入数据库中,向数据库提交查询时可以修改查询。这种查询修改
可用于限制客户只能接收或修改某些行。过程在运行查询时能查看是谁在运行查询,他们从哪个终端运行
查询,等等,然后能适当地约束对数据的访问。利用 FGAC,可以保证以下安全性,例如:
? 某类用户在正常工作时间之外执行的查询将返回 0 条记录。
? 如果终端在一个安全范围内,可以向其返回所有数据,但是远程客户终端只能得到不敏感
的信息。
实质上讲,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 倍,你会惊讶地发现,居然这么快就把数据库独立性需求抛在脑后了。
|
|