Gerenciando chaves primárias em um ambiente de multi-tenacidade

Há alguns anos venho acompanhando algumas discussões (aquelas de sempre) sobre a melhor forma de gerir registros de bancos de dados relacionados (o famoso mestre/detalhe). A maioria absoluta dos desenvolvedores opta por chaves artificiais simples obtidas por sequenciadores, enquanto outros adotam uma forma mais espartana (e falível) de gestão destas chaves, a qual ilustrarei mais adiante. Um pequeno grupo, restrito ao mundo dos bancos de dados mais escalares e poderosos, adota chaves naturais. Mas há um pequeníssimo grupo, extremamente restrito, que faz uso de um modelo mais inteligente de controle de suas chaves. Este modelo, apesar de parecer complexo, mostra-se infinitamente mais eficiente que os demais modelos de gestão de chaves pela sua principal característica: a unicidade absoluta da chave. Em ambientes de multi-tenacidade, independentemente da arquitetura, o maior desafio de todos é manter o controle sobre as chaves primárias de todas as tabela envolvidas no sistema, e isso só conseguiremos se estabelecermos uma boa política para tal. Como este é um artigo – essencialmente – sobre chaves primárias, as demonstrações se referem a uma arquitetura de banco de dados único, a qual centraliza os dados de todos os tenants e facilita a manutenção. Em um momento futuro, discutiremos outras arquiteturas para ambientes de multi-tenacidade e suas vantagens e desvantagens.

No transcorrer deste artigo descreverei (e ilustrarei) estes modelos de gestão de chaves primárias, apontando seus pontos fortes e, principalmente, suas falhas, afim de trazê-los um novo olhar sobre o que se sabia e o que muitos ainda não sabem sobre este assunto.

O que é uma chave primária?

Chaves primárias são, em suma, índices únicos atribuídos aos registros em uma tabela do banco de dados para o efetivo controle destes registros e seus relacionamentos. O valor da chave primária de um registro não pode, em nenhuma circunstância, ser igual ao valor da chave primária de outro registro na mesma tabela, pois isto invalidaria sua unicidade (característica daquele que é único). Com isso, outro conceito – a integridade referencial – deixaria de existir, já que a resposta a uma consulta relativa ao valor de uma chave primária retornaria mais de um resultado.

Tendo em vista o que é uma chave primária, podemos dividi-la em 2 grupos: chaves naturais e chaves artificiais. Chaves naturais se valem de valores que devem ser, naturalmente, únicos para cada registro. Como exemplo, tomemos os números de CPF (Cadastro de Pessoa Física) ou CNPJ (Cadastro Nacional de Pessoa Jurídica). Ao cadastrarmos em uma base de dados uma pessoa, seja ela física ou jurídica, pressupomos que seu número de cadastro junto à Receita Federal seja único, viabilizando seu uso como chave primária. De igual forma, quando precisamos cadastrar um veículo, os números e letras em suas placas, os quais devem corresponder a um único veículo registrado em solo brasileiro, são sérios candidatos a tornarem-se chaves primárias. Já chaves artificiais são aquelas em que este identificador único não tem qualquer relação com o registro a ser inserido ou manipulado, servindo apenas para sua identificação.

Cada um destes grupos pode ser subdividido em outros 2 grupos: chaves simples e chaves compostas. O primeiro grupo refere-se às chaves que utilizam apenas um campo (ou propriedade) de uma tabela (ou entidade) para concretizar este índice. Já o segundo grupo refere-se às chaves que incorporam mais de um campo em seu índice, podendo, quaisquer dos campos (ou até mesmo todos), ser uma chave estrangeira dentro de uma tabela.

As chaves naturais

Sabendo o que são chaves primárias naturais fica mais fácil compreender seu uso. Como ilustrado acima, os números de CPF e CNPJ caberiam como chaves primárias em qualquer tabela de cadastro de pessoas. Seu uso foi largamente propagado quando os requisitos de espaço para armazenamento e desempenho dos sistemas de bancos de dados começaram a se tornar críticos, pois estes campos, atuando como chaves, eliminam a necessidade de novos campos para este fim, reduzindo o espaço de armazenamento e, por conseguinte, melhorando o desempenho do sistema.

