Edition-Based Redefinition

  • 2021 年 1 月 29 日
  • 筆記

Oracle在11g引入了Edition-Based Redefinition(EBR),主要是为了解决在更新数据库对象,比如PL/SQL程序,视图等,如果该对象被锁住了,会导致更新必须等待,如果要使更新立即完成,则需要停止应用的问题。实现方式就是通过创建版本,新的代码在子版本实现,通过指定版本来对新旧版本代码进行切换。

这个功能主要用在有大量PL/SQL程序的数据库,例如Oracle EBS数据库。

EBR通过版本化的方式可实现以下目标:

  • 代码的改变,包括PL/SQL,表定义等,安装在子(新)版本中

  • 数据的改变只写到新的表或新的列,父(老)版本不会看到

  • 跨版本触发器将父(老)版本应用所做改变反映到子(新)版本中,反之亦然

EBR依赖于3类新的对象,即edition、editioning view和crossedition trigger。

  • 若只改变视图,同义词和PL/SQL对象,edition就够了

  • 若表结构和数据的改变并是在后端完成,不涉及终端用户,则只需edition和editioning view

  • 若表结构和数据的改变是由终端用户发起,则三者都需要

Edition

版本化和非版本化对象

Edition是非Schema对象,不属于任何用户,从11gR2开始,每个数据库都有一个默认版本,即ORA$BASE。

SQL> show edition  

EDITION
------------------------------
ORA$BASE

SQL> SELECT SYS_CONTEXT('userenv', 'current_edition_name') from dual;

SYS_CONTEXT('USERENV','CURRENT_EDITION_NAME')
--------------------------------------------------------------------------------
ORA$BASE

SQL> select edition_name,parent_edition_name,usable from dba_editions;

EDITION_NAME                   PARENT_EDITION_NAME            USA
------------------------------ ------------------------------ ---
ORA$BASE                                                      YES

可版本化对象

不是所有的模式对象都是可版本化(editionable )的,可版本化的模式对象有:

  • SYNONYM
  • VIEW
  • All PL/SQL object types:
    • FUNCTION
    • LIBRARY
    • PACKAGE and PACKAGE BODY
    • PROCEDURE
    • TRIGGER
    • TYPE and TYPE BODY

除此之外,其他对象,例如表,公有同义词,都是不支持版本化的。

如果可版本化对象的所有者是editions-enabled ,则该对象是editioned ,否则该对象是potentially editioned

版本化对象规则

  • A noneditioned object cannot depend on an editioned object.

    For example:

    • A public synonym cannot refer to an editioned object.
    • A function-based index cannot depend on an editioned function.
    • A materialized view cannot depend on an editioned view.
    • A table cannot have a column of a user-defined data type (collection or Abstract Data Type (ADT)) whose owner is editions-enabled.
    • A noneditioned subprogram cannot have a static reference to a subprogram whose owner is editions-enabled.

    For the reason for this rule, see “Actualizing Referenced Objects”.

  • An ADT cannot be both editioned and evolved.

    For information about type evolution, see Oracle Database Object-Relational Developer’s Guide.

  • An editioned object cannot be the starting or ending point of a FOREIGN KEY constraint.

    The only editioned object that this rule affects is an editioned view. An editioned view can be either an ordinary view or an editioning view.

用户启用版本

可以在CREATE USER或者ALTER USER语句中使用使用ENABLE EDITIONS语句来为用户启用版本。该操作是不可逆的,一旦为用户启用版本,则该用户现有可版本化对象及后续创建的可版本化对象就自动版本化了。

如果启用版本的两个用户之间存在相关的依赖关系,还需要使用FORCE关键字先为某一个用户启用版本。例如用户A有可版本化的对象a1和a2,用户B有可版本的对象b1和b2,对象a1依赖对象b1,对象b2依赖对象a2,则为用户A和用户B启用版本步骤如下:

  1. Using FORCE, enable editions for user A:

    ALTER USER A ENABLE EDITIONS FORCE;
    

    Now a1 and a2 are editioned objects, and b2 (which depends on a2) is invalid.

  2. Enable editions for user B:

    ALTER USER B ENABLE EDITIONS;
    
  3. Recompile b2, using the appropriate ALTER statement with COMPILE. For a PL/SQL object, also specify REUSE SETTINGS.

    For example, if b2 is a procedure, use this statement:

    ALTER PROCEDURE b2 COMPILE REUSE SETTINGS
    

