Vitor Pamplona

Innovation on Vision: Imaging , Enhancement and Simulation

UUID ou GUID em rails

UUID (Universally Unique Identifier) ou GUID (Globally Unique Identifier) são técnicas de identificação de registros globais, ou seja, os seus registros recebem um id de forma que não existirá outro no mundo com o mesmo (probabilidade muito baixa de acontecer).

Na prática, este tipo de identificação é muito útil para evitar problemas de segurança e evitar que softwares mal intencionados encontrem registros desprotegidos. Por exemplo, se você recebe um e-mail com id = 5600, você pode assumir que existem outros 5599 e-mails, e pode tentar acessá-los simplesmente mudando a url. Porém, se você receber um e-mail com id = 11885-6c7f0bd17f, como o gmail faz, encontrar outro e-mail é algo computacionalmente custoso.

Em ruby, existe um gem que calcula este " hash " chamado uuidtools. Para integrá-lo ao rails, você pode fazer na mão, ou baixar o plugin:

script/plugin install http://tools.assembla.com/svn/breakout/breakout/vendor/plugins/guid 

Inclua a chamada de função a usesguid em cada classe do seu modelo:

class Mymodel < ActiveRecord::Base
usesguid

Altere suas migrations, adicionando a opção id igual a false na função create_table e uma nova coluna: id como string. Lembre-se, também, de alterar todas as referências (FKs) para strings.

create_table :mytable, :id => false do |t|
t.column :id, :string, :limit => 22
... more fields
end

Se você não puder alterar as migrations terá um sério problema na migração dos dados.

Depois destas alterações, teoricamente, tudo deveria funcionar. Porém, não é bem assim.

Rails + GUID + PostgreSQL

Por convenção do banco de dados, o PostgreSQL não aceita comparar tipos diferentes. No caso do id ser um varchar, esta consulta retorna um erro:

select * from something where id = 12345 

O select deveria ficar assim

select * from something where id = '12345' 

Se a mesma consulta errada for feita em Oracle, ela roda, mas você quebra o índice, tornando as consultas bem lentas. Quando tentei validar os testes, uma série de erros relacionando o find e este tipo de consulta foram reportados. Descobri que estavam relacionadas as fixtures.

Por exemplo, a fixture abaixo:

one:
id: 1
descricao: MyString1
outra_tabela_id: 1

Os campos id e outra_tabela_id são entendidos como integers pelo ruby, logo pelo rails e em consequência, pelo método find, que irá gerar uma sql parecida com aquela errada demonstrada acima.

Solução? Adicione alguns caracteres nos campos ID, só para serem interpretados como String, ao invés de integer:

one:
id: M1
descricao: MyString1
outra_tabela_id: M1

O Erro do Mac Address

Ao executar os testes novamente me deparei com a seguinte mensagem de erro, que estava sendo disparada pela uuidtools:

NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.split
/home/vfpamplona/workspaceRuby/xx/vendor/plugins/guid/lib/uuidtools.rb:236:in `timestamp_create'
/home/vfpamplona/workspaceRuby/xx/vendor/plugins/guid/lib/uuidtools.rb:226:in `synchronize'
/home/vfpamplona/workspaceRuby/xx/vendor/plugins/guid/lib/uuidtools.rb:226:in `timestamp_create'

Se você procurar pela linha, ela vai apontar para uma função get_mac_address, que estará retornando nil. Ainda não entendi porque, apenas nos testes, o ruby não consegue buscar o endereço mac da placa de rede. A solução é setar um endereço mac default na mão mesmo. Abra o uuidtools.rb, dentro da pasta do plugin, e altere a linha:

@@mac_address = nil

para um endereço mac válido, exemplo:

@@mac_address = "00:1b:63:1e:b2:da" 

Automatizando mais!

Se você desejar transformar o ID para GUID / UUID em todos os modelos, sem exceção, você pode, simplesmente, alterar o usesguid.rb do plugin, adicionando a linha 3:

ActiveRecord::Base.class_eval do
  include ActiveRecord::Usesguid
  usesguid
end

Mas, se fizer isso, você também precisa que todas as suas tabelas tenham um id default como VARCHAR (22). Isto pode ser feito utilizando os alias do rails. Crie um módulo chamado TableDefinition.rb e digite:

module TableDefinition
def self.included(base)
base.class_eval do
alias_method_chain :create_table , :uuid_create_table
end
end

# Change the defaults of create table.
# Instead of a sequenced primary key,
# it creates an id with string(22) and an it's index
def create_table_with_uuid_create_table(table_name, options = {}, &block)
options[:id] = false
create_table_without_uuid_create_table(table_name, options) do |table_defintion|
table_defintion.column :id, 'varchar(22) primary key'
yield table_defintion
end
end
end

O alias_method_chain redirecionará todas as chamadas da função create_table para a create_table_with_uuid_create_table e criará uma nova função create_table_without_uuid_create_table para permitir uma chamada ao create_table original. O código da nova função faz exatamente o que deveríamos fazer para instalar o plugin, seta o id default para false e adiciona uma coluna id. Note que aqui não estamos utilizando o tipo string, mas sim passando o tipo direto ao banco de dados. Desta forma, garantimos que o índice seja criado, e que o campo seja not_null.

Adicione esta linha ao final do arquivo usesuid.rb para mesclar o módulo criado com o existente:

ActiveRecord::ConnectionAdapters::SchemaStatements::send(:include, TableDefinition)

E pronto, agora você não precisa mais aterar nenhuma PK das suas migrations, apenas as FKs.

Lembrando que: não foi necessário alterar nenhum código da aplicação, apenas as fixtures.

Posted in Mar 7, 2008 by Vitor Pamplona - Edit - History

Showing Comments

velho vi seu post me interessou mto porem na versao atual do uuidtools nao encontrei o arquivo usesguid.rb mais algumas coisinhas, se possivel entrar em contato comigo, meu nole eh leonardo justino, leojustino@gmail.com

- - Leo

- - Posted in May 25, 2010 by 189.7.26.2

Add New Comment

Your Name:


Write the code showed above on the text below.