Contudo, há um problema. Dependendo da modelagem de dados elaborada, certos registros não conterão identificadores únicos naturais em um único campo, demandando a composição da chave. Um exemplo banal e, por isso mesmo, de fácil compreensão, é uma tabela que armazena registros de pneus. Sabendo que vários fabricantes produzem pneus ao redor do mundo e que estes têm características semelhantes entre si, é impossível determinar uma única característica que o distingua dentre todos os outros, já que o fabricante Goodyear produz um pneu radial para aros de 14 polegadas, assim como a Pirelli e a Firestone, com banda (largura ou bitola) de 185 mm, altura de 70% em relação à banda, carga máxima de 475 kg e velocidade máxima de rodagem de 190 km/h. Assim, tanto Goodyear quanto Pirelli e Firestone fabricam ao menos um modelo de pneu 185/70R14 82T, sendo que qualquer um deles pode fabricar mais de um modelo com estas mesmas características. Portanto, para podermos criar uma chave natural para registros assim deveremos levar em conta uma composição de chave que contemple os campos Fabricante, Banda, Altura, Carga Máxima, Velocidade Máxima e Nome do Modelo, o que implica, em caso de postergação da chave como chave estrangeira em um relacionamento entre tabelas, como formação de preços ou controle de estoque, na utilização de todos os campos da chave primária em uma tabela relacionada. Com isso, além de uma complexidade absurda do modelo, ainda teremos uma sensível queda de desempenho, já que o conteúdo das chaves primárias é “replicado” em sua postergação.

Temos, ainda, outro problema. Ao utilizarmos um número de CNPJ para cadastro de uma pessoa jurídica, este valor pode vir a não ser único dentro de sua base de dados. Como exemplo, tomemos nossos colégios públicos estaduais. Quando registramos em uma base de dados um colégio qualquer, lembrando que o número de CNPJ deste colégio deva ser único e por isso este número foi escolhido como chave primária, nos depararemos com a multiplicidade deste número, já que várias unidades da federação, se não todas, utilizam o número de CNPJ de sua secretaria de estado da educação para a identificação de seus colégios estaduais junto ao estado, tribunal de contas e fornecedores, inviabilizando o uso deste número como chave primária, já que ele não é tão único assim e, para ambientes de multi-tenacidade, esse tipo de chave primária tornaria o controle dos tenants impossível.

As chaves artificiais

Tendo como base os problemas obtidos ao usarmos chaves naturais para controlar nossos registros, podemos, então, partir para o segundo tipo de chaves primárias: as chaves artificiais. Por não terem qualquer relação com o registro gerido, chaves artificiais podem conter qualquer valor, desde que se mantenham únicos dentro de uma mesma tabela. Apenas por esta razão, a adoção de chaves artificiais justifica-se quando comparada à adoção de chaves naturais. E é sobre esta forma de controle de chaves primárias que trataremos a seguir, descrevendo duas de suas formas mais comuns e apresentando uma alternativa a estas formas. E como, para ambientes de multi-tenacidade, já descartamos o uso de chaves primárias naturais, me dedicarei à demonstração apenas das chaves primárias artificiais.

Sequenciadores

A forma mais prática, simples e rápida de obter-se uma chave primária e valer-se do uso de sequenciadores. Sequenciadores são entidades criadas em um banco de dados afim de controlarmos a atribuição de valores às nossas chaves primárias. Como os sequenciadores trabalham “à margem da lei” (não são controlados em contextos transacionais), uma vez obtido o índice do sequenciador, tendo a operação que faz uso deste índice sido validada ou não por um contexto transacional, ele nunca voltará a se repetir, ao menos não sozinho. Existem implementações e nomes diversos para estes sequenciadores, variando conforme o sistema de banco de dados adotado. Bancos de dados como o MySQL, SQLite e MS SQL Server utilizam sequenciadores de auto incremento, sendo chamados de AUTOINCREMENT pelos dois primeiros e IDENTITY pelo terceiro. Já bancos de dados como o PostgreSQL e o Oracle utilizam sequenciadores “manuais”, chamados SEQUENCES, e bancos de dados como Interbase e Firebird utilizam, assim como PostgreSQL e Oracle, sequenciadores manuais, mas dão a eles o nome de GENERATORS. Devido ao fato de que eu sou um profissional extremamente ligado à tecnologias livres, minhas ilustrações ficarão restritas aos bancos de dados de utilização livre, de todo ou em parte, PostgreSQL, MySQL, SQLite e Firebird.

