Edition-Based Redefinition, upgrade com zero downtime

Por Tércio Silva Costa Oracle Associate
Publicado em Junho 2019

Revisado por Francisco Riccio




Edition-Based Redefinition(EBR) possibilita que uma aplicação seja atualizada, upgrade, com zero downtime. Isso acontece por que com o EBR, tanto a versão antiga da aplicação, como a nova, podem funcionar simultaneamente. E o melhor de tudo, uma sessão existente pode funcionar na versão antiga enquanto as novas irão funcionar já na nova versão. Assim, quando não existir mais sessões utilizando a versão antiga, podemos remover essa versão da aplicação. Neste artigo, iremos descrever de forma mais simples e resumida. Recomendo a leitura da documentação no link:https://docs.oracle.com/en/database/oracle/oracle-database/12.2/adfns/editions.html

O EBR nos possibilita mudar a estrutura de objetos enquanto uma versão antiga do objeto ainda é utilizada. Uma vez que as alterações foram finalizadas e estão corretas, podemos alterar a versão a ser utilizada. Podemos criar cópias dos objetos em questão, criando assim versões diferentes do mesmo objeto, mas que apenas uma versão será visível ao usuário, de maneira totalmente transparente.

Os seguintes objetos podem ser editionables:

  • SYNOYM
  • VIEW
  • SQL TRANSLATION PROFILE
  • FUNCTION
  • LIBRARY
  • PACKAGE, PACKAGE BODY
  • PROCEDURE
  • TRIGGER
  • TYPE, TYPE BODY


Todo ambiente Oracle possui um único, e default, edition, a versão ORA$BASE.

Para alterar um schema para que ele possa suportar EBR, o seguinte comando tem que ser executado:

ALTER USER user ENABLE EDITIONS [ FOR type [, type ]... ] [FORCE]



Lembrando que o comando assim é irreversível. A lista de objects types do FOR é quais tipos de objetos serão editionables, ou poderá ter versões diferentes. Caso seja omitida, os objetos editionables serão os que estão na view DBA_EDITIONED_TYPES. Já o FORCE deverá ser utilizado quando um objeto EDITIONED depender de um objeto noneditioned.

Vamos então habilitar o EDITIONS para o usuário terciocosta. Os únicos objetos habilitados foram VIEWs e TRIGGERs, pois apenas estes serão utilizados no meu exemplo.

$ sqlplus / as sysdba

SQL*Plus: Release 12.2.0.1.0 Production on Tue Mar 5 00:11:00 2019

Copyright (c) 1982, 2016, Oracle.  All rights reserved.

Connected to:
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production

SQL> ALTER USER terciocosta ENABLE EDITIONS FOR VIEW,TRIGGER;

User altered.



Vamos agora criar uma tabela, que será a base do nosso teste e que seria utilizada na aplicação.

SQL> CREATE TABLE terciocosta.funcionario AS
  2  SELECT employee_id,
  3         first_name,
  4         last_name
  5  FROM   hr.employees;

Table created.
[/code]


Vamos agora criar a edição nova.

[code language="sql"]
SQL> COLUMN EDITION_NAME A10
SQL> COLUMN EDITION_NAME FORMAT a10
SQL> COLUMN PARENT_EDITION_NAME FORMAT a10
SQL> COLUMN USA FORMAT a3
SQL> SELECT * FROM DBA_EDITIONS;

EDITION_NA PARENT_EDI USA
---------- ---------- ---
ORA$BASE              YES

SQL> CREATE EDITION e2;

Edition created.

SQL> SELECT * FROM DBA_EDITIONS;

EDITION_NA PARENT_EDI USA
---------- ---------- ---
ORA$BASE              YES
E2         ORA$BASE   YES

SQL> GRANT USE ON EDITION e2 TO terciocosta;

Grant succeeded.



O comando CREATE EDITION tem uma cláusula opcional que é o CHILD OF, que informa quem vai ser o parent edition da edition sendo criada. Caso seja omitida, como foi o caso, a parent edition foi a current edition, no caso a ORA$BASE.

Imagine agora que queremos fazer uma atualização na nossa aplicação, e sendo assim, é necessário alterar a estrutura da tabela, como adicionando colunas e/ou renomeando? Bem, sabemos que tabelas não são objetos editionable. Então, como devemos fazer?

Devemos trabalhar com editionable views. Podemos renomear a tabela para outro nome, e criar uma editionable view com o mesmo nome anterior da tabela. Assim tudo será através da view e não mais através da tabela, como insert, select e etc.

Vejamos como seria este processo.

SQL> SELECT SYS_CONTEXT('USERENV','CURRENT_EDITION_NAME') AS CURRENT_EDITION FROM DUAL;

CURRENT_EDITION
-----------------------------------------------------------------------
ORA$BASE