创建版本

使用CREATE EDITION语句来创建版本。

继承对象和实际对象

每个数据库会话一次只能使用一个版本。创建时,子版本从其父版本继承数据库中在父版本中可见的所有已版本化对象。每个继承的对象在子版本中可见。

例子: creates a procedure named hello in the edition ora$base, and then creates the edition e2 as a child of ora$base. When e2 invokes hello, it invokes the inherited procedure. Then e2 changes hello, actualizing it. The procedure hello in the edition ora$base remains unchanged, and is no longer visible in e2. Now when e2 invokes hello, it invokes the actual procedure.

先创建用户,授予权限,启用版本,切换到新创建的用户

SQL> create user user1 identified by user1 default tablespace users;

User created.

SQL> grant dba to user1;

Grant succeeded.

SQL> alter user user1 enable editions;

User altered.

SQL> conn user1/user1
Connected.

再执行如下步骤:

  1. Create procedure in parent edition:

    CREATE OR REPLACE PROCEDURE hello IS
      BEGIN
        DBMS_OUTPUT.PUT_LINE('Hello, edition 1.');
      END hello;
    /
    
  2. Invoke procedure in parent edition:

    BEGIN hello(); END;
    /
    

    Result:

    Hello, edition 1.
     
    PL/SQL procedure successfully completed.
    
  3. Create child edition:

    CREATE EDITION e2;
    
  4. Use child edition:

    ALTER SESSION SET EDITION = e2;
    

    For information about ALTER SESSION SET EDITION, see “Changing Your Session Edition”.

  5. In child edition, invoke procedure:

    BEGIN hello(); END;
    /
    

    Child edition inherits procedure from parent edition. Child edition invokes inherited procedure. Result:

    Hello, edition 1.
     
    PL/SQL procedure successfully completed.
    
  6. Change procedure in child edition:

    CREATE OR REPLACE PROCEDURE hello IS
      BEGIN
        DBMS_OUTPUT.PUT_LINE('Hello, edition 2.');
      END hello;
    /
    

    Child changes only its own copy of procedure. Child’s copy is an actual object.

  7. Invoke procedure:

    BEGIN hello(); END;
    /
    

    Child invokes its own copy, the actual procedure:

    Hello, edition 2.
    
    PL/SQL procedure successfully completed.
    
  8. Return to parent:

    ALTER SESSION SET EDITION = ora$base;
    
  9. Invoke procedure and see that it has not changed:

    BEGIN hello(); END;
    /
    

    Result:

    Hello, edition 1.
     
    PL/SQL procedure successfully completed.
    

删除继承对象

如果子版本的用户删除继承对象,则该对象在子版本中不再可见,但在父版本中仍然可见。

例子: creates a procedure named goodbye in the edition ora$base, and then creates edition e2 as a child of ora$base. After e2 drops goodbye, it can no longer invoke it, but ora$base can still invoke it.

先删除之前创建的版本:

SQL> show edition

EDITION
------------------------------
ORA$BASE

SQL> drop edition e2 cascade;

Edition dropped.

再执行以下步骤:

  1. Create procedure in edition ora$base:

    CREATE OR REPLACE PROCEDURE goodbye IS
      BEGIN
        DBMS_OUTPUT.PUT_LINE('Good-bye!');
      END goodbye;
    /
    
  2. Invoke procedure:

    BEGIN goodbye; END;
    /
    

    Result:

    Good-bye!
     
    PL/SQL procedure successfully completed.
    
  3. Create edition e2 as a child of ora$base:

    CREATE EDITION e2;
    
  4. Use edition e2:

    ALTER SESSION SET EDITION = e2;
    

    ALTER SESSION SET EDITION must be a top-level SQL statement. For more information, see “Changing Your Session Edition”.

  5. In e2, invoke procedure:

    BEGIN goodbye; END;
    /
    

    e2 invokes inherited procedure:

    Good-bye!
     
    PL/SQL procedure successfully completed.
    
  6. In e2, drop procedure:

    DROP PROCEDURE goodbye;
    
  7. In e2, try to invoke dropped procedure:

    BEGIN goodbye; END;
    /
    

    Result:

    BEGIN goodbye; END;
          *
    ERROR at line 1:
    ORA-06550: line 1, column 7:
    PLS-00201: identifier 'GOODBYE' must be declared
    ORA-06550: line 1, column 7:
    PL/SQL: Statement ignored
    
  8. Return to parent:

    ALTER SESSION SET EDITION = ora$base;
    
  9. In parent, invoke procedure:

    BEGIN goodbye; END;
    /
    

    Result:

    Good-bye!
     
    PL/SQL procedure successfully completed.
    