Como o MySQL, assim como o SQLite, se valem de um sequenciador por auto incremento, não é necessário ilustrar seu uso já que eles não permitem a atribuição direta de valores a campo de auto incremento. Mas como os bancos de dados PostgreSQL e Firebird instituem sequenciadores manuais, segue um pequeno exemplo de sua criação:

-- Criação de uma sequence em um banco de dados PostgreSQL
create sequence nome_da_sequence;
-- Criação de um generator em um banco de dados Firebird
create generator nome_do_generator;
set generator nome_do_generator to 0;
commit;

Nota: mesmo que, para adequação, a equipe responsável pelo Firebird tenha criado um dispositivo chamado SEQUENCE, este nada mais é que um apelido para GENERATOR.

Para selecionarmos manualmente os valores dos sequenciadores, utilizamos os seguintes comandos SQL:

-- Selecionando um valor através de uma sequence no PostgreSQL
select nextval('nome_da_sequence');
-- Selecionando um valor através de um generator no Firebird
select gen_id(nome_do_generator, 1)
from RDB$DATABASE;

Caso queiramos um comportamento similar aos campos AUTOINCREMENT em um banco de dados PostgreSQL, basta atribuirmos ao valor padrão do campo que seja a chave primária a expressão nextval(‘nome_da_sequence’). Já para o Firebird, isto não será possível sem o uso de um gatilho (TRIGGER) que faça esta atribuição. Exemplo:

-- Criação de um trigger para auto incremento em um banco de dados Firebird
set term !! ;
CREATE TRIGGER nome_do_trigger_bi FOR nome_da_tabela
ACTIVE BEFORE INSERT POSITION 0
AS
BEGIN
  if (NEW.ID is NULL) then NEW.ID = GEN_ID(nome_do_generator, 1);
END!!
set term ; !!

Até aqui é tudo muito simples e, principalmente, tudo muito eficaz. Sequenciadores são, realmente, uma mão na roda quando precisamos controlar as chaves primárias de uma tabela. Porém, tudo começa a se complicar quando precisamos controlar, além da chave primária de um registro, sua postergação em um registro relacionado dentro de um mesmo contexto transacional. Quando usamos PostgreSQL, desde que o valor padrão do campo não tenha sido usado para incremento automático através da SEQUENCE, ou Firebird, quando precisamos reservar um identificador único para o registro mestre e postergá-lo em registros detalhe, basta selecionarmos este identificador manualmente como visto anteriormente e atribui-lo manualmente às instruções DML que o utilizarão. Mas, se ao invés destes estivermos utilizando bancos de dados como MySQL, SQLite ou até mesmo o PostgreSQL com atribuição automática de valores obtidos através de SEQUENCES, a situação fica um pouco mais complicada. O PostgreSQL conta com uma cláusula de retorno chamada RETURNING, a qual pode ser utilizada para retornar um ou mais campos inseridos ou alterados por uma instrução DML. Exemplo:

-- Exemplo de utilização da cláusula RETURNING no PostgreSQL
insert into nome_da_tabela (campo_id
                          , campo_nome
                          , campo_descricao)
                    values (nextval('nome_da_sequence')
                          , 'Nome'
                          , 'Descricao')
returning campo_id;

Já com o MySQL e o SQLite a operação não é tão simples. Para obtermos estes valores seremos obrigados a realizar uma nova instrução SQL, a qual nos retornará o valor utilizado para a posterior postergação. Exemplo:

