DEV Community

Stephann
Stephann

Posted on

Evitando IDs sequenciais no Ruby on Rails

Introdução

Por padrão as aplicações Ruby on Rails utilizam identificadores sequenciais para representar a chave primária de uma tabela. Por alguns motivos esse comportamento pode não ser o desejado, seja por revelar informações estratégicas do produto ou por facilitar que alguém mal intencionado explore falhas da aplicação. Por exemplo, se um sistema utiliza ID sequenciais, fica fácil descobrir quantos usuários a empresa tem ou então quantos pedidos ela está recebendo. Ou então, alguém criar um script que saia percorrendo os IDs de 1 em 1 extraindo as informações disponíveis, sejam públicas ou que foram expostas por alguma falha de autorização. Uma alternativa que pode ser utilizada para evitar esses problemas é o UUID. Nesse artigo mostro como configurar sua aplicação para utilizar o UUID como a chave primária de suas tabelas.

Como toda escolha que fazemos no desenvolvimento de software traz vantagens e desvantagens, essa decisão entre UUID x Inteiro não seria diferente, vale a pena ler o artigo do Rafael Ponte sobre esse assunto: https://rponte.com.br/2021/01/30/nao-use-uuid-como-pk-nas-tabelas-do-seu-banco-de-dados/. Leve em consideração o peso de utilizar UUIDs como chave primárias e analise se faz sentido para o seu caso.

O que é UUID?

O UUID é um unificador único universal, que também pode ser chamado de GUID (identificador único global). Ele é formado por 32 dígitos hexadecimais agrupados em 5 grupos separados por hífens. Exemplos de UUID:

71b69829-813b-44c7-b75d-8a2f3728520d
ec1a3b99-528f-4f90-a8dc-46f467848b93
a04f2e7c-47ab-419d-bfab-32fd4492d80e
f9daaec4-b915-4e07-8690-6246b48a1cb9

Imaginando a URL de detalhes de um pedidos de compra, ao invés de meusite.com/orders/32, ela seria algo assim: meusite.com/orders/71b69829-813b-44c7-b75d-8a2f3728520d. Ou seja, não revela nada sobre a quantidade de pedidos que a aplicação recebeu, e caso alguém queira tentar acessar pedidos de outros usuários, será quase impossível de advinhar os IDs de pedidos existentes.

Instalando a extensão do PostgreSQL

A função que o PostgreSQL utilizará para gerar os UUID para as nossas chaves primárias é a gen_random_uuid(), uma função da extensão pgcryto que não vem instalada por padrão. Então para instalá-la, primeiro crie uma migração:

bundle exec rails g migration enable_pgcrypto_extension
Enter fullscreen mode Exit fullscreen mode

E no arquivo gerado coloque:

class EnablePgcryptoExtension < ActiveRecord::Migration[6.1]
  def change
    enable_extension 'pgcrypto'
  end
end
Enter fullscreen mode Exit fullscreen mode

Ao executar um bundle exec rails db:migrate, a extensão estará instalada e pronto para ser utilizada.

Configurando os generators para utilizar o UUID

Mesmo com a extensão instalada, se você gerar um modelo ou um scaffold, as estruturas de banco de dados continuarão sendo geradas com um ID sequencial. Isso porque não foi configurado nada no Rails ainda para que ele utilizasse o UUID. Então vamos fazer isso. Crie o arquivo config/initializers/generators.rb e adicione o seguinte código:

Rails.application.config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
end
Enter fullscreen mode Exit fullscreen mode

Agora ao gerar um modelo a migração será criada assim:

class CreateOrders < ActiveRecord::Migration[6.1]
  def change
    create_table :orders, id: :uuid do |t|
      t.string :description
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

A diferença é o parâmetro id: :uuid passado no create_table, que fará o tipo da coluna ID ser UUID. Outra coisa que essa configuração faz, é informar automaticamente o tipo do ID na definição da chave estrangeira. Explicando melhor: Se por acaso eu precise adicionar a coluna com o ID do pedido na tabela pagamento, essa coluna precisa ser do tipo UUID também. Então caso eu execute um bundle exec rails g migration add_order_to_payments order:reference, será gerada uma migração assim:

class AddOrderToPayments < ActiveRecord::Migration[6.1]
  def change
    add_reference :payments, :order, 
      null: false, 
      foreign_key: true, 
      type: :uuid
  end
end
Enter fullscreen mode Exit fullscreen mode

Note o type: :uuid nos parâmetros do add_reference, isso fará a coluna order_id ser do tipo UUID e assim poderá se referenciar a outra tupla da tabela orders.

Finalizando a configuranção

Basicamente já está tudo configurado e pronto para a utilização, não precisa fazer mais nada. Podemos confirmar isso criando objetos pelo terminal com o bundle exec rails c e executando algum Order.create(description: 'Some description') ou então utilizando as telas normalmente para cadastrar os dados. Os IDs estarão preenchidos normalmente com os UUID, a navegação na URL também estará funcionando normalmente.

Se você for curioso o suficiente, ao navegar nas telas ou executando algo como Order.first ou Order.last, perceberá que a ordenação está estranha. O Model.first nem sempre retornará o primeiro registro criado, e o Model.last poderá não retornar o registro mais recente. Isso se deve ao fato do Rails, por padrão, ordenar as consultas pelo ID, ou seja, ele automagicamente faz por baixo algo como um ORDER BY id nas consultas. Como o UUID não é gerado em uma ordem específica, o primeiro registro criado pode ter seu ID iniciando com ec1a3..., e o último criado ter o ID iniciando com a04f2..., então ao fazer um Model.first, o Rails trará na verdade o último registro criado.

Mas isso pode ser configurado, é bem simples. Essa configuração é feita no arquivo app/models/application_record.rb, pois é a classe que todos os outros modelos herdam. Esse arquivo deve ficar assim:

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
  self.implicit_order_column = :created_at
end
Enter fullscreen mode Exit fullscreen mode

Foco na linha self.implicit_order_column = :created_at, que fará o Rails utilizar a coluna created_at para ordenar os registros caso não seja explicitamente informado nenhum outro campo de ordenação.

Conclusão

É isso. É algo simples, fácil, que não interfere em controller, nas rotas, o uso do framework continua basicamente o mesmo e você ainda tem todas as vantagens de não revelar informações estratégicas sobre o produto.

Um curiosidade para encerrar: O UUID não é único, o gen_random_uuid() do PostgreSQL pode gerar um UUID repetido em uma mesma tabela. Mas não precisa se preocupar, mesmo que você tenha trilhões de registros em uma tabela, é mais fácil você morrer devido a uma queda de meteorito na sua cabeça, do que ser gerado um UUID duplicado, e quando isso acontecer, não será um problema grande, pois as chaves primárias do PostgreSQL têm restrição de unicidade. Mas se acontecer: Que azar, hein? Cuidado ao sair de casa.

Discussion (0)