例子e2 creates a function named goodbye and then an edition named e3 as a child of e2. When e3 tries to invoke the procedure goodbye (which e2 dropped), an error occurs, but e3 successfully invokes the function goodbye (which e2 created).

  1. Return to e2:

    ALTER SESSION SET EDITION = e2;
    

    For information about ALTER SESSION SET EDITION, see “Changing Your Session Edition”.

  2. In e2, create function named goodbye:

    CREATE OR REPLACE FUNCTION goodbye
      RETURN BOOLEAN
    IS
    BEGIN
      RETURN(TRUE);
    END goodbye;
    /
    
  3. Create edition e3:

    CREATE EDITION e3 AS CHILD OF e2;
    
  4. Use edition e3:

    ALTER SESSION SET EDITION = e3;
    
  5. In e3, try to invoke procedure goodbye:

    BEGIN
      goodbye;
    END;
    /
    

    Result:

      goodbye;
      *
    ERROR at line 2:
    ORA-06550: line 2, column 3:
    PLS-00221: 'GOODBYE' is not a procedure or is undefined
    ORA-06550: line 2, column 3:
    PL/SQL: Statement ignored
    
  6. In e3, invoke function goodbye:

    BEGIN
      IF goodbye THEN
        DBMS_OUTPUT.PUT_LINE('Good-bye!');
      END IF;
    END;
    /
    

    Result:

    Good-bye!
     
    PL/SQL procedure successfully completed.
    

版本授权

版本创建者可以使用语句 GRANT USE ON EDITION 将版本的使用权限授予其他用户。

如果要授予所有用户,则可以使用以下两种方式:

  • Grant the USE privilege on the edition to PUBLIC:

    GRANT USE ON EDITION edition_name TO PUBLIC
    
  • Make the edition the database default edition:

    ALTER DATABASE DEFAULT EDITION = edition_name
    

    This has the side effect of granting the USE privilege on edition_name to PUBLIC.

当前版本和会话版本

数据库会话在任何时间使用的版本为当前版本(current edition),当数据库会话开始,当前版本就是其会话版本(session edition),改变会话版本,当前版本也会随之改变。但是,也会有当前版本与会话版本不同的情况。

初始会话版本

当连接数据库时,可以指定初始会话版本,通过以下SQL查询可以指定的版本:

SELECT EDITION_NAME FROM ALL_EDITIONS;

那么如何在连接数据库的时候指定版本呢,可以通过在service中指定,Oracle推荐使用 srvctl add service 或者srvctl modify service 命令,通过-t选项指定服务的默认初始会话版本。

如果连接时没有指定版本,则会使用数据库默认版本作为初始会话版本。

修改会话版本

使用 ALTER SESSION SET EDITION 语句修改当前会话版本。

查看当前版本和会话版本

查看当前版本:

SELECT SYS_CONTEXT('USERENV', 'CURRENT_EDITION_NAME') FROM DUAL;

查看会话版本:

SELECT SYS_CONTEXT('USERENV', 'SESSION_EDITION_NAME') FROM DUAL;

退役版本

通过收回edition的use权限实现。

删除版本

系统默认的edition不能删除。
必须没有人使用此edition时才可删除。
或者其没有子edition,或者其子edition中没有editioned对象时才可删除。

Editioning view

由于表是不能被版本化的,那么如果要更改表的结构,如增加字段,这时就需要使用版本视图来向用户提供不同版本下的表结构。

Crossedition Triggers

如果发布版本时还涉及到数据的变化,那么就需要使用跨版本触发器。跨版本触发器分为:

  • 正向跨版本触发器:将父版本表字段的数据变化同步到子版本相关字段中,在父版本触发
  • 反向跨版本触发器:将子版本表字段的数据变化同步到父版本相关字段中,在子版本触发