-- Obtendo o último ID de auto incremento inserido no MySQL
-- Nota: esta instrução deve ser executada imediatamente após a instrução INSERT
insert into nome_da_tabela (campo_nome
                          , campo_descricao)
                    values ('Nome'
                          , 'Descricao');

select LAST_INSERT_ID();
-- Obtendo o último ID de auto incremento inserido no SQLite
-- Nota: esta instrução deve ser executada preferencialmente após a instrução INSERT
insert into nome_da_tabela (campo_nome
                          , campo_descricao)
                    values ('Nome'
                          , 'Descricao');

select max(campo_id)
  from nome_da_tabela;

Mesmo assim, ainda conseguimos controlar muito bem nossas chaves primárias e sua postergação e estamos a salvo do risco de duplicarmos nossas chaves primárias. Se aplicarmos esse modelo a um ambiente de multi-tenacidade, logicamente não teremos problemas ao gerir tenants múltiplos em arquiteturas de múltiplos bancos de dados ou servidores decentralizados, mas, como informei lá no início do texto, estamos tratando de ambientes com um único banco de dados servindo todos os tenants, portanto, sim, teremos problemas com este modelo também.

E, a seguir, demonstrarei, como mencionado na introdução deste artigo, um método espartano de controle de chaves primárias.

 

This is Sparta! - LT Digital Labs

This is Sparta!

Em uma alusão à história, mais especificamente à história grega, veremos que entre nós existem alguns espartanos (gentílico de Esparta, mas que também pode referir-se a alguém bravo, corajoso, minimalista e nem sempre eficaz). Em uma recente discussão sobre tratamentos de chaves para controles de registros mestre/detalhe, um colega de profissão confessou usar, em bancos de dados que utilizam-se de sequenciadores para controles de chaves primárias, o seguinte dispositivo:

select max(campo_id) + 1
  from nome_da_tabela;

Esta não é uma solução inválida, já que você receberá do banco de dados o maior valor de chave primária utilizado em uma tabela, acrescido em 1, para que esta seja sua nova chave primária. Isto, com certeza, funcionará, desde que você não tenha qualquer concorrência na geração deste novo valor. Imaginemos um cenário em que 3 usuários decidem efetuar o registro de um novo pedido de vendas em sua base de dados simultaneamente. Assim, com a solução proposta acima, todos os 3 usuários receberão o mesmo identificador para seu novo pedido e isto significa que apenas um deles conseguirá efetivar seu pedido dentro do sistema, deixando os demais muito descontentes. Como vimos anteriormente, os valores de chaves primárias não podem ser duplicados, sendo assim, quem dentre os 3 usuários for o primeiro a concluir e registrar seu pedido obterá êxito, pois seu identificador ainda não foi utilizado. Já quando qualquer um dos dois usuários restantes, ou até mesmo os dois, for registrar seu pedido, receberá uma mensagem de exceção dizendo que houve uma violação por duplicação de valores na chave primária, fazendo com que estes não fiquem tão felizes quanto o primeiro usuário. Agora, imaginem isso em um ambiente de multi-tenacidade, onde inúmeros clientes estarão conectados à uma única instância do mesmo sistema!

Sendo assim, podemos abolir este método de controle de chaves primárias, já que por si só ele é falível e serve apenas como ilustração. Portanto, poderíamos considerar o uso de sequenciadores um método de controle de chaves infalível? Ainda não.

O quão falível é algo infalível

Por mais que a ideia da infalibilidade do uso de sequenciadores inunde o imaginário popular (na verdade, não tão popular assim, pois esta ideia está restrita aos desenvolvedores e administradores de bancos de dados), o uso de sequenciadores pode nos garantir, em cenários diversos, várias dores de cabeça. Imaginemos um mundo hipotético em que nosso sistema, aplicativo, programa, etc., seja utilizado por dois dos maiores concorrentes de um setor específico e um destes concorrentes, mais rico e poderoso, decide absorver o outro para a aquisição de sua carteira de clientes. Nosso sistema opera seu controle de chaves primárias exclusivamente sobre o uso de sequenciadores e isto tem funcionado muito bem. Porém, durante o processo de aquisição, verificou-se a necessidade de unificarmos as bases de dados, tornando-as apenas uma. Este seria um processo tranquilo, afinal, ambas as empresas utilizam a mesma estrutura de dados. Seria…

