Go to English Blog

Usando UUID no PostgreSQL com ActiveRecord

Leia em 3 minutos

O PostgreSQL possui muitos tipos de campos e grande parte deles é mapeado no ActiveRecord. Um desses tipos de campo que venho utilizando cada vez mais é o uuid.

Embora o UUID possua diversas implementações, a versão mais útilizada é de longe o UUIDv4. A versão 4 cria strings randômicas seguindo o formato xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, conforme especificação abaixo:

Veja, por exemplo, alguns identificadores gerados com o comando uuidgen e como eles seguem a especificação mencionada anteriormente.

847862BF-424C-482F-81B1-09A48CFD547F
A378F7E1-94AD-432E-9DB1-41BADDBDAE8F
077570B7-63F5-4FB0-8A11-97BBF6998501
2B99E07E-36D4-4E07-9A6C-8DBEB66DA9BF

No Ruby, você pode gerar uuids usando o método SecureRandom.uuid, presente na standard library.

require 'securerandom'
SecureRandom.uuid
#=> ff039085-5e16-4d74-9039-4225533e1f35

No PostgreSQL, você pode utilizar a extensão uuid-ossp. Para ativá-la, basta executar o comando à seguir:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

À partir daí você pode gerar uuids ad-hoc ou definir colunas com o tipo uuid.

SELECT uuid_generate_v4();
-- cfe48d4f-246f-4f24-85b5-fb1d1a8a520e

CREATE TABLE users (
  id uuid PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
  name text NOT NULL
);

INSERT INTO users (name) VALUES ('John Doe');
-- SELECT * FROM users;
--                  id                  |   name
-- -------------------------------------+----------
-- e84f4f0e-2127-4e66-a0be-1463141ae7ae | John Doe

Uma das vantagens de se usar uuid como identificadores de registro é que você pode descentralizar a criação desses identificadores. Você pode, por exemplo, gerar o id à partir do cliente, apenas salvando os valores no banco de dados. Em um mundo onde temos cada vez mais clientes para uma mesma API, isso faz muito sentido. Pense, por exemplo, em uma aplicação client-side como Ember.js, onde você pode simplesmente salvar os registros localmente (localStorage) sem travar a interface esperando uma resposta do servidor se o registro foi salvo ou não, fazendo esse processamento de forma assíncrona.

Outra vantagem é que você não dará mais dicas de quantos registros foram criados em uma determinada tabela. Embora isso possa parecer besteira, esconder esse tipo de informação é importante para alguns clientes e pode evitar que você tenha queda de 18% no valor de suas ações, como foi o caso do Twitter.

Usando UUID no Rails

O Rails possui suporte nativo para uuid. Para isso, basta utilizar o método uuid.

enable_extension 'uuid-ossp'

create_table :users do |t|
  t.uuid :api_key, null: false, default: 'uuid_generate_v4()'
end

Você também pode utilizar chaves primárias como uuids.

create_table :users, id: :uuid do |t|
  # ...
end

Para definir relactionamentos, você deve utilizar a opção type.

create_table :posts, id: :uuid do |t|
  t.references :user, type: :uuid, null: false, index: true
  # or
  t.belongs_to :user, type: :uuid, null: false, index: true
end

A mesma coisa é necessária caso você utilize o método add_reference.

add_reference :posts, :users, type: :uuid, null: false, index: true

Embora a probabilidade de ids duplicados serem criados seja muito baixa, ela é possível.

O uso de uuids no Rails tem um problema. Ele faz com que os métodos ActiveRecord::Base.first e ActiveRecord::Base.last deixem de funcionar como o esperado, já que o Rails faz a busca ordenando os registros pelo campo id.

User.first
#=> SELECT * FROM users ORDER id ASC LIMIT 1

Caso você precise fazer algo do tipo, ordene pelo campo created_at e só então faça a chamada do método.

User.order(created_at: :asc).first
#=> SELECT * FROM users ORDER created_at ASC LIMIT 1

Você pode extrair isso para um módulo/concern e adicionar em cada classe que queira utilizar este comportamento.

Outras funcionalidades afetadas pelo uso de uuids são ActiveRecord::Base.find_in_batches e ActiveRecord::Base.find_each. Na verdade, eles deixarão de funcionar completamente, então se você precisa deles, considere implementar algo semelhante que utiliza o campo created_at para ordenação.

Evitando a parte chata

Umas das coisas que acho inconveniente é sempre ter que adicionar o tipo uuid em nossas chaves primárias. Embora seja uma tarefa simples, eu esqueço de fazer isso com uma certa frequência. Para evitar este problema, criei uma gem bastante simples que sempre define os campos como uuid. Basta adicionar a linha à seguir ao seu arquivo Gemfile e pronto! Você não precisará mais especificar id: :uuid ou ter que usar type: :uuid quando for criar um relacionamento.

gem 'ar-uuid'

Finalizando

Mesmo com algumas inconsistências do Rails, é possível usar campos uuid como chaves primárias com uma certa facilidade. Implementar uma lógica mais inteligente para os métodos ActiveRecord::Base.first, ActiveRecord::Base.last, ActiveRecord::Base.find_each e ActiveRecord::Base.find_in_batches não seria algo muito difícil de fazer; talvez verificar o tipo da chave primária e alternar o modo de consulta automaticamente para usar um campo bigserial ou o próprio created_at. Esses métodos podem ser implementados pelos adapters de conexão ao banco de dados, em vez de ser uma estratégia global definida no próprio ActiveRecord.

Você utiliza campos uuids? Você tem outras motivações para fazer isso? Deixe seu comentário!