实战

使用EBR来进行在线程序更新,具体是将HR用户下的EMPLOYEES表 PHONE_NUMBER 字段拆分为 COUNTRY_CODE和PHONE_NUMBER_WITHIN_COUNTRY两个字段 ,这里涉及到表结构和数据的变化,需要用到Editioning view和Crossedition Triggers。

准备工作

  1. 安装数据库
  2. 下载和解压 ebr.zip

配置环境

使用SYS用户连接到数据库:

SQL> conn / as sysdba
Connected.

使用HR用户连接到数据库,默认使用的是父版本ORA$BASE:

SQL> conn hr/hr
Connected.

SQL> show edition

EDITION
------------------------------
ORA$BASE

EBR准备

在SYS用户下为HR用户启用版本:

SQL> ALTER USER hr ENABLE EDITIONS;

User altered.

在HR用户下重命名需要修改的表:

SQL> ALTER TABLE employees RENAME TO employees_;

Table altered.

在HR用户下为重命名的表创建版本视图,视图的名字为表之前的名字:

SQL> CREATE EDITIONING VIEW employees AS
  2  SELECT
  3  employee_id, first_name, last_name, email, PHONE_NUMBER, hire_date, job_id, salary, commission_pct, manager_id, department_id
  4  FROM employees_;

View created.

创建子版本

在SYS用户下基于当前默认版本Ora$Base创建新的版本post_upgrade:

SQL> CREATE EDITION post_upgrade AS CHILD OF Ora$Base;

Edition created.

使用SYS用户将版本post_upgrade的USE权限授予HR用户:

SQL> GRANT USE ON EDITION post_upgrade TO hr;

Grant succeeded.

使用子版本

在HR用户下设置当前版本为post_upgrade:

SQL> ALTER SESSION SET EDITION = post_upgrade;

Session altered.

修改表结构

在HR用户下修改employees_表结构:

SQL> ALTER TABLE employees_ ADD
  2  (COUNTRY_CODE VARCHAR2(5),
  3  PHONE_NUMBER_WITHIN_COUNTRY VARCHAR2(20));

Table altered.

修改版本视图

在HR用户下,在子版本post_upgrade中,修改之前在父版本Ora$Base中创建的版本视图employees,增加字段COUNTRY_CODE和PHONE_NUMBER_WITHIN_COUNTRY :

SQL> CREATE OR REPLACE EDITIONING VIEW employees AS
  2  SELECT employee_id, first_name, last_name, email, phone_number, hire_date, job_id, salary, commission_pct, manager_id, department_id, phone_number_within_country, country_code
  3  FROM employees_;

View created.

创建正向跨版本触发器

在HR用户下,在子版本post_upgrade中,执行脚本fwd_ce.sql,具体代码为:

-- The procedure called by empl_forward
VARIABLE Warnings VARCHAR2(4000)
begin
  :Warnings := $$Plsql_Warnings;
end;
/
--------------------------------------------------------------------------------

alter session set Plsql_Warnings = ' enable:all, disable:06005, disable:06006'
/
create or replace procedure Set_Country_Code_And_Phone_No(
  Phone_Number     in  varchar2, 
  Country_Code     out nocopy varchar2,
  Phone_Number_V2  out nocopy varchar2)
is
  Char_To_Number_Error exception;
  pragma Exception_Init(Char_To_Number_Error, -06502);
  Bad_Phone_Number exception;
  Nmbr varchar2(30) := Replace(Phone_Number, '.', '-');

  function Is_US_Number(Nmbr in varchar2) return boolean is
    Len number := Length(Nmbr);
    Dash_Pos number := Instr(Nmbr, '-');
    n pls_integer;
  begin
    if Len is null or Len <> 12 then
      return false;
    end if;

    if Dash_Pos is null or Dash_Pos <> 4 then return false; end if;
    begin
      n := To_Number(Substr(Nmbr, 1, 3));
    exception when Char_To_Number_Error then
      return false;
    end;

    Dash_Pos := Instr(Nmbr, '-', 5);
    if Dash_Pos is null or Dash_Pos <> 8 then return false; end if;
    begin
      n := To_Number(Substr(Nmbr, 5, 3));
    exception when Char_To_Number_Error then
      return false;
    end;

    begin
      n := To_Number(Substr(Nmbr, 9));
    exception when Char_To_Number_Error then
      return false;
    end;
    return true;
  end Is_US_Number;