SQL> ALTER TABLE funcionario RENAME TO funcionario_table;

Table altered.

SQL> CREATE OR REPLACE EDITIONING VIEW terciocosta.funcionario AS
  2  SELECT employee_id,
  3         first_name,
  4         last_name
  5  FROM   terciocosta.funcionario_table;

View created.

ALTER SESSION SET EDITION = e2;

Session altered.

SQL> ALTER TABLE terciocosta.funcionario_table ADD(nome VARCHAR2(80));

Table altered.

SQL> CREATE OR REPLACE EDITIONING VIEW terciocosta.funcionario AS
  2  SELECT employee_id,
  3         nome
  4  FROM   terciocosta.funcionario_table;

View created.

SQL> COLUMN OBJECT_NAME FORMAT a11
SQL> COLUMN OBJECT_TYPE FORMAT A4;
SQL> column OBJECT_TYPE FORMAT a8;
SQL> SELECT object_name, object_type, edition_name FROM   
user_objects_ae WHERE edition_name IS NOT NULL;

OBJECT_NAME OBJECT_T EDITION_NAME
----------- -------- --------------------------------------------------
FUNCIONARIO VIEW     ORA$BASE
FUNCIONARIO VIEW     E2

SQL> DESC FUNCIONARIO
 Name                                      Null?    Type
 ----------------------------------------- -------- ------------------------
 EMPLOYEE_ID                                        NUMBER(6)
 NOME                                               VARCHAR2(80)

SQL> ALTER SESSION SET EDITION=ORA$BASE;

Session altered.

SQL> DESC funcionario
 Name                                      Null?    Type
 ----------------------------------------- -------- ------------------------
 EMPLOYEE_ID                                        NUMBER(6)
 FIRST_NAME                                         VARCHAR2(20)
 LAST_NAME                                 NOT NULL VARCHAR2(25)



Agora que já temos as duas views, cada uma em uma edition diferente, temos duas versões da nossa aplicação. Mas, ao fazer um insert ou update por exemplo, em uma versão, isso não irá refletir na outra versão. Os dados vão estar lá, em ambas as versões, mas não em todas as colunas, apenas nas colunas daquela versão em que aconteceu o evento.

Para se trabalhar com duas versões ao mesmo tempo e resolver o problema descrito anteriormente, temos a nossa disposição o crossedition trigger. Temos duas versões, o forward crossedition triggers que move os dados de uma versão antiga para a mais nova e o reverse crossedition triggers que faz o caminho contrário.
Existe algumas diferenças entre views editionables e não editionables. Ao contrário das views não editionable, em que podemos criar triggers apenas do tipo INSTED OF, em editionable views podemos criar triggers como se fosse uma tabela normal. As triggers em views editionables também podem ser ordenadas, a ordem de disparo, com outras triggers de outras tabelas. E essas triggers geralmente são temporárias, apenas até finalizar o upgrade de toda a aplicação.

Vamos agora criar dois editionable triggers, um foward e outro reverse para que as duas versões da aplicação possa funcionar ao mesmo tempo e assim ter um zero downtime de upgrade, hot rollover.

SQL> SELECT SYS_CONTEXT('USERENV','CURRENT_EDITION_NAME') AS CURRENT_EDITION FROM DUAL;

CURRENT_EDITION
--------------------------------------------------------------------------------
E2

SQL> CREATE OR REPLACE EDITIONABLE TRIGGER tr_fw_funcionario
  2  BEFORE INSERT OR UPDATE ON funcionario_table
  3  FOR EACH ROW
  4  FORWARD CROSSEDITION
  5  DISABLE
  6  BEGIN
  7    :new.nome := :new.first_name || ' ' || :new.last_name;
  8  END;
  9  /

Trigger created.

SQL> CREATE OR REPLACE TRIGGER terciocosta.tr_rv_funcionario
  2  BEFORE INSERT OR UPDATE ON terciocosta.funcionario_table
  3  FOR EACH ROW
  4  REVERSE CROSSEDITION
  5  DISABLE
  6  BEGIN
  7    :NEW.first_name := SUBSTR(:NEW.nome, 1, INSTR(:NEW.nome, ' ')-1);
  8    :NEW.last_name  := SUBSTR(:NEW.nome, INSTR(:NEW.nome, ' ')+1);
  9  END;
 10  /

Trigger created.

SQL> ALTER TRIGGER terciocosta.tr_rv_funcionario ENABLE;

Trigger altered.

SQL> ALTER TRIGGER terciocosta.tr_fw_funcionario ENABLE;

Trigger altered.