Em uma ilustração rápida, tomemos como base os registros de clientes. Para a empresa A, o cliente 1458 será o Senhor X, sendo que para a empresa B o cliente 1458 será a Senhora H. Ao tentarmos conciliar as bases de dados das empresas A e B, por mais que a estrutura dos dados seja idêntica, não poderemos apenas exportar os dados de uma base de dados e importá-la na outra, pois correremos um grande e plausível risco de nos confrontarmos com a duplicação de valores para as chaves estrangeiras. Muitos dirão: “podemos simplesmente adicionar à chave primária dos registros pertencentes à empresa B um valor qualquer, como o maior valor de chave primária pertencente à empresa A, por exemplo, e o problema estará resolvido.” Realmente, estará. Mas, reflitamos um pouco mais. Imaginem que na empresa B há um funcionário com 20 anos de serviços prestados, que sabe de cor todos os códigos de seus clientes. O que acontecerá se mudarmos, da noite para o dia, todos os códigos de cliente que este funcionário está habituado a trabalhar? Até que ele decore todos os códigos destes clientes, o que pode levar mais 20 anos, toda a sua produtividade se perderá. E este é apenas um dos diversos problemas que enfrentaríamos. Então, alguém sugerirá: “podemos adicionar mais um campo à chave primária, identificando quem pertence à empresa A e quem pertence à empresa B.” Tudo bem, é uma solução mais elegante, mas imaginem quanta mão de obra será despendida para a reescrita de toda a aplicação, adequação da base de dados e treinamento de usuários. Poderíamos arcar com este custo? Poderíamos repassá-lo ao cliente? Isso caberá a cada um decidir. Sei que novas ideias surgirão além destas duas mas, acreditem: todas elas serão dispendiosas e gerarão um nível de estresse muito alto.

Abandonemos este cenário e busquemos outro. Imaginem que nossa aplicação está trabalhando tranquilamente com o controle de chaves primárias por sequenciadores e que modelamos nossa estrutura de dados de forma a separar em tabelas distintas Fornecedores, Clientes, Funcionários e Transportadoras, mesmo sabendo que funcionários podem ser clientes, que clientes podem ser fornecedores e assim sucessivamente. Imaginem o que aconteceria se fôssemos obrigados a unificar todos estes cadastros, seja por uma imposição legal ou por uma questão de manutenção evolutiva de nossa aplicação. Sim, teríamos as mesmas dores de cabeça do cenário anterior, e isso sem a necessidade de uma fusão de bases de dados.

Imaginemos um terceiro cenário, onde precisamos de um serviço de replicação e/ou contingência de dados. Quando trabalhamos com bases de dados decentralizadas, sempre seremos direcionados ao uso de replicadores de dados, para que todos as bases de dados fiquem em sintonia. Suponhamos que este ambiente faça sua replicação em lote todos os dias às 03:00h, pois o tráfego de rede é menor, não há usuários utilizando o sistema, etc. Durante a última atualização, a base de dados A recebeu a última chave primária da tabela clientes com valor 5892 e, após esta atualização, realizou o cadastro de 8 novos clientes. Ao mesmo tempo, a base de dados B, que recebeu os mesmos dados que a base A, realizou 16 novos cadastros. Sendo assim, o último valor de chave na base A passou a ser 5900, enquanto na base B passou a ser 5908. Quando as bases passarem pelo processo de replicação, estes valores serão alterados, passando a ser 5916 o último valor de chave primária. Se os clientes desta aplicação estiverem sendo referenciados por tais códigos e estes códigos deverão ser utilizados no futuro, teremos um problema. Se não, tudo bem, mas pensem em toda a mão de obra dispendida para efetuar a replicação destas bases de dados, garantindo a integridade referencial e evitando futuros problemas. E o mesmo vale para sistemas de contingência de dados.