begin
  if Nmbr like '011-%' then
    declare
      Dash_Pos number := Instr(Nmbr, '-', 5);
    begin
      Country_Code := '+'||To_Number(Substr(Nmbr, 5, Dash_Pos-5));

      Phone_Number_V2 := Substr(Nmbr, Dash_Pos+1);



    exception when Char_To_Number_Error then
      raise Bad_Phone_Number;
    end;
  elsif Is_US_Number(Nmbr) then
    Country_Code := '+1';
    Phone_Number_V2 := Nmbr;
  else
    raise Bad_Phone_Number;
  end if;
exception when Bad_Phone_Number then
  Country_Code := '+0';
  Phone_Number_V2 := '000-000-0000';
end Set_Country_Code_And_Phone_No;
/

--------------------------------------------------------------------------------
declare
  Stmt constant varchar2(32767) := '
    alter session set Plsql_Warnings = '''||:Warnings||'''';
begin
  execute immediate Stmt;
end;
/

-- The trigger empl_forward

CREATE OR REPLACE TRIGGER empl_forward
BEFORE INSERT OR UPDATE ON employees_
FOR EACH ROW
FORWARD CROSSEDITION
DISABLE  
BEGIN
Set_Country_Code_And_Phone_No(
    :New.Phone_Number,
    :New.Country_Code,
    :New.Phone_Number_Within_Country);
END;

这段代码在表employees_ 创建了一个正向跨版本触发器empl_forward,当在父版本Ora$Base上对表employees_ 进行DML操作时就会触发该触发器,将对父版本的修改传递到子版本去。

SQL> @fwd_ce.sql   

PL/SQL procedure successfully completed.


Session altered.


SP2-0804: Procedure created with compilation warnings


PL/SQL procedure successfully completed.

 12  /

Trigger created.

创建反向扩版本触发器

在HR用户下,在子版本post_upgrade中,执行脚本rev_ce.sql,具体代码为:

CREATE OR REPLACE TRIGGER empl_reverse
  BEFORE INSERT OR UPDATE ON employees_
  FOR EACH ROW
  REVERSE CROSSEDITION
  DISABLE
BEGIN
    :NEW.phone_number :=
    CASE :NEW.country_code
      WHEN '+1' THEN
        REPLACE(:NEW.phone_number_within_country, '-', '.')
      ELSE
        '011.'||LTRIM(:NEW.country_code, '+')||'.'||
          REPLACE(:NEW.phone_number_within_country, '-', '.')
    END;
END employees_reverse;
/

这段代码在表employees_ 创建了一个反向跨版本触发器empl_reverse,当在子版本post_upgrade上对表employees_ 进行DML操作时就会触发该触发器,将对子版本的修改传递到父版本去。

SQL> @rev_ce.sql

Trigger created.

正向操作

在HR用户下,在子版本post_upgrade中,执行脚本bulk_fwd.sql,具体代码为:

ALTER trigger empl_forward enable
/
ALTER trigger empl_reverse enable
/
DECLARE
  c number := DBMS_Sql.Open_Cursor();
  x number;
BEGIN
  DBMS_Sql.Parse(
    c                          => c,
    Language_Flag              => DBMS_Sql.Native,
    Statement                  => 'update employees set employee_id = employee_id',
    Apply_Crossedition_Trigger => 'empl_forward');

  x := DBMS_Sql.Execute(c);
  DBMS_Sql.Close_Cursor(c);
  commit;
end;
/

这段代码先启用前面创建的两个触发器,然后执行一个update语句,并触发正向跨版本触发器empl_forward。

SQL> @bulk_fwd.sql

Trigger altered.


Trigger altered.


PL/SQL procedure successfully completed.

执行成功后,再执行脚本ver_fwd.sql查看结果,具体代码为:

--切换到父版本,执行update会触发正向跨版本触发器
ALTER SESSION SET EDITION =ORA$BASE;
SELECT phone_number FROM employees WHERE employee_id=101;
UPDATE employees SET phone_number = '515.123.4444' WHERE employee_id =101;
COMMIT;