Vamos analisar o que foi feito logo acima. Criamos as duas triggers, tanto forward quanto a reverse, na nova versão da aplicação, a versão E2. Perceba que as duas triggers foram criadas DISABLE. Isso é uma boa prática, pois se a trigger compilasse com erros não iria gerar impacto na aplicação. Logo após habilitamos as duas triggers visto que ambas foram compiladas com sucesso.

Outra boa prática ao se trabalhar com crossedition triggers é aplicar a transformação aos dados já existentes, mas antes devemos verificar se existe alguma transação ainda não finalizada na tabela. Podemos fazer isso com o ajuda da function WAIT_ON_PENDING_DML da package DBMS_UTILITY.

SQL> DECLARE
  2    scn              NUMBER  := NULL;
  3    timeout CONSTANT INTEGER := NULL;
  4  BEGIN
  5    IF NOT DBMS_UTILITY.WAIT_ON_PENDING_DML(Tables  => 'TERCIOCOSTA.FUNCIONARIO_TABLE',
  6                                            timeout => timeout,
  7                                            scn     => scn)
  8      THEN
  9        RAISE_APPLICATION_ERROR(-20000,
 10      'Wait_On_Pending_DML() timed out. CETs were enabled before SCN: '||SCN);
 11    END IF;
 12  END;



Logo após finalizar o bloco acima, vamos aplicar a transformação aos dados já existentes.

SQL> SELECT COUNT(*) FROM funcionario_table WHERE nome IS NULL;

  COUNT(*)
----------
       107

SQL> SQL> DECLARE
  2    c NUMBER := DBMS_SQL.OPEN_CURSOR();
  3    x NUMBER;
  4  BEGIN
  5    DBMS_SQL.PARSE(
  6      c                          => c,
  7      Language_Flag              => DBMS_SQL.NATIVE,
  8      Statement                  => 'UPDATE terciocosta.funcionario_table SET 
employee_id = employee_id',
  9      Apply_Crossedition_Trigger => 'TR_FW_FUNCIONARIO'
 10    );
 11    x := DBMS_SQL.EXECUTE(c);
 12    DBMS_SQL.CLOSE_CURSOR(c);
 13    COMMIT;
 14  END;
 15  /

PL/SQL procedure successfully completed.

SQL> SELECT COUNT(*) FROM funcionario_table WHERE nome IS NULL;

  COUNT(*)
----------
         0



Com o COUNT logo no início, verificamos que existe 107 linhas na versão anterior da nossa aplicação, pre-upgrade. Com a ajuda do DBMS_SQL.PARSE com o parâmetro Apply_Crossedition_Trigger, uma das únicas maneiras de se trabalhar com a current_version diferente da session_version, aplicamos a transformação as linhas existentes antes da versão nova. Após isso, é verificado que não existe mais linhas na versão antiga.
Essa não é a única maneira de se transformar os dados para a nova versão, recomendo ler a documentação para ver outras opções, como o subprograma DBMS_PARALLEL_EXECUTE e a procedure DBMS_EDITIONS_UTILITIES.SET_NULL_COLUMN_VALUES_TO_EXPR.

Após finalizar todo o trabalho de criar a nova versão, criar os novos objetos na nova versão, criar os objetos temporários e transformar os dados já existentes, é hora então de disponibilizar a nova versão para os usuários e colocar a nova versão como DEFAULT. Para finalizar, pode-se remover os objetos temporários, crossedition triggers, e aposentar a antiga versão.

SQL> GRANT USE ON EDITION E2 TO PUBLIC;

Grant succeeded.

SQL> ALTER DATABASE DEFAULT EDITION = E2;

Database altered.

SQL> DROP TRIGGER terciocosta.tr_rv_funcionario;

Trigger dropped.

SQL> DROP TRIGGER terciocosta.tr_fw_funcionario;

Trigger dropped.

SQL> DROP EDITION ORA$BASE CASCADE;

Edition dropped.



Antes de executar um DROP na edition, devemos dar um REVOKE do privilégio USE de cada GRANTEE. E isso só poderá ser feito se não existir mais sessões utilizando a versão antiga.

Bem, essa foi uma introdução ao Oracle EBR. Espero que tenham notado o grande poder dessa feature do Oracle, e que planejem utilizar ela ao fazer um upgrade de aplicação com zero downtime.




Tércio Costa Formado em Ciências da Computação pela UFPB com experiência em Servidores Windows Server e Linux e banco de dados Oracle desde 2008 juntamente com os seus serviços. Desenvolvimento de Sistemas em Java SE com banco de dados Oracle e MySQL. Certificado Oracle Certified SQL Expert, mantendo o blog https://oraclepress.wordpress.com/ reconhecido pela OTN e articulista no portal http://www.profissionaloracle.com.br/gpo

Este artigo foi revisto pela equipe de produtos Oracle e está em conformidade com as normas e práticas para o uso de produtos Oracle.