Identificadores únicos salvam vidas

Em um exemplo prático, vou colocar uma situação que ocorreu com um colega, desenvolvedor experiente, que passou a adotar UUIDs como chave primária em uma aplicação. Esta aplicação tem, como característica principal, a multi-tenacidade e, justamente por isso, já nasceu utilizando o conceito abordado aqui.

Durante o desenvolvimento desta aplicação, ele verificou que esta retornava um erro ao tentar atualizar um registro em seu banco de dados. Ao verificar a situação, ele pode constatar que, durante um dos vários processos que a aplicação executa, um dos objetos dentro da aplicação foi sobrescrito, mudando completamente suas propriedades. Como a aplicação em questão é desenvolvida em PHP, os objetos não precisam ser do mesmo tipo para ocupar a mesma variável, o que complica ainda mais o cenário. A aplicação emitiu a mensagem de erro para a atualização, indicando que o identificador do objeto (chave primária no banco de dados) não existia. Caso o sistema utilizasse sequenciadores como chave primária, haveria uma possibilidade gigantesca de que esse erro passasse desapercebido, sendo capturado apenas quando a aplicação estivesse em produção. Mas, como era de se esperar, o erro pode ser verificado no momento em que o processo foi implementado, já que os identificadores dos objetos são únicos e, logicamente, nunca se repetem.

É. Identificadores únicos também podem salvar vidas (e empregos)…

Ampliando os horizontes

Após verificarmos a fragilidade e a falibilidade do uso de chaves primárias através de sequenciadores, imaginem um cenário mais amplo, global. Imaginem um sistema em que vários clientes, de várias empresas, de várias partes do mundo, com uma gama infindável de dispositivos, em um universo imenso de conexões usem nossa aplicação. Como controlaríamos tudo isso? É o que veremos a seguir.

Antes da chuva, a nuvem

Em um mundo cada vez mais conectado e cada vez mais plural, tudo converge para a universalização do acesso à informação. Quando imaginamos o quanto podemos evoluir ao passarmos a utilizar novos vetores tecnológicos, sempre esbarramos naquilo que criamos e somos obrigados a manter da forma como foi concebido, pois a evolução tecnológica torna obsoletos conceitos extremamente arraigados.

A computação na nuvem trouxe consigo uma série de evoluções, não só tecnológicas, mas conceituais, e a principal delas é a multi-tenacidade. Afinal, o que é multi-tenacidade?

Em uma tradução livre do que pode ser encontrado aqui, multi-tenacidade pode ser definida como:

“Multi-tenacidade refere-se a um princípio de arquitetura de software em que uma única instância do software executado em um servidor serve vários inquilinos. Um inquilino é um grupo de usuários que compartilham a mesma parcela do software que utilizam. Com uma arquitetura multi-tenaz, um aplicativo de software é projetado para fornecer a cada inquilino uma parte dedicada da instância, incluindo os seus dados, configuração, gerenciamento de usuários, funcionalidades individuais do inquilino e propriedades não-funcionais. A multi-tenacidade contrasta com arquiteturas de várias instâncias em que as instâncias de software operam separadamente para o atendimento de diferentes inquilinos. Os comentaristas consideram a multi-tenacidade como uma característica importante da computação em nuvem.”

Isso significa dizer que, ao invés de provermos inúmeras instâncias de nossa aplicação para nossos clientes (inquilinos/tenants), forneceríamos apenas um único serviço a todos eles, centralizando e unificando processos. Este é um conceito lindo, concordo. Mas, afinal, o que ele tem a ver com o controle de chaves primárias? É o que veremos a seguir.

Um universo de identificadores únicos

Aqueles que estão mais familiarizados com o mundo Linux ou o uso de interfaces em programação orientada a objetos devem conhecer o conceito de identificadores únicos universais. Estes identificadores, também conhecidos como UUID (Universally Unique IDentifier) ou GUID (Globally Unique IDentifier) são valores gerados a partir de vários parâmetros, como elementos de hardware, data e hora de sua geração, sistema operacional, entre outros. Como seu próprio nome sugere, estes identificadores são universalmente únicos, não sendo estes gerados em duplicidade por nenhum outro equipamento dentro de nosso universo conhecido.