ALTER SESSION SET EDITION = post_upgrade;
SELECT employee_id, country_code, phone_number_within_country 
FROM employees WHERE employee_id =101;

执行结果如下:

SQL> @ver_fwd.sql

Session altered.


PHONE_NUMBER
--------------------
515.123.4568


1 row updated.


Commit complete.


Session altered.


EMPLOYEE_ID COUNT PHONE_NUMBER_WITHIN_
----------- ----- --------------------
        101 +1    515-123-4444

在父版本插入或者更新employees_表之前,empl_forward触发器使用PHONE_NUMBER字段的新值更新字段COUNTRY_CODE和PHONE_NUMBER_WITHIN_COUNTRY。所以在子版本中,查询COUNTRY_CODE和PHONE_NUMBER_WITHIN_COUNTRY就可以看到更新的值。

反向操作

在HR用户下,在子版本post_upgrade中,执行脚本ver_rev.sql,具体代码为:

--在子版本执行update会触发反向跨版本触发器
ALTER SESSION SET EDITION =post_upgrade;
UPDATE employees SET phone_number_within_country = '515.123.4567'
WHERE employee_id =101;
SELECT employee_id, country_code, phone_number_within_country
FROM employees WHERE employee_id=101;
COMMIT;

ALTER SESSION SET EDITION = ORA$BASE;
SELECT employee_id, phone_number 
FROM employees WHERE employee_id =101;

执行结果如下:

SQL> @ver_rev.sql

Session altered.


1 row updated.


EMPLOYEE_ID COUNT PHONE_NUMBER_WITHIN_
----------- ----- --------------------
        101 +1    515.123.4567


Commit complete.


Session altered.


EMPLOYEE_ID PHONE_NUMBER
----------- --------------------
        101 515.123.4567

当在子版本post_upgrade中向表employees_插入数据或者更新PHONE_NUMBER_WITHIN_COUNTRY字段时,就会触发empl_reverse触发器,使用PHONE_NUMBER_WITHIN_COUNTRY这个字段的新值来更新PHONE_NUMBER字段。

发布版本

通过回收HR用户对父版本的权限,来发布子版本。

--连接到HR用户,默认使用的还是父版本,查询的数据也是来自于父版本的版本视图
SQL> conn hr/hr
Connected.

SQL> show edition

EDITION
------------------------------
ORA$BASE

SQL> select * from employees where employee_id=101;

EMPLOYEE_ID FIRST_NAME LAST_NAME  EMAIL      PHONE_NUMBER         HIRE_DATE         JOB_ID         SALARY COMMISSION_PCT MANAGER_ID DEPARTMENT_ID
----------- ---------- ---------- ---------- -------------------- ----------------- ---------- ---------- -------------- ---------- -------------
        101 Neena      Kochhar    NKOCHHAR   515.123.4567         20050921 00:00:00 AD_VP           17000                       100            90

--连接到SYS用户,设置数据库的默认版本为子版本,然后退役父版本
SQL> conn / as sysdba
Connected.

SQL> ALTER DATABASE DEFAULT EDITION = post_upgrade;

Database altered.

SQL> select grantee,privilege from dba_tab_privs where table_name='ORA$BASE';

GRANTEE                        PRIVILEGE
------------------------------ ----------------------------------------
PUBLIC                         USE

SQL> REVOKE USE ON EDITION ora$base FROM PUBLIC;

Revoke succeeded.

--再次连接到HR用户,默认使用的就是子版本了,查询的数据也是来自于子版本的版本视图
SQL> conn hr/hr
Connected.

SQL> show edition

EDITION
------------------------------
POST_UPGRADE

SQL> select * from employees where employee_id=101;

EMPLOYEE_ID FIRST_NAME LAST_NAME  EMAIL      PHONE_NUMBER         HIRE_DATE         JOB_ID         SALARY COMMISSION_PCT MANAGER_ID DEPARTMENT_ID PHONE_NUMBER_WITHIN_ COUNT
----------- ---------- ---------- ---------- -------------------- ----------------- ---------- ---------- -------------- ---------- ------------- -------------------- -----
        101 Neena      Kochhar    NKOCHHAR   515.123.4567         20050921 00:00:00 AD_VP           17000                       100            90 515.123.4567         +1

参考:

欢迎关注我的公众号,好好学习,天天向上。