BuiltWithNOF
Oracle Design (1)

O caso da unicidade seletiva

Oracle é um grande banco de dados não só pela sua robustez e escalabilidade mas também pelos recursos de projeto físico que oferece.

Vejamos, por exemplo, o caso de certa aplicação que tudo o que precisava era de um repositório de dados para guardar os grupos e as tarefas associadas a cada grupo, bem como o status de cada projeto.

Após algum tempo de estudo das regras de negócio, o projetista de banco de dados propôs a seguinte tabela:

SQL> create table PROJECT
  2  (PROJECT_ID number primary key,
  3  TEAMID number,
  4  JOB varchar2(100),
  5  STATUS varchar2(20) check (status in ('ACTIVE', 'INACTIVE')));

A proposta cobria praticamente todas as regras de negócio, a saber:

- em um dado projeto há apenas um time trabalhando em um determinado job;

- um time pode trabalhar em diferentes projetos e jobs ao mesmo tempo

- projetos estao necessariamente em um de dois status: ativos or inativos

Porém , o que esta proposta ainda carecia é atender a uma outra regra de negócio:

- o job tem que ser único dentro do mesmo time para projetos ativos.

Em outras palavras: TEAMID e JOB tem que ser unique key quando STATUS=ACTIVE.

Como implementar esta regra de negócio?

Buenas, para ser justo é preciso dizer que certamente a regra de negócio poderia ser implementada na aplicação. Mas, neste caso, toda e qualquer aplicação teria que construir a mesma implementação de novo. Isto não é raro de acontecer. Casualmente, no momento em que escrevo este artigo trabalho em um projeto onde o mesmo banco de dados Oracle é acessado por aplicações Java, ASP e Oracle Forms!

Focando então no banco de dados de novo, temos a seguintes alternativas:

Database triggers

Database triggers são recursos disponíveis em Oracle desde há muito tempo. São ferramentas absolutamente úteis, desde que o projetista atente ao potencial problema de performance e ao infame problema das “mutating tables”.

O potencial problema de performance advém do fato de que Oracle triggers não são compilados. Isto significa, em especial, que todo e qualquer acesso ao banco de dados efetuado por um database trigger passa pela fase de “hard parse”. Dependendo dos requisitos de performance e escalabilidade da aplicação, isto pode ser um real fator inibidor de uso do recurso. Uma conhecida técnica para driblar este problema de performance é criar uma stored procedure com o código que se deseja rodar e colocar no corpo do trigger apenas a chamada à stored procedure. Como stored procedures são compiladas, a escalabilidade deste design é muito superior.

O problema de “mutanting table” é um velho conhecido de que tem alguma experiência com Oracle. Basicamente, se uma tabela está no meio de uma transação, não é possível codificar um database trigger que lê a mesma tabela durante a transação. Existem recursos de código para driblar esta restrição mas, francamente, são bastante deselegantes e adicionam indesejada complexibilidade à manutenção da aplicação.

Outra desvantagem deste design é que requer a construção de código para todas as operações DML (insert, delete, update).

Instead-of triggers

Apesar de que atualmente em IT basta existir para se ser declarado velho, este é um recurso relativamente novo em Oracle. Trata-se de uma ferramenta que permite ao projetista de banco de dados desenhar um mecanismo para capturar uma solicitação de um comando DML efetuada pela aplicação e definir que ações vão realmente acontecer no banco de dados.

Quando combinado com views é definitivamente um recurso que se deseja prestar atenção.

Por exemplo, no caso deste artigo, o seguinte design poderia ser implementado:

- uma view - PROJECT_VIEW - é criada com base na table PROJECT

SQL> create view project_view as select * from project;

- um ou mais instead-of triggers são associados à view. Estes triggers capturam os comandos DML. No caso de um insert, o trigger primeiro acessa a tabela PROJECT para verificar se o time já está associado ao job em um outro projeto ativo. Se não é o caso, então o trigger dispara um DML de insert na tabela PROJECT. Um exemplo de como este trigger poderia ser criado é fornecido abaixo:

create or replace trigger team_job_uniqueness
instead of insert on PROJECT_VIEW
for each row
declare
  v_count number;
begin
  if (:new.status = 'ACTIVE') then
   begin   
     select count(*)
     into v_count
     from project
     where teamid = :new.teamid
     and job = :new.job
     and status = 'ACTIVE';
     if (v_count > 0) then
       raise_application_error(-20001, 'Unicidade seletiva nao respeitada');
     end if;
   exception
     when others then
       raise_application_error(-20002, 'Problemas no acesso a tabela PROJETO');
   end;
  end if;
  begin
   insert into project(project_id, teamid, job, status) values (:new.project_id, :new.teamid, :new.job, :new.status);
  exception
   when others then
       raise_application_error(-20003, 'Problemas para criar novo PROJETO');
  end;
end;

- à aplicação são dados privilégios de acesso à view PROJECT_VIEW mas não à tabela PROJECT.

Este design evita o enervante problema das “mutanting tables” de uma maneira mais elegante mas ainda assim é bastante verborrágica, cheia de código para se manter.

Function-based indexes (FBIs)

A vida devia ser simples e fácil. É por isto que Oracle implementou - desde a versão 8i (8.1) - a noção de índices com base em expressões/funções criadas pelo projetista/desenvolvedor.

FBIs permitem, por exemplo, a criação de índices como o apresentado abaixo:

SQL> CREATE UNIQUE INDEX
  2  JOB_UNIQUE_IN_TEAMID ON PROJECT
  3  (CASE WHEN STATUS = 'ACTIVE' THEN TEAMID ELSE NULL END,
  4  CASE WHEN STATUS = 'ACTIVE' THEN JOB ELSE NULL END);

O resultado da existência de um índice assim criado em nosso estudo de caso é que

- apenas para projetos ativos serão criadas entradas no índice

- o fato de que o índice é UNIQUE garante que um time jamais trabalhará em um mesmo job ao mesmo tempo (para projetos ativos).

É simples de implementar e de manter, tem mínimo impacto em aplicações OLTP, é enxuto (pois existirão entradas neste índice apenas projetos ativos), é furiosamente rápido para recuperar a informação, não requer extra select para garantir a regra de negócio e é um mecanismo mantido e gerenciado pelo núcleo do banco de dados (já ví calejados projetistas de banco de dados chorando ao serem apresentados à esta técnica de design).

Conclusão

Não há nenhuma grande novidade a se descobrir aqui. O que vale é a velha regra de que para criar o projeto físico de um banco de dados a melhor opção é conhecer os recursos existente no banco de dados em questão. Eles variam imensamente de um bd para outro e entre versões do mesmo bd.

Claro que se portabilidade de bd é um requisito importante esta conclusão deve ser repensada. Agora... quantas vêzes realmente isto acontece? Já perdi o número de vezes em que ví projeto de desenvolvimento de aplicações VB em Windows se recusando a usar recursos específicos de Oracle devido a uma portabilidade que ninguém pediu. VB é proprietário, Windows é proprietário mas... o bd tinha que ser portável...


Este artigo é uma extensão do um estudo de caso apresentado por Thomas Kyte em seu excelente livro “Effective Oracle by Design”. São mais de 600 páginas de valiosos conselhos mas o capítulo Effective Schema Design por si só já vale o investimento.

[Home] [IT] [Oracle/Db] [SD] [PM] [Lendas Corporativas] [Diversas] [The Secret of Life] [About IT talks]