Por sua característica de unicidade universal, tecnologias como o BaaS (Backend as a Service) tem se valido destes identificadores para o controle de dados, principalmente de suas chaves primárias, e é ao estudo desta tecnologia que nos dedicaremos a seguir.

Por que eu deveria utilizar um UUID ao invés de um número?

Simples: porque números se repetem. Como vimos nos cenários propostos anteriormente, o uso de sequenciadores numéricos podem se tornar falíveis a medida em que nossas aplicações vão evoluindo ou se tornando populares. Alguns aqui dirão: “é muito mais fácil lembrar que o código do cliente é 5432 do que de305d54-75b4-431b-adb2-eb6b9e546013.” Concordo plenamente. Mas cabe lembrar o abismo que separa, dentro da teoria de bancos de dados, chaves primárias de chaves de negócio: chave primária é aquela da qual o sistema de banco de dados faz uso para controlar seus registros e chave de negócio é aquela que o usuário de um sistema de banco de dados usa para controlar seus registros. Quando tratamos de uma modelagem dimensional, a chave de negócio da dimensão em uma tabela de clientes, por exemplo, pode ser o CPF ou CNPJ de um cliente, um código arbitrário ou qualquer coisa que o identifique ao usuário, contudo, a chave primária deste registro não precisa ser visível a ele, podendo esta receber qualquer valor, desde que único. A este tipo de chave primária artificial é dado o nome de chave substituta (surrogate key) e ela deve obedecer às seguintes regras:

  • O valor é único para todo o sistema, portanto nunca reutilizado;
  • O valor é gerado pelo sistema;
  • O valor não é manipulável pelo usuário ou aplicação;
  • O valor não contém nenhum significado semântico;
  • O valor não é visível para o usuário ou aplicação (no caso de modelagem relacional);
  • O valor não é composto de vários valores a partir de diferentes domínios.

Ao atribuir a uma chave primária um valor universalmente único, garantimos a inviolabilidade e a infalibilidade do controle de chaves primárias, já que estes valores são únicos e imutáveis. Mas se isso é tão eficiente, por que tão poucos usam este recurso? Simples: porque uma maioria avassaladora de desenvolvedores acredita que seus usuários devem ter acesso às chaves primárias controladas pelo sistema de banco de dados pois não sabem ou ignoram o fato de que, para todos os casos, chaves primárias e chaves de negócio dimensionais são coisas completamente distintas. Como posso implementar isso em meus sistemas?

Os quatro bancos de dados utilizados neste artigo contam com tipos de dados que suportam os octetos binários de um identificador universal, sendo eles:

  • PostgreSQL: uuid
  • MySQL: BINARY(16)
  • SQLite: BLOB(16)
  • Firebird: char(16) character set OCTETS

Caso precisemos gerar o identificador único dentro da aplicação, podemos gerá-lo da seguinte forma:

// Object Pascal
function GenGUIDID: String;
var
  guidID: TGuid;
begin
  CreateGUID(guidID);
  Result := GUIDToString(guidID);
  Result := StringReplace(Result, '{', EmptyStr, [rfReplaceAll]);
  Result := StringReplace(Result, '}', EmptyStr, [rfReplaceAll]);
end;
/* PHP */
function genGUIDID() {
  $uuid = md5(uniqid(rand(), true));
  return sprintf('%08s-%04s-%04s-%04s-%12s', substr($uuid, 0, 8)
                                           , substr($uuid, 8, 4)
                                           , substr($uuid, 12, 4)
                                           , substr($uuid, 16, 4)
                                           , substr($uuid, 20, 12));
}

Para outras linguagens, consulte o manual da própria linguagem para saber como interagir com o tipo UUID.

Isso garantirá à aplicação que ela, no momento da geração do registro mestre/detalhe, já contenha seu valor de chave primária sem a necessidade de consultar o banco de dados para isso ou de aguardar que ele gere esta informação durante o processo de persistência.

