[翻译]11gR2 新特性:在线版本变更(Edition-Based Redefinition)
转自 http://www.itpub.net/thread-1396183-1-1.html更(Edition-Based Redefinition)
众所周知PLSQL程序具有依赖性,一旦依赖对象被修改程序就会失效,需要重编译,这也是很多人反对使用存储过程的理由,比如这个:
http://www.itpub.net/thread-1393352-1-1.html
现在这个问题已经被ORACLE完美解决了,你用PLSQL开发的应用可以实现无缝的平滑过渡。总的来说有这么几点:
1. 你可以在当前库创建一个新的版本号,这个新版本在发布之前对其他会话是不可见的,你可以随意修改编译你的PLSQL程序而不会影响到正在运行的旧版本应用。
2. 让你的应用访问版本视图,而不是直接访问表,这样你可以修改表的结构,而旧版本程序看到的还是原来的结构,修改对它没有任何影响。
3. ORACLE新提供的dbms_parallel_execute让你可以把一个大型DML划分成小片执行,从而实现逐步的数据迁移,而不会一下子锁住太多的数据。
4. 新的版本触发器使得你可以在新旧版本之间互相传递数据,这样两个版本的应用程序可以同时运行。
以下是TOM在ORACLE杂志写的系列专栏,介绍这一奇妙的新功能:
11gR2 新特性:在线版本变更(Edition-Based Redefinition, 直译为“基于版本的重定义”)
By Tom Kyte
第一部分:
http://www.Oracle.com/technetwor ... 9asktom-089919.html
节译:
在线版本变更,在我看来,是Oracle数据库11gR2版的杀手级新功能。它使得我们能够去掉计划中不得已的最后一点停机时间。
过去发行的Oracle数据库已经让我们能够做很多在线操作,比如:修改大部分参数,创建索引,在线重新定义、组织任何对象结构,甚至数据库升级。唯一必须真正停机,在一段时间内切断对数据库的访问的时候,是在应用程序升级期间。这是因为我们必须花时间进行PL/SQL单元的重编译,重新创建视图,修改授权,等等。这些变更通常要求没有人在执行我们的PL/SQL单元,使用我们的视图,等等。但这已成为历史。有了Oracle数据库11gR2版,我们现在能够在数据库中创建不同版本的“新命名空间”。
一个新的版本是作为当前版本的子版本创建的,它继承了父版本的所有“状态”:所有的PL/SQL代码,视图,同义词,等等。当一个子版本首次被创建,它是父版本的一个镜像,但这个镜像是可以修改的。例如,你可以在你当前的生产库代码基础上创建新版本,在这个新版本中,执行CREATE OR REPLACE PROCEDURE P指令,在新版本中覆盖存储过程P。而当前的版本,即你的最终用户所见的那个版本,具有存储过程P的旧版,你的用户将不受新版本的影响。你可以在新版本中安装新版本的存储过程P并且重新编译那些失效的代码,不会对当前版本有任何影响。当你确认你在新版本中的所有修改都已经是正确和完整的,你可以把这个新版本公开发布,新代码就可以使用了。为了安装新版本的存储过程P并且使得所有依赖代码生效,你用不着把正在执行过程P的用户赶出去;我们可以在旧代码还被使用的时候做这些事情。产品环境的变更本来可能是一个冗长的、复杂的、脱机的过程,现在对最终用户(这是最重要的人群)而言瞬间就完成了。
如果你升级到11gR2需要一个理由,这个理由就是在线版本变更。我只是简单描述了它的能力,下一篇我会讲述更多细节。(更多信息请参见Oracle数据库11gR2版高级应用开发者指南第19章,“在线版本变更”)
第二部分:
http://www.Oracle.com/technetwor ... 0asktom-098897.html
上一期的Oracle杂志(2010 一/二月刊),我描述了Oracle数据库11gR2版的杀手级新功能: 在线版本变更。你可能还记得上期的内容, 在线版本变更指的是能够在线升级你的应用程序。
在历史上,Oracle数据库已经允许很多操作可以在线完成,例如下面这些:
■修改大多数的参数(350个中只有90个不能够在线修改)
■对象重组:把一个非分区表变成分区表,回收空闲空间,等等
■创建索引
■利用Oracle RAC为数据库打补丁
■把Oracle数据库从一个主版本升级到另一个主版本
现在, Oracle 数据库11gR2版为这个清单加入了“升级你的应用”功能。
上一次,在“A Closer Look at the New Edition”一文中,我们看到了一个简单的应用场景。我们的数据库中安装了一个版本为1.0的应用程序,我们想为它打补丁;具体地说,我们需要改正其中的一个PL/SQL单元。通常情况下,DBA或者应用管理员不得不安排一个系统维护时段来打这个补丁,因为他要重新编译代码。大部分DBA都知道,如果别人正在执行一个PL/SQL单元,你就无法编译它。此外,就算你能够重编译那个包,你的应用用户会告诉你,他们突然看到一个“ORA-04068: existing state of packages has been discarded” 的错误信息。这些情形使得在现实生活中,你不可能在产品环境中为你的应用程序做在线升级或者在线补丁。这就是在线版本变更功能所要改变的:现在,当最终用户正在执行旧版本的代码时,我们能够在线为应用程序打补丁。
如果你没有读过上一篇专栏,我建议你在阅读本篇之前去读一读。在本篇中,我假设你已经知道在线版本变更,理解它的基本功能,并且看过了上一篇专栏的例子。
在这一部分,我们将比上次更进一步。在上一篇专栏中,我们的应用升级只是一个简单的补丁:我们实时替换和测试了一个PL/SQL单元,而且没有招致最终用户停机。这一次我们不仅仅要替换某些可版本化的对象(上一期例子中的PL/SQL单元),同时还要做一个物理模式的变更。我们本次的目标是要实现代码和物理模式的变更,同时使得停机时间尽可能地最小化。
你可能记得,在Oracle数据库11gR2版, 所有对象类型可分为两大类:可版本化和非版本化。下列对象类型是可版本化的:
■同义词
■视图(包括版本视图,我们下面会给出定义)
■所有PL/SQL对象(函数,过程,包,等等)
所有其他类型的对象(例如表)就是非版本化的。但这一事实并不妨碍我们为我们的应用程序制造出这些非版本化对象也是“可版本化”的假象。
让我们从本次的例子入手。在标准的HR模式中有一张EMPLOYEES(职员)表。这张公司的所有职员的清单包含了一些属性,例如姓,名,电子邮件,和电话号码。我们假装这张表历来就是一个美国公司的应用程序的一部分,所以电话号码是以美国公司能够方便辨认的格式来存储的。美国电话号码存储为一个地区号码,然后是一个7位数字,例如: 650.507.9876。国际号码的存储格式有一个美国“转义”码(在拨打国际长途的时候使用),紧跟着是国家号码,然后是电话号码,例如:011.44.1644.429262。
这个虚构的公司被收购,现在是一个全球化公司的一部分。为了适应新的母公司的标准,被收购公司必须修改它存储电话号码的格式。现在它利用两个字段来存储和现实所有的号码:一个国家号码,和一个电话号码。例如,两个电话号码例子前后的存储格式如下所示:
-----------------------------------------------------
修改前 | 修改后
--------------------|--------------------------------
PHONE_NUMBER | COUNTRY_CODE | PHONE#
--------------------|-----------------|--------------
650.507.9876 | +1 | 650.507.9876
011.44.1644.429262 | +44 | 1644.429262
-----------------------------------------------------
现在,这个小小的修改(事实上,任何的修改都不是“小小”的!)包含了几个步骤。具体地说,我们需要:
■修改模式,使得EMPLOYEE表包含两个新的列:COUNTRY_CODE 和 PHONE#
■修改模式,使得EMPLOYEE表不再包含PHONE_NUMBER列
■在PHONE#为一个搜索页面创建一个索引(也许还有其他索引,但我们先做一个索引)
■将PHONE_NUMBER的数据批量转移到COUNTRY_CODE and PHONE#
■将应用程序中依赖于PHONE_NUMBER列的代码替换掉
这些步骤的任何一个都可能需要很多时间;那些最费时的步骤是索引创建、数据迁移和代码替换。我们的目标是将这次应用升级导致的任何停机时间最小化,所以我们将会在版本1.0的应用程序运行期间执行所有这些步骤,把所有的变更集结在应用程序的2.0版本。我们将会把停机时间缩短到仅仅是切换到新应用程序所需要的时间(一个ALTER DATABASE操作就可以为我们完成切换)。简而言之,这次升级的停机时间应该是以秒来衡量,而不是分钟或小时。
那么我们来看看面对哪些挑战。第一件要做的事是新增两个列并去掉一个旧的列。我们必须以一种特殊方式来增加新列,使得现在的应用程序不受影响。万一应用程序中有一个SELECT * FROM EMPLOYEES查询怎么办?这是一个编程坏习惯,但是它仍然可能发生。或者应用程序中有这样的代码:INSERT INTO EMPLOYEES VALUES (. . . ), 没有指定列的清单(另一个非常糟糕的编程习惯)。增加新的两个列可能破坏现有的应用程序。类似的,我们需要删除一个当前应用程序正在使用的列(所以显然我们不能够真正删除它)。
为了对付这些难题,Oracle数据库11gR2版引入了一个版本视图的概念,它为应用程序代码和物理模式之间提供了一个缓冲空间。一个版本视图是一种特殊的视图,它只能够从单一的表执行SELECT操作,并把它所需要的属性清单投影到那个表。没有WHERE子句,也没有表连接,仅仅是SELECT和FROM。你可以把一个版本视图想象为和同义词非常相似,但这个同义词不仅仅能够将指向对象重命名,而且还能够选择列和列的命名。不仅如此,这个版本视图还可以在上面定义触发器;不是一个视图通常具有的INSTEAD OF触发器,而是所有常规的表触发器类型,例如BEFORE, BEFORE EACH ROW, 和复合触发器(译者注:compound trigger是11G以上支持的新类型)。因此,我们可以利用这些版本视图来为现有的应用程序隐藏两个新列,我们还能用一个新的版本视图在我们的新应用程序中来隐藏那个旧列(就像我们已经删除了它一样)并且把两个新列显露出来。
在这个简单的“增加两个新列,删除一个旧列”的操作中,我们还面临其他的什么挑战?嗯,还有数据库中的阻塞和锁的问题。在过去,为了向一个表增加一个新列,我们需要在那个表上的一个排他锁。如果你试过为一个活跃的表增加一个新列,这个错误可能对你很亲切:
SQL> alter table emp add resume blob;
alter table emp add resume blob
*
ERROR at line 1:
ORA-00054: resource busy and acquire
with NOWAIT specified
在过去,在一个活跃的系统上执行DDL操作通常是不可能的。可是,在Oracle数据库11GR1和R2的某些场景中,在一个活跃的系统上执行DDL的能力被大大增强了。许多DDL操作,比如增加一个列,现在可以用一种不阻塞的方式来执行,或者换句话说,假如这个DDL确实需要隔离执行,我们可以使用等待型的DDL来避免ORA-00054错误(参见Oracle杂志 2008年五/六月刊中有关这个话题的文章“On Seeing Double in V$SQL” http://www.Oracle.com/technetwor ... 8asktom-085659.html)
在过去,DDL修改例如"ADD COLUMN"所带来的另一个问题是相关的对象失效:任何在此表上创建的视图都会失效,任何依赖于这张表的PL/SQL单元都会失效,等等。这使得你不能够在线增加一个列。但是,Oracle数据库11GR1开始有了"精细依赖性跟踪"(参见Oracle杂志2009年七/八月刊文章“ On Popularity and Natural Selection ”中有关“不用SELECT *的第13134213个理由”一节, http://www.Oracle.com/technetwor ... 9asktom-090487.html),我们已经解决了第一个问题:“如何安全地增加两个新列而不会使任何现有代码失效”。我们利用版本视图对现有应用程序隐藏新列,这可以受到精细依赖性跟踪的保护。此外,我们现在可以利用版本视图,虚拟地将现有的列删掉;同时把两个新增的列显露给最新版本的应用程序,同样还是利用版本视图。
那么在PHONE#列上创建一个索引又会如何?这里有两个问题:创建索引,还要担心这个索引影响现有系统的查询性能。增加一个新索引可能改善某些查询的响应时间,对另外一些查询没有影响,而对第三类查询却可能有负面影响。过去,在Oracle数据库11GR1之前,一个CREATE INDEX语句总是需要在表上加一个排他锁, 甚至一个CREATE INDEX ONLINE的操作也需要在开始创建索引的瞬间需要这么一个锁。然而,从Oracle数据库11GR1开始,在线创建一个索引已经完全没有阻塞了。它完全不需要任何锁,所以能够避免加锁和阻塞问题。(注意:CREATE INDEX ONLINE操作是Oracle企业版数据库才有的功能)此外,索引现在可以被创建为隐形的,意思是它会存在、被修改维护,但是不会被用在存取路径中,除非有一个会话明确地要求使用这个索引。所以,现在我们能够在PHONE#列上增加一个索引而不用担心阻塞/加锁的问题,并且能够确定这个新索引不会对现有的查询性能产生影响,而这个查询我们也许还没有用新索引测试过。因此我们可以安全地增加这个索引而不会对现版本的应用程序产生影响。
下一步我们有个真正困难的任务:批量数据迁移。我们需要把数据从PHONE_NUMBER列迁移到两个新列。此外,我们还要考虑当我们在做这个应用升级的时候,那些被现有版本的应用程序插入和修改的数据要怎么办。如果仅仅在EMPLOYEES表上作一个批量UPDATE操作来迁移数据,是行不通的,有两个原因:
■单个UPDATE的批量操作会锁住整张表,这就不那么“在线”。
■如果单个批量UPDATE完成了,现有的应用程序插入了一个新行或者修改了一个旧记录的PHONE_NUMBER列,那些修改将不会反映到新列中,所以我们就丢失了这些修改。
幸运的是,我们可以解决这两个问题。第一个问题:单个批量UPDATE对整个表加锁的问题可以利用一个新的包来解决,DBMS_PARALLEL_EXECUTE (在Oracle杂志2009年十一/十二月刊的文章“Looking at the New Edition”中我们已经见识过这个功能,我给它取了个昵称叫做“DIY并行操作”http://www.Oracle.com/technetwor ... 9asktom-089919.html)利用DBMS_PARALLEL_EXECUTE包,我们能够把任务划分成很小的片, 以我们所需的任何并行度:从并行度1(其实就是串行)到1000不等,来修改表中的每一行。这会缩减每片表数据涉及到的数据量,以及被锁的时间。如果我们把表分成100片并且使用并行度1, 我们每次加锁的表数据不超过1%;如果我们把表分成1000片, 我们每次加锁的表数据不超过0.1%, 以此类推。
第二个问题:持续地将数据从现有应用迁移到新的应用,就更难解决。我们必须以某种方式教会现有系统来为新版本维护数据,但是我们在这样做的同时又不需要修改现有的版本!听起来像“魔法”,这确实就是魔法。这个魔法就是一种新型的触发器:一个跨版本触发器。跨版本触发器仅在应用升级的过程中使用(我们在升级过程结束后将会尽快把它们删除)。跨版本触发器可以被用来“转送”旧版本上的一个修改到新的版本。它们也能被用来“返送”新版本所作的一个修改到旧版本。(接下来,在关于“在线版本变更”的这一系列文章的下一篇也就是终结篇中,我们将会使用一个返送型的跨版本触发器)
最后,为了使用新版本代码,我们必须替换现有的应用程序代码,而不会影响现有的应用程序。幸运的是,我们上次已经知道这一步很容易做到。为了完整性我们会再重复这个步骤。
在我们开始增加和删除列之前,我们必须介绍一下版本视图:应用程序和物理模式之间的缓冲区。这将会导致停机,是一次性的停机,你的应用程序将永远不需要重复这个步骤。
提示:在将来,当你开发新的应用程序的时候,从一开始就在你的应用程序中使用版本视图,这样你就可以完全避免这次停机。这个办法可以允许你轻而易举地重命名或重新排列你的表中的列,并且可以让你的应用系统结合物理模式更新进行在线升级。
为了嵌入版本视图,我们需要一个一次性的停机时段。这个过程通常需要以下几个步骤:
■把表改名,因为版本视图需要占用旧的名字。
■创建版本视图,把它命名为原表的名字。
■把当前表上的触发器删除,转移到版本视图。这是推荐做法,因为CREATE TRIGGER需要直接引用表名。你需要版本视图上的触发器拥有旧的表名。
■在版本视图上重新创建触发器。
■收回对基表的访问权限,赋予对版本视图的访问权限。
■其他步骤,例如把一个精细访问控制的策略从基表转移到版本视图。
我们的应用程序没有触发器和权限控制,所以我们可以跳过其中的几步。但是通常来说,如果你不单要进行应用程序代码修改,同时还要修改物理模式,那么为了做好准备,上述过程是你必须执行的。
我们假设我们已经有了在上一篇专栏中设置和使用过的DEMO账户。你可能记得,DEMO账户是一个普通模式,它仅仅有CREATE SESSION 和 CREATE PROCEDURE,另外还有为模式创建版本的权限(ALTER USER DEMO ENABLE EDITIONS),同时还能够使用一个现有的称为VERSION2 的版本(GRANT USE ON EDITION VERSION2 TO DEMO)。在这个例子中,我们会赋予DEMO账户创建表和序列的权限,这样我们就能够拷贝EMPLOYEES表;我们还要赋予创建视图和触发器的权限,除此之外的权限集保留原样。
我们来看看现有版本1.0的应用程序的设置:
SQL> create table
2 employees
3 as
4 select *
5 from hr.employees;
Table created.
SQL> create sequence emp_seq
2 start with 500;
Sequence created.
这就是我们要用的数据的拷贝;为了这次演示创建一个序列,它的开始号码大于任何一个EMPLOYEES中现有的号。现有的应用程序代码执行两个功能:给定一个搜索字串,显示一个EMPLOYEES报表,带有职员的电话号码和e-mails;雇用一个新职员,这将会修改现有的表。这个包的包头定义部分如代码清单1所示。
代码清单1: emp_pkg 包头,应用程序版本1.0
SQL> create or replace package emp_pkg
2 as
3 procedure show
4 ( last_name_like in employees.last_name%type );
5
6 function add
7 ( FIRST_NAME in employees.FIRST_NAME%type := null,
8 LAST_NAME in employees.LAST_NAME%type,
9 EMAIL in employees.EMAIL%type,
10 PHONE_NUMBER in employees.PHONE_NUMBER%type := null,
11 HIRE_DATE in employees.HIRE_DATE%type,
12 JOB_ID in employees.JOB_ID%type,
13 SALARY in employees.SALARY%type := null,
14 COMMISSION_PCT in employees.COMMISSION_PCT%type := null,
15 MANAGER_ID in employees.MANAGER_ID%type := null,
16 DEPARTMENT_ID in employees.DEPARTMENT_ID%type := null )
17 return employees.employee_id%type;
18 end;
19 /
Package created.
实现代码也是非常直观的,一个“展示”程序和一个简单的“新增职员”事务,如代码清单2所示。
代码清单2: emp_pkg 包体,应用程序版本1.0
SQL> create or replace package body emp_pkg
2 as
3
4 procedure show
5 ( last_name_like in employees.last_name%type )
6 as
7 begin
8 for x in
9 ( select first_name, last_name,
10 phone_number, email
11 from employees
12 where last_name like
13 show.last_name_like
14 order by last_name )
15 loop
16 dbms_output.put_line
17 ( rpad( x.first_name || ' ' ||
18 x.last_name, 40 ) ||
19 rpad( nvl(x.phone_number, ' '), 20 ) ||
20 x.email );
21 end loop;
22 end show;
23
24 function add
25 ( FIRST_NAME in employees.FIRST_NAME%type := null,
26 LAST_NAME in employees.LAST_NAME%type,
27 EMAIL in employees.EMAIL%type,
28 PHONE_NUMBER in employees.PHONE_NUMBER%type := null,
29 HIRE_DATE in employees.HIRE_DATE%type,
30 JOB_ID in employees.JOB_ID%type,
31 SALARY in employees.SALARY%type := null,
32 COMMISSION_PCT in employees.COMMISSION_PCT%type := null,
33 MANAGER_ID in employees.MANAGER_ID%type := null,
34 DEPARTMENT_ID in employees.DEPARTMENT_ID%type := null
35 )
36 return employees.employee_id%type
37 is
38 employee_id employees.employee_id%type;
39 begin
40 insert into employees
41 ( EMPLOYEE_ID, FIRST_NAME, LAST_NAME,
42 EMAIL, PHONE_NUMBER, HIRE_DATE,
43 JOB_ID, SALARY, COMMISSION_PCT,
44 MANAGER_ID, DEPARTMENT_ID )
45 values
46 ( emp_seq.nextval, add.FIRST_NAME, add.LAST_NAME,
47 add.EMAIL, add.PHONE_NUMBER, add.HIRE_DATE,
48 add.JOB_ID, add.SALARY, add.COMMISSION_PCT,
49 add.MANAGER_ID, add.DEPARTMENT_ID )
50 returning employee_id into add.employee_id;
51
52 return add.employee_id;
53 end add;
54
55 end;
56 /
Package body created.
现在我们可以看看这个应用程序在SQL*PLUS中如何工作,如代码清单3所示。
代码清单3: 在SQL*Plus 中运行应用程序版本1.0
SQL> exec emp_pkg.show( '%K%' );
Payam Kaufling 650.123.3234 PKAUFLIN
Alexander Khoo 515.127.4562 AKHOO
Steven King 515.123.4567 SKING
Janette King 011.44.1345.429268 JKING
Neena Kochhar 515.123.4568 NKOCHHAR
Sundita Kumar 011.44.1343.329268 SKUMAR
PL/SQL procedure successfully completed.
SQL> begin
2 dbms_output.put_line
3 ( emp_pkg.add
4 ( first_name => 'Tom',
5 last_name => 'Kyte',
6 email => 'TKYTE',
7 phone_number => '703.123.9999',
8 hire_date => sysdate,
9 job_id => 'IT_PROG' ) );
10 end;
11 /
500
PL/SQL procedure successfully completed.
SQL> exec emp_pkg.show( '%K%' );
Payam Kaufling 650.123.3234 PKAUFLIN
Alexander Khoo 515.127.4562 AKHOO
Janette King 011.44.1345.429268 JKING
Steven King 515.123.4567 SKING
Neena Kochhar 515.123.4568 NKOCHHAR
Sundita Kumar 011.44.1343.329268 SKUMAR
Tom Kyte 703.123.9999 TKYTE
PL/SQL procedure successfully completed.
现在我们来准备好我们的模式,使得它能允许应用程序的在线升级,包括物理模式的更新。请记住,这需要一次停机——最后一次的停机,这是为了把版本视图放进去。在这个例子中,创建版本视图包含下列步骤:
SQL> alter table employees
2 rename to employees_rt;
Table altered.
SQL> create editioning view employees
2 as
3 select
4 EMPLOYEE_ID, FIRST_NAME,
5 LAST_NAME, EMAIL, PHONE_NUMBER,
6 HIRE_DATE, JOB_ID, SALARY,
7 COMMISSION_PCT, MANAGER_ID,
8 DEPARTMENT_ID
9 from employees_rt
10 /
View created.
就是这样。一旦做好,我们又在线了。不仅如此,现有的应用程序百分之百不受影响;我们放进去的版本视图看起来、用起来都和表一样。现有的应用程序和以前一样运行。
现在我们增加我们的新列和索引,为发布新的应用程序做准备:
SQL> alter table employees_rt
2 add
3 ( country_code varchar2(3),
4 phone# varchar2(20)
5 )
6 /
Table altered.
SQL> create index employees_phone#_idx
2 on employees_rt(phone#)
3 ONLINE INVISIBLE
4 /
Index created.
从Oracle数据库11GR1开始,新增列可以在线操作。不仅如此,注意索引是如何可以在线创建(在Oracle数据库11GR1以上版本,是真正的在线操作)并可隐形(INVISIBLE)的。在这个特定例子中,这个INVISIBLE选项在技术上并不是必须的,因为PHONE#在现有的应用程序中从未以任何途径、形式、方式被引用过。我们在这里把它包括进去只是为了演示的完整性。
现在我们的任务是把数据从现有版本迁移到新版本。为了做到这一点,我们要依靠一个转送型的跨版本触发器,我们利用这个触发器来保证在当前版本中所有被插入或修改的数据会被发送到新版本中。利用这个触发器,我们不仅可以捕获旧程序产生的数据变更,而且还可以作批量数据迁移。也就是说,我们要写一段逻辑代码来根据旧的PHONE_NUMBER列维护COUNTRY_CODE和PHONE#列(我们必须得这样做,才能把当前应用程序的修改转送到新的物理模式),同时我们要利用这段逻辑来做数据迁移。一旦这个转送型的跨版本触发器准备就绪,利用DBMS_PARALLEL_EXECUTE包发出一个“update employees set phone_number = phone_number”指令来做数据迁移将是一件很简单的事。代码清单4展示了这个转送型的跨版本触发器。
代码清单 4: 跨版本触发器
SQL> alter session set edition = version2;
Session altered.
SQL> create or replace trigger employees_fwdxedition
2 before insert or update of phone_number on employees_rt
3 for each row
4 forward crossedition
5 declare
6 first_dot number;
7 second_dot number;
8 begin
9 if :new.phone_number like '011.%'
10 then
11 first_dot
12 := instr( :new.phone_number, '.' );
13 second_dot
14 := instr( :new.phone_number, '.', 1, 2 );
15 :new.country_code
16 := '+'||
17 substr( :new.phone_number,
18 first_dot+1,
19 second_dot-first_dot-1 );
20 :new.phone#
21 := substr( :new.phone_number,
22 second_dot+1 );
23 else
24 :new.country_code := '+1';
25 :new.phone# := :new.phone_number;
26 end if;
27 end;
28 /
Trigger created.
关于这个触发器有几件事要注意:
我们是在一个“未来”的版本中创建的,这个版本就是新的应用模式(VERSION2)。因此ALTER SESSION 语句是必需的。我们的目标是不影响当前的应用,所以我们只会在新版本中做这些版本相关的工作。你的跨版本触发器必须在“未来”版本中创建,永远不在旧版本的应用中创建。在触发器的第四行你可以见到这个“魔法”:FORWARD CROSSEDITION。这个子句意味着这个触发器仅仅在INSERT, UPDATE, 或DELETE语句的当前版本为旧版(即旧版本应用程序发出的这些语句)才会被触发。
现在我们已经准备好批量数据迁移。这可以用一个简单的UPDATE来做到:
SQL> update employees
set phone_number = phone_number;
109 rows updated.
但是请记住,这将会锁住整个表,因为它修改了每一行,而这是我们尽量要避免的。但是,在任何情况下,现有旧应用发出的任何插入或删除都会为我们把这些变更转送到新的列中。
在下一期,我们将会完成这个例子并演示:
■如何将批量的修改划分成小片,一点一点完成,这样就不会在任一时刻锁住表的太多数据,也就是说,如何在线地完成一个批量修改。
■安装新的应用程序代码,包括为这个新应用服务的新的版本视图。
■完成到新版本应用的切换,清理旧版本应用遗留下来的代码和列,这些已经用不着了。
然后我会给出这个例子的另一个结尾。它不是简单地“切断旧版本并开始使用新版本”,即“温切换”,我们要进行的是一个“热交割”,借助它我们可以使得旧版本和新版本同时并发运行,直到我们不再需要旧版本应用程序为止。这个热交割技术把停机时间从几秒缩减到根本没有。等这些做完,我们会看看如何利用在线版本变更来进行:
■为一个在线系统打补丁而不需要任何停机(第一期提到的)
■将一次应用系统升级的停机时间最小化到几秒钟,包括物理模式变更和批量修改操作
■完全去除停机时间
如果你等不及下一期的终结篇,我建议你下载:
http://www.Oracle.com/technetwor ... nition-1-133045.pdf
这是Bryn Llewellyn为在线版本变更写的白皮书。Llewellyn继续担任Oracle的PL/SQL产品经理,现在他还负责在线版本变更。Oracle数据库高级应用开发者指南,11gR2版 (11.2) 的第19章也包含了一个完整的在线版本变更教程。
第三部分:
http://www.Oracle.com/technetwor ... 0asktom-082672.html
在上一期的Oracle杂志中(2010年三/四月刊),我继续描述了Oracle数据库11gR2版的杀手级功能:在线版本变更。如果你还记得,在线版本变更使得你能够在线升级你的应用程序。
这是关于这一新功能的系列专栏文章的第三篇也是终结篇。如果你还没有读过前两篇,我建议你现在就去读一读,因为本篇专栏基于我们一直在使用的例子。
上次,我们的应用升级不是一个简单的应用程序补丁。它不仅替换了某些PL/SQL单元(补丁),而且修改了我们的应用程序所涉及到的一个表的物理结构。这是一个真正的应用升级,它包括了可版本化的对象类型的变更(我们例子中的PL/SQL单元),同时也包括了非版本化类型的对象的变更(我们的表结构)。以下的对象类型是可版本化的:
■同义词
■视图(包括版本视图,我们下面会给出定义)
■所有PL/SQL对象(函数,过程,包,等等)
其他所有的数据库对象类型是非版本化的。一个表是永远不可版本化的,这是有意的设计。这是因为它包含了数据,开发者必须明确决定哪些数据(也就是说,哪些表列)不会被升级所修改,哪些表列会被替换。回想上一篇专栏,为了给我们的应用创造一个表是可版本化的假象,我们利用版本视图隐藏了对表的物理模式修改。同一个版本视图在不同版本中出现,从表中仅仅读取应用程序在那一个特定版本中所需要的列。
让我们从本次的例子入手。在标准的HR模式中有一张EMPLOYEES(职员)表。这张公司的所有职员的清单包含了一些属性,例如姓,名,电子邮件,和电话号码。我们假装这张表历来就是一个美国公司的应用程序的一部分,所以电话号码是以美国公司能够方便辨认的格式来存储的。美国电话号码存储为一个地区号码,然后是一个7位数字,例如: 650.507.9876。国际号码的存储格式有一个美国“转义”码(在拨打国际长途的时候使用),紧跟着是国家号码,然后是电话号码,例如:011.44.1644.429262。
我们先来概括一下现在例子的状况。我们有一张应用程序中要用到的表,它来自标准的例子HR模式,确定地说就是EMPLOYEES表。这张某个公司所有职员的清单包括了这些属性,如:姓,名,电子邮件地址和电话号码。我们假装这张表历来就是一个美国公司的应用程序的一部分,所以电话号码是以美国公司能够方便辨认的格式来存储的。美国电话号码存储为一个地区号码,然后是一个7位数字,例如: 650.507.9876。国际号码的存储格式有一个美国“转义”码(在拨打国际长途的时候使用),紧跟着是国家号码,然后是电话号码,例如:011.44.1644.429262。
这个虚构的公司被收购,现在是一个全球化公司的一部分。为了适应新的母公司的标准,被收购公司必须修改它存储电话号码的格式。现在它利用两个字段来存储和现实所有的号码:一个国家号码,和一个电话号码。例如,两个电话号码例子前后的存储格式如下所示:
-----------------------------------------------------
修改前 | 修改后
--------------------|--------------------------------
PHONE_NUMBER | COUNTRY_CODE | PHONE#
--------------------|-----------------|--------------
650.507.9876 | +1 | 650.507.9876
011.44.1644.429262 | +44 | 1644.429262
-----------------------------------------------------
现在,这个小小的修改(事实上,任何的修改都不是“小小”的!)包含了几个步骤。具体地说,我们需要:
■修改模式,使得EMPLOYEE表包含两个新的列:COUNTRY_CODE 和 PHONE#
■修改模式,使得EMPLOYEE表不再包含PHONE_NUMBER列
■在PHONE#为一个搜索页面创建一个索引(也许还有其他索引,但我们先做一个索引)
■将PHONE_NUMBER的数据批量转移到COUNTRY_CODE and PHONE#
■将应用程序中依赖于PHONE_NUMBER列的代码替换掉
在我的前一篇专栏中,我们演练了版本视图的概念和实现。那个视图为我们的应用代码和底层的物理模式之间提供了一个缓冲区。我们在例子中引入的版本视图,配合Oracle数据库11GR1版以上所提供的非阻塞、快速新增列功能,使得我们能够增加两个新列而完全不对当前应用系统有任何影响。版本视图也使得我们能够把现有的PHONE_NUMBER列对新版本应用隐藏,效果上相当于使得我们能够在应用程序的版本2中虚拟地删除PHONE_NUMBER列,而不影响版本1。
我们还利用ONLINE和INVISIBLE功能,在新增的列上建立了一个新索引。从Oracle数据库11GR1版开始,CREATE INDEX ONLINE操作是100%的在线操作:它从未在任何时间点锁住表,而在以前的版本是要锁表的。新的INVISIBLE选项使得我们能够创建索引而不会影响当前应用版本的查询计划。我们使用这个选项是因为增加一个索引可能使得某些查询跑得更快,另一些维持原速度,而第三类查询可能变得更慢。因为我们没有用这个新索引测试过现有应用的查询计划,所以我们想让这个新索引隐藏起来。
上一期我们的例子停留在“批量迁移数据”的阶段。我们描述了一个单一的批量UPDATE操作来迁移数据会如何锁住整张表。在批量修改的过程中。EMPLOYEES表上的其他事务将被阻塞;事实上,现有的应用(版本1)已经离线了。此外,一旦这个单一的批量UPDATE结束,如果现有的应用(版本1)插入一个新行,或者修改一个现有记录的PHONE_NUMBER列,这些变更将不会反映到新列中。我们丢失了这些变更。
为了克服这些障碍,我们查看了两个新功能。首先我们讨论了DBMS_PARALLEL_EXECUTE包。(在Oracle杂志2009年十一/十二月刊的文章“Looking at the New Edition”中我们首次见识过这个功能,我给它取了个昵称叫做“DIY并行操作”http://www.Oracle.com/technetwor ... 9asktom-089919.html)利用DBMS_PARALLEL_EXECUTE包,我们能够把任务划分成很小的片, 以我们所需的任何并行度:从并行度1(其实就是串行)到1000不等,来修改表中的每一行,因而我们永远不需要锁住整个表,而是每次只锁住一小片。然后,我们引入了一个新型的触发器:跨版本触发器。跨版本触发器只在应用升级的过程中被使用(我们在升级过程结束后将会尽快把他们删除)。跨版本触发器可以被用来“转送”旧版本上的一个修改到新的版本。它们也能被用来“返送”新版本所作的一个修改到旧版本。
所以,为了赶上本篇的进度,我假设你已经准备好这几样东西:
■我们一直在使用的DEMO账户。
■作为ORA$BASE版本的子版本而创建的VERSION2版本。
■HR.EMPLOYEES表的一个拷贝,在DEMO模式中已经更名为EMPLOYEES_RT(“RT”代表“Real Table”(真正的表):这是我的命名习惯)。这个表已经被改过了,增加了我们需要的两个新列。此外,EMPLOYEES_PHONE#_IDX索引已经建好了。
■在ORA$BASE版本中的原来的EMP_PKG包,它代表我们的应用代码;它知道如何增加一个新职员,以及如何展示现有的数据。
■EMPLOYEES在ORA$BASE版本中的版本视图,它只返回了EMPLOYEES_RT的那些列,就是我们的应用程序所需要的。
■一个转送型的跨版本触发器EMPLOYEES_FWDXEDITION,它将旧格式的电话号码转换为新的两列格式的电话号码。
这是在第二篇专栏中提供的(代码参见http://www.Oracle.com/technetwor ... 0asktom-098897.html)。在例子的结尾我们留下了一个简单的UPDATE:
SQL> update employees
set phone_number = phone_number;
109 rows updated.
它进行了数据的批量迁移,而转送型的跨版本触发器保证了当前版本应用所做的后续修改会被反映到新列中。但那个UPDATE操作会锁住整个表,当它处理所有的行时。它将会是一个离线操作,而我们的目标是要使它成为一个在线升级。因此,为了完成整个应用升级,我们这次要讲到的步骤将会:
■利用DBMS_PARALLEL_EXECUTE包来将表划分成小片进行修改,使得批量迁移和现有应用程序能够并发执行
■安装新的应用,同时现有应用还处于运行状态
■安装一个返送型的跨版本触发器,它使得我们能够并发地执行新旧版本的应用代码,从而实现“热交割”,根本不需要任何停机时间。
■当新版本应用是唯一使用的版本时,作一个清理
使用DBMS_PARALLEL_EXECUTE包:
我们假装没有执行过那个UPDATE,而是为EMPLOYEES_RT表增加了列,同时将转送型的跨版本触发器准备就绪。现在我们想要修改EMPLOYEES_RT表的每一行来触发那个转送型的跨版本触发器,从而将旧列中的数据拷贝并转换到两个新列中。现在我们进入DBMS_PARALLEL_EXECUTE包。
在我的书《Oracle 9i10g编程艺术》中,我花了几页篇幅描述了如何将批量操作“并行”执行,采用的是一种DIY的并行化办法。这个方法是把一个表按照范围划片,使用ROWID(参见asktom.Oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:10498431232211这里有关于这个方法的概述)或者主键的范围(参见"分割一个大表" http://www.Oracle.com/technetwor ... 6asktom-101983.html 这里有关于这个方法的概述)。虽然我描述的方法都很直观,但它们毕竟是手工操作。你不得不拿过我的“技术”并且为你的特定情况作调整。
现在,没有必要调整了。在Oracle数据库11gR2版, 我们有一个简单办法,来达到和我的演示完全相同的目的。新的DBMS_PARALLEL_EXECUTE包能够把一个大表按照ROWID范围,或键值范围,或用户自定义方法进行分割。表被逻辑上进行分割,数据库在后台处理每一个范围,利用调度器,错误日志,重试,等等。但是,在我们使用DBMS_PARALLEL_EXECUTE之前,我们将不得不(为了演示的目的)扩充我们的EMPLOYEES_RT表,因为它现在实在是太小了。首先我们会把它扩大100倍,如清单1所示:
代码清单 1: 把 EMPLOYEES_RT 表扩大
SQL> insert into employees
2 select * from
3 (
4 with data(r)
5 as
6 (select 1 r from dual
7 union all
8 select r+1 from data where r <= 100
9 )
10 select rownum+(select max(employee_id)
11 from employees_rt),
12 FIRST_NAME, LAST_NAME, EMAIL,
13 PHONE_NUMBER, HIRE_DATE, JOB_ID,
14 SALARY, COMMISSION_PCT, MANAGER_ID,
15 DEPARTMENT_ID
16 from employees_rt, data
17 );
11009 rows created.
清单1在第4行到第9行使用了Oracle数据库11gR2版的一个新功能:递归的WITH子查询,来产生100个行。这个清单还作了一个和EMPLOYEES_RT的笛卡尔连接,这将会产生一个结果集,在这个集合中EMPLOYEES_RT的每行数据都被重复100遍。它还利用ROWNUM加上当前的最大的EMPLOYEE_ID来在我们的表上产生一个新的主键,并且插入这些新的行,立即使得我们的表比原来增大了100倍(严格地说是原来的101倍)。我们可以用清单2来查看现有的数据:
代码清单 2: 查看EMPLOYEES_RT 中的数据
SQL> select phone_number, country_code, phone#
2 from employees_rt
3 where country_code is null
4 and rownum <= 5
5 union all
6 select phone_number, country_code, phone#
7 from employees_rt
8 where country_code is NOT null
9 and rownum <= 5;
PHONE_NUMBER COUNTRY_CODE PHONE#
------------ ------------ -----------
650.507.9833
650.507.9844
515.123.4444
515.123.5555
603.123.6666
011.44.1344.429268 +44 1344.429268
011.44.1344.467268 +44 1344.467268
011.44.1344.429278 +44 1344.429278
011.44.1344.619268 +44 1344.619268
011.44.1344.429018 +44 1344.429018
10 rows selected.
SQL> select count(*),
2 count(distinct dbms_rowid.rowid_block_number(rowid)) cnt_blk
3 from employees
4 /
COUNT(*) CNT_BLK
------------ ---------
11118 138
注意,其中的一些行的新列已经被填入了数据。我们并没有插入数据,那是我们放入的转发型跨版本触发器在起作用。我们已经把它设置好了,所以当我们在当前的应用插入或者修改PHONE_NUMBER列时,转发型跨版本触发器将会自动把变动从旧版本应用使用的列转发到新应用使用的列。因此,这演示了在旧版本应用中新产生的数据会保持并且为我们透明地转换为新版本应用的格式。
接下来,这些步骤将执行对EMPLOYEES_RT表的批量修改操作而不会锁住整张表。我们需要修改所有当前的行,将会触动到PHONE_NUMBER列使得数据被从PHONE_NUMBER拷贝到COUNTRY_CODE和PHONE#列。我们当前的表有差不多138个数据块(如清单2所示),我们将会每次修改它的10%。(EMPLOYEES_RT是一张小表;在更大的表中,你可能会用一个小得多的百分比来避免一次锁定太多的表数据。)所以我们将会把它分割为10个数据块或者类似尺寸的小片。这样做的方法如清单3所示。
代码清单 3: 为批量修改创建数据划片
SQL> begin
2 dbms_parallel_execute.create_task('update employees_rt');
3 dbms_parallel_execute.create_chunks_by_rowid
4 ( task_name => 'update employees_rt',
5 table_owner => user,
6 table_name => 'EMPLOYEES_RT',
7 by_row => false,
8 chunk_size => 10);
9 end;
10 /
PL/SQL procedure successfully completed.
代码清单 3中的PL/SQL块做了两件事:首先它创建了一个带名字的任务,我们随后可以操纵它。它为这个任务所做的第一件事,是利用CREATE_CHUNKS_BY_ROWID过程加入表的数据划片。当代码调用到这个过程,过程会读取EMPLOYEES_RT表并把它按照10个数据块为一片来划分(chunk_size=>10)。请注意这个CREATE_CHUNKS_BY_ROWID API运行使用两种方式来为表划片:基于数据块数或者行数。因为代码里面用了by_row=>false,API使用了按数据块划片的方法。代码清单3的PL/SQL过程的结果可以在新的USER_PARALLEL_EXECUTE_CHUNKS视图看到,如清单4所示。
代码清单 4: 查看 USER_PARALLEL_EXECUTE_CHUNKS 里面的划片
SQL> select chunk_id, status, start_rowid, end_rowid
2 from user_parallel_execute_chunks
3 where task_name = 'update employees_rt'
4 and rownum <= 5
5 /
CHUNK_ID STATUS START_ROWID END_ROWID
---------- --------- ------------------ ------------------
134 UNASSIGNED AAAVkkAAEAAAA/YAAA AAAVkkAAEAAAA/fCcP
135 UNASSIGNED AAAVkkAAEAAAA/oAAA AAAVkkAAEAAAA/vCcP
136 UNASSIGNED AAAVkkAAEAAAA/wAAA AAAVkkAAEAAAA/3CcP
137 UNASSIGNED AAAVkkAAEAAAA/4AAA AAAVkkAAEAAAA//CcP
138 UNASSIGNED AAAVkkAAEAAABwIAAA AAAVkkAAEAAABwPCcP
我们可以看到一系列的数据片,实际上是ROWID的范围,它们逻辑上把这个表划成了小片。清单4中的每一个ROWID范围是不交叠的,但是所有范围覆盖了整张表。这就是我们如何将单个UPDATE划分成N个更小的UPDATE而不会彼此冲突。
现在我们已经准备好执行我们的UPDATE操作了。这是通过清单5中的代码来完成的,它使用两个执行线程(parallel_level=>2)运行我们的任务。我使用2只是为了演示你可以把某件东西划成比你并发运行的数目多得多的小片。我们知道我们至少有5片数据(如清单4所示),但我们每次只是执行其中的2片。我们执行的UPDATE被参数化,只接受一个ROWID范围并且操作那个范围内的记录。因此,利用“并行度2”,我们一点一点地修改了表中所有的行,每次只是锁住表的一个小小的子集。这使得我们正在同时批量迁移数据的时候,现有的应用能够正常运作。
代码清单 5: 运行UPDATE EMPLOYEES_RT 任务
SQL> begin
2 dbms_parallel_execute.run_task
3 ( task_name => 'update employees_rt',
4 sql_stmt => 'update employees_rt
5 set phone_number = phone_number
6 where rowid between :start_id
7 and :end_id',
8 language_flag => DBMS_SQL.NATIVE,
9 parallel_level => 2 );
10 end;
11 /
PL/SQL procedure successfully completed.
注意:用并行模式执行任务需要一个具有CREATE JOB权限的模式,因为RUN_TASK会利用调度器来并行地执行更新。如果你串行地执行任务,则CREATE JOB权限不是必需的。
操作完成后,如果我们对结果满意,可以把创建的任务删掉:
SQL> begin
2 dbms_parallel_execute.drop_task('update employees_rt');
3 end;
4 /
PL/SQL procedure successfully
completed.
现在我们已知所有的旧数据已经被转换和修改到新应用中,并且转送型的跨版本触发器会自动转换和修改所有“新产生的旧格式数据”,所以我们是安全的。
安装新应用的代码:
现在我们想要安装我们的新应用代码。我们采用的方法,和这个三部分系列文章中的第一篇中使用的方法相同:我们简单地切换到新版本并且CREATE OR REPLACE我们的可版本化对象(即EMPLOYEES版本视图和EMP_PKG包)。注意为了简洁起见,并不是所有代码都在清单6中列出,而仅仅是对当前代码的变动。
代码清单 6: 安装新的应用
SQL> alter session set edition = version2;
Session altered.
SQL>
SQL> select object_name, object_type, status, edition_name
2 from user_objects_ae
3 where object_name in ( 'EMPLOYEES', 'EMP_PKG' );
OBJECT_NAME OBJECT_TYPE STATUS EDITION_NAME
----------- ----------- ------ ------------
EMPLOYEES VIEW VALID ORA$BASE
EMP_PKG PACKAGE VALID ORA$BASE
EMP_PKG PACKAGE BODY VALID ORA$BASE
这个清单显示我们正在使用VERSION2版本,并且现在所有的代码从现有的版本(version 1),即ORA$BASE版本中的应用程序继承过来。
现在我们替换视图和包,如清单7所示。
代码清单 7: 替换视图和包
SQL> create OR REPLACE editioning view employees
2 as
3 select EMPLOYEE_ID, FIRST_NAME,
4 LAST_NAME, EMAIL, COUNTRY_CODE, PHONE#,
...
9 /
View created.
SQL> create or replace package emp_pkg
2 as
...
9 EMAIL in employees.EMAIL%type,
10 COUNTRY_CODE in employees.COUNTRY_CODE%type := null,
11 PHONE# in employees.PHONE#%type := null,
12 HIRE_DATE in employees.HIRE_DATE%type,
...
18 return employees.employee_id%type;
19 end;
20 /
Package created.
SQL> create or replace package body emp_pkg
2 as
3
4 procedure show
...
9 ( select first_name, last_name,
10 country_code, phone#, email
11 from employees
12 where last_name like
13 show.last_name_like
14 order by last_name )
15 loop
16 dbms_output.put_line
17 ( rpad( x.first_name || ' ' ||
18 x.last_name, 40 ) ||
19 rpad( nvl(x.country_code, ' '), 5 ) ||
20 rpad( nvl(x.phone#, ' '), 20 ) ||
21 x.email );
22 end loop;
23 end show;
24
25 function add
26 ( FIRST_NAME in employees.FIRST_NAME%type := null,
27 LAST_NAME in employees.LAST_NAME%type,
28 EMAIL in employees.EMAIL%type,
29 COUNTRY_CODE in employees.COUNTRY_CODE%type := null,
30 PHONE# in employees.PHONE#%type := null,
...
37 )
38 return employees.employee_id%type
39 is
40 employee_id employees.employee_id%type;
41 begin
42 insert into employees
43 ( EMPLOYEE_ID, FIRST_NAME, LAST_NAME,
44 EMAIL, COUNTRY_CODE, PHONE#, HIRE_DATE,
45 JOB_ID, SALARY, COMMISSION_PCT,
...
55 end add;
56
57 end;
58 /
Package body created.
现在我们可以看到两个版本都安装了,如清单8所示:
代码清单 8: 查看已安装的两个版本的应用
SQL> select object_name, object_type, status, edition_name
2 from user_objects_ae
3 where object_name in ( 'EMPLOYEES', 'EMP_PKG' );
OBJECT_NAME OBJECT_TYPE STATUS EDITION_NAME
----------- ----------- ------ ------------
EMP_PKG PACKAGE BODY VALID ORA$BASE
EMP_PKG PACKAGE VALID ORA$BASE
EMPLOYEES VIEW VALID ORA$BASE
EMP_PKG PACKAGE BODY VALID VERSION2
EMP_PKG PACKAGE VALID VERSION2
EMPLOYEES VIEW VALID VERSION2
6 rows selected.
安装返送型跨版本触发器:
两个版本都已经安装了,但还未真正就绪。ORA$BASE中的代码已经准备好,但VERSION2中的代码还不是完全到位。如果我们调用了VERSION2中的EMP_PKG.ADD过程,会怎么样?它会填写COUNTRY_CODE和PHONE#列,但不会写入旧的PHONE_NUMBER列!现在进入返送型跨版本触发器, 如清单9所示。
代码清单 9: 创建返送型跨版本触发器
SQL> create or replace trigger employees_revxedition
2 before insert or update of country_code,phone# on employees_rt
3 for each row
4 reverse crossedition
5 declare
6 first_dot number;
7 second_dot number;
8 begin
9 if :new.country_code = '+1'
10 then
11 :new.phone_number :=
12 :new.phone#;
13 else
14 :new.phone_number :=
15 '011.' ||
16 substr( :new.country_code, 2 ) ||
17 '.' || :new.phone#;
18 end if;
19 end;
20 /
Trigger created.
现在我们有了一个触发器,它仅仅在VERSION2版本应用程序的上下文中触发,并且会将数据维护到旧版本应用的格式。它将会存储不带国家代码的美国电话号码,或者存储美国格式的国外号码,采用的是011.国家代码.电话号码的格式。只要它准备就绪,我们就可以使用VERSION2的新应用代码了:
SQL> begin
2 dbms_output.put_line
3 ( emp_pkg.add
4 ( first_name => 'Tom'
5 last_name => 'Kight',
6 email => 'TKYTE',
7 country_code => '+44',
8 phone# => '703.123.4567',
9 hire_date => sysdate,
10 job_id => 'IT_PROG' ) );
11 end;
12 /
502
PL/SQL procedure successfully
completed.
SQL> exec emp_pkg.show( 'Kight' );
Tom Kight +44 703.123.4567 TKYTE
PL/SQL procedure successfully
completed.
我们可以看到数据被正确地输入了。不仅如此,在ORA$BASE版本我们可以验证旧的数据格式依然有效:
SQL> connect demo/demo
Connected.
demo> exec emp_pkg.show( 'Kight' );
Tom Kight 011.44.703.123.4567 TKYTE
请记住,因为数据库的缺省版本是ORA$BASE,任何会话缺省地会使用(看到)这个版本。所以只要简单地再次登录,我们就会运行旧版本的应用程序并且看到旧的PHONE_NUMBER列的输出格式。
设置缺省版本:
为了让新版本成为缺省版本,我们会用一个AFTER LOGON触发器来为每个新创建的会话设置版本。这使得新的会话会看到我们的新版本代码,同时现有的会话继续执行旧的版本。因为现有版本继续执行旧代码,我们可以完全去除可怕的“ORA-4068 existing state of packages has been discarded”错误,在过去每当你替换现有的代码时,你往往会得到这个错误。这个功能进一步增强了应用系统升级的能力。我们不仅仅消除了应用升级过程中的停机时间,而且还消除了切换到新版本的停机时间。
这个AFTER LOGON触发器将会使用DBMS_SESSION包中的一个叫做SET_EDITION_DEFERRED的新的API调用。这个API切换到指定版本,该版本是一个单独参数,而且是以一种延迟的方式,这意味着切换发生在(登录之后)的数据库调用完成之后。会话版本的延迟设置是必需的,这是为了防止发生“代码该做什么?”这样的问题。如果版本切换在初始化调用的时候立即发生,就会出现这种问题。假设有一段代码执行了这样的逻辑:
Begin
调用存储过程Proc;
立即切换到另一个不同版本;
调用存储过程Proc;
End;
如果在执行这段代码的过程中,PROC有两个不同的版本怎么办?它应该执行两个不同版本的代码吗?那怎么可能(又有什么合理性)?并非如此,版本的切换是延迟发生的,它发生在初始化调用之后。那样的话,所发生的一切就不会有歧义。
写代码来把我们的新版本应用设置为新会话的缺省版本,是很简单的:
SQL> grant use on
edition version2 to public;
Grant succeeded.
SQL> create or
replace trigger set_edition_on_logon
2 after logon on database
3 begin
4 dbms_session.set_edition_deferred( 'VERSION2' );
5 end;
6 /
Trigger created.
GRANT语句是必不可少的,它使得所有用户都可见到版本VERSION2(当然,你也可以把它授权给一个更小的用户群,这取决于你的需求),而触发器则让我们的新版本就绪。现在,每当我们登录到数据库,我们会看到:
SQL> connect demo/demo
Connected.
demo> SELECT SYS_CONTEXT ('userenv','current_edition_name') sc
2 FROM DUAL;
SC
---------
VERSION2
demo> exec emp_pkg.show( 'Kight' );
Tom Kight +44 703.123.4567 TKYTE
PL/SQL procedure successfully
completed.
清理:
现在剩下的工作就是清理。清理必须发生在所有人都不再使用ORA$BASE版本之后,也就是没有任何现存的会话使用旧代码。我们可以查看数据库里的每个会话正在使用哪个版本,只要查询V$SESSION的SESSION_EDITION_ID列(这个列可以被连接到*_OBJECT视图的OBJECT_ID, 这样你就可以看到版本名称),如果没有人在使用ORA$BASE,我们就指定可以进行清理了。在此情况下,清理包括了:
■删除转送和返送跨版本触发器
■你也可以删除PHONE_NUMBER列,或者把它设置为UNUSED
就是这样。我们已经完成了应用系统的在线升级!