Caso seja necessário criar o identificador universal através do banco de dados, os seguintes passos devem ser seguidos:

-- PostgreSQL
-- Cria a extensão necessária para gerar identificadores únicos
-- Mais informações na documentação do PostgreSQL
CREATE EXTENSION 'uuid-ossp';

-- Gera um identificador único
select uuid_generate_v4();
-- MySQL
-- Criar estas funções para manipular os valores binários, quando necessário
CREATE FUNCTION UuidToBin(_uuid BINARY(36))
RETURNS BINARY(16)
LANGUAGE SQL DETERMINISTIC CONTAINS SQL
SQL SECURITY INVOKER
  RETURN UNHEX(CONCAT(SUBSTR(_uuid, 15, 4),
                      SUBSTR(_uuid, 10, 4),
                      SUBSTR(_uuid, 1, 8),
                      SUBSTR(_uuid, 20, 4),
                      SUBSTR(_uuid, 25)));

CREATE FUNCTION UuidFromBin(_bin BINARY(16))
RETURNS BINARY(36)
LANGUAGE SQL DETERMINISTIC CONTAINS SQL
SQL SECURITY INVOKER
  RETURN LCASE(CONCAT_WS('-',
                         HEX(SUBSTR(_bin, 5, 4)),
                         HEX(SUBSTR(_bin, 3, 2)),
                         HEX(SUBSTR(_bin, 1, 2)),
                         HEX(SUBSTR(_bin, 9, 2)),
                         HEX(SUBSTR(_bin, 11))));

-- Para obter um identificador único
select uuid();
-- Firebird
-- Criar procedure que retorna o identificador único
set term !! ;

create procedure get_hex_uuid
returns(real_uuid char(16) character set OCTETS, hex_uuid varchar(32))
AS
  declare variable i integer;
  declare variable c integer;
BEGIN
  real_uuid = GEN_UUID();
  hex_uuid = '';
  i = 0;

  while (i < 16) do
  begin
    c = ascii_val(substring(real_uuid from i+1 for 1));

    if (c < 0) then
      c = 256 + c;
  
    hex_uuid = hex_uuid || substring('0123456789abcdef' from bin_shr(c, 4) + 1 for 1)
                        || substring('0123456789abcdef' from bin_and(c, 15) + 1 for 1);
    i = i + 1;
  end

  suspend;
END !!
-- SQLite
select randomblob(16); -- Identificador único em forma binária
select hex(randomblob(16)); -- Identificador único em forma hexadecimal

Nota: até meus últimos testes com o Firebird 2.5, ele era incapaz de relacionar chaves de integridade referencial em instruções SQL que utilizassem a cláusula JOIN vinculando campos char(16) character set OCTETS como referência, o que dificultava, e muito, a implementação deste recurso neste banco de dados.

Tudo isso é muito perfeito. Existe alguma desvantagem?

Sim. Mínima, mas existe. Valores inteiros são compostos por 64 bits (8 bytes) enquanto identificadores únicos são compostos por 128 bits (16 bytes), ou seja, ocupam o dobro do espaço e da memória de utilização. Contudo, esta desvantagem é irrisória diante dos ganhos que este modelo de controle de chaves nos proporciona. Exemplo? Imagine que seu sistema conta com uma aplicação mobile para gestão de dados. Smartphones e tablets possuem conexão com a internet através de redes sem fio ou internet móvel (2G/3G/4G), e estes podem acessar diretamente seu servidor de aplicações. Contudo, para que se evite o consumo desnecessário de banda e torne a aplicação mais eficiente, cada dispositivo móvel pode gerar e controlar seus próprios identificadores únicos, o que também vem a ser útil em casos de conectividade nula ou limitada.

Por fim, por mais complexo que este conceito pareça, controlar suas chaves primárias através de identificadores únicos acaba trazendo infinitamente mais vantagens do que desvantagens aos nossos sistemas, os quais poderão sofrer quaisquer tipos de alterações sem o risco da quebra de integridade dos dados.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *