DEV Community

Cover image for Enums no Rails 8: anatomia e uma aplicação prática
Dominique Morem
Dominique Morem

Posted on

Enums no Rails 8: anatomia e uma aplicação prática

Introdução

Por uma questão de Legibilidade e Manutenibilidade escolherei trabalhar com enums do tipo string e não sendo do tipo integer, que é o mais comum. Michał Rudzki fez um artigo muito bom com um comparativo de perfomance, bem como indicando os pontos positivos e negativos entre os dois.

Rudzki deu dois grandes motivos para optar pelo enum com o tipo string: em enums baseados em string, você pode ler os dados do registro diretamente do banco de dados (o que favorece a legibilidade), e, você pode adicionar um enum facilmente a uma coluna existente, sem precisar se preocupar com possível perda de dados se utilizar o tipo string ( o que contribui na manutenibilidade).

Dito isso, vamos aplicação do enum. Antes de começarmos, pressuponho que você já tenha feita e migrada a tabela na qual deseja colocar o seu enum. A tabela - não só ela mas todo o MVC envolvido nos Usuários - que usaremos aqui foi criada da seguinte forma e com os seguintes campos:

rails g scaffold User name email birthdate:date
Enter fullscreen mode Exit fullscreen mode

Depois disso eu dei o rails db:migrate (já tinha criado o banco de dados previamente) e então partimos para o ponto de colocação do enum que veremos à seguir...

1 - Gere a MIGRATION

rails g migration AddMaritalStatusToUsers marital_status:string:index
Enter fullscreen mode Exit fullscreen mode

Onde:

  • AddMaritalStatusToUsers - É o comando que indica pro Rails que você quer colocar uma nova coluna em uma determinada entidade do seu banco de dados. No seu caso será: AddNomeDoSeuEnumToNomeDaSuaTabela.
  • marital_status - É o nome da coluna a ser adicionada a tabela Users
  • string - O tipo de dado que essa coluna receberá
  • index - Cria um índice para a coluna, o índice é uma estrutura auxiliar do banco que acelera buscas.

2 - Edite a MIGRATION

Acrescente o default: "solteiro":

'Image description'

Isso vai colocar por padrão (se o usuário não marcar nada) o enum como: "solteiro". Todos os campos que estão em branco ficarão com esse status automaticamente.

Execute no prompt:

rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Explicando melhor as linhas da migration

Sobre o add_column:

'Image description'

Sobre o add_index:
'Image description'

3 - Anatomia do enum no MODEL

Vamos configurar agora o nosso enum no model.

Bem, a "anatomia" do Enum do tipo string no Rails 8 é tipo essa:

enum :nome_do_seu_enum, {
chave1: "valor1",
chave2: "valor2,
chave3: "valor3"
}
Enter fullscreen mode Exit fullscreen mode

As bases para se chegar a essa 'anatomia' estão aqui (no artigo do Michał Rudzki) e aqui (na documentação do Rails 8).

Aplicando o enum ao nosso cado aqui vai ficar assim:

enum :marital_status, {
    solteiro: "solteiro",
    casado: "casado",
    divorciado: "divorciado",
    viuvo: "viuvo",
    uniao_estavel: "uniao_estavel"
  }
Enter fullscreen mode Exit fullscreen mode

Há algo interessante que podemos aplicar a esse enum também para facilitar a nossa vida... o "prefix: true". Mas para quê ele serve?

3.1 - O que o "prefix: true" faz?

'Image description'

Ele adiciona um prefixo com o nome do atributo aos métodos gerados pelo enum.

Sem prefixo, o Rails criaria métodos assim:

customer.solteiro?
customer.casado?
customer.viuvo!
Enter fullscreen mode Exit fullscreen mode

Com prefix: true, eles viram:

customer.marital_status_solteiro?
customer.marital_status_casado?
customer.marital_status_viuvo!
Enter fullscreen mode Exit fullscreen mode

Com prefix: true:

  • tudo fica explícito
  • mais legível em projetos grandes
  • menos bugs “fantasma”

3.2 - Adicionando validações

Vamos acrescentar as seguintes validações ao nosso enum:

validates :marital_status, presence: true, inclusion: { in: marital_statuses.keys }
Enter fullscreen mode Exit fullscreen mode

Onde cada validação diz o seguinte:

  • validates - Rails valide para mim a seguinte estrutura
  • :marital_status - A coluna a ser validada
  • presence: true - Torna o preenchimento daquela coluna obrigatório, ou seja, não pode ser enviado ao banco de dados como nulo (nil), não pode ir em branco.
  • inclusion: - Valida que o valor do atributo está dentro de uma lista específica de valores permitidos.
  • marital_statuses.keys - Retorna um array com as chaves que poderão ser aceitas: ["solteiro", "casado", "divorciado", "viuvo", "uniao_estavel"].

Ou seja, o "presence" impede valores vazios e o "inclusion" impede valores inválidos, que não correspondam aos valores previamente passados.

4 - Permita a coluna no CONTROLLER

No Rails existe um conceito chamado: Strong Parameters.

Esse Strong Parameters quer dizer basicamente: Nenhum dado vindo do usuário entra no model se o programador do projeto não permitir explicitamente (isso por uma questão de segurança).

Ou seja, se você não for ao método "user_params" e não colocar o "marital_status" explicitamente lá, o Rails simplesmente vai ignorar esse campo e seguir pleníssimo sem enviar essa informação para o banco da dados.

Por isso, vamos acrescentar a coluna 'marital_status' ao nosso método 'user_params':

'Image description'

5 - Trabalhando com o enum nas VIEWs

Vamos precisar mexer em duas partials (fragmentos de view reutilizáveis):

  • Uma que diz respeito ao formulário que será aplicado ao usuário (_form.html.erb)
  • Outra que diz respeito a exibição daquele usuário depois de cadastrado (_user.html.erb)

Prensando primeiro no select (menu) que faremos para o usuário escolher o seu status civil no formulário...

5.1 - Lidando com o enum no _form.html.erb

No enum atualmente temos os seguintes valores para os estados civis:

enum :marital_status, {
    solteiro: "solteiro",
    casado: "casado",
    divorciado: "divorciado",
    viuvo: "viuvo",
    uniao_estavel: "uniao_estavel"
  }, prefix: true
Enter fullscreen mode Exit fullscreen mode

Como podem ver, teremos um sério desconforto caso esses valores forem exibidos no menu para o usuário dessa forma. Isso porque:

  • União Estável possui espaço e acentos
  • Viúvo também possui acento
  • O ideal é que os status civis viessem com letra maiúscula
  • Os usuários serão de ambos os gêneros, logo, o ideal seria colocar a partícula (a) na intenção de contemplar a ambos.

Para resolver isso ao renderizar a partial sair com os valores bonitinhos no menu.

5.1.1 - Criando a constante que vai receber o hash com a tradução

Façamos o seguinte, vamos ao model user.rb:

app > models : user.rb
Enter fullscreen mode Exit fullscreen mode

Uma vez que estejamos no model, vamos criar uma constante que irá conter um hash (chave/valor) de dados contendo as chaves que estão do jeitinho lá do enum e os valores traduzidos:

MARITAL_STATUS_TRANSLATIONS = {
    "solteiro" => "Solteiro(a)",
    "casado" => "Casado(a)",
    "divorciado" => "Divorciado(a)",
    "viuvo" => "Viúvo(a)",
    "uniao_estavel" => "União Estável"
  }
Enter fullscreen mode Exit fullscreen mode

Pronto agora já temos uma 'tradução' com as perfumarias que queremos exibir!

Ok, mas como vamos aplicar isso a partial?

5.1.2 - Entendendo o helper options_for_select

Vamos a documentação

Para montar o nosso select (menu com os enums) vamos utilizar o helper options_for_select (ele é do tipo Form Options Helper). Vamos dar uma olhadinha na documentação dele:

'Image description'

Ele aceita diversas opções de entrada: hash, array e enumerável. Vamos utilizar essa primeira onde ele aceita receber arrays com chave/valor. Repare que:

  • O Primeiro elemento - É o texto que o usuário vê (ex: "Dollar")
  • O Segundo elemento - É o valor a ser salvo no banco (ex: "\$")

Então o array que vamos gerar precisará ter os primeiros elementos como sendo o valores bonitinhos e os segundos elementos os valores corretos do enum que são os que de fato vão para o banco. Vamos lá criá-lo entao...

5.1.3 - Criando método no model que vai nos ajudar na view

Vamos ao user.rb (app > models : user.rb) e neste criaremos um método de classe - ou seja, um método que pertence à classe em si. Que não precisa criar um objeto, chama direto na classe - para facilitar a nossa vida na hora de montar o select lá na view e deixar o codigo da view mais limpo e independente:

def self.marital_status_to_select
    marital_statuses.keys.map { |values|
      [MARITAL_STATUS_TRANSLATIONS[values], values]
    }
end
Enter fullscreen mode Exit fullscreen mode

Onde:

  1. self. - indica que é um método de classe, não de instância
  2. marital_statuses.keys - pega todas as chaves do enum como array: ["solteiro", "casado", "divorciado", "viuvo", "uniao_estavel"]
  3. .map { |values| ... } - itera sobre cada valor
  4. [MARITAL_STATUS_TRANSLATIONS[values], values] - cria um array com 2 elementos:
    • Primeiro elemento: texto que o usuário vê (ex: "Solteiro(a)")
    • Segundo elemento: valor salvo no banco (ex: "solteiro")

Com isso, teremos o seguinte retorno:

[
  ["Solteiro(a)", "solteiro"],
  ["Casado(a)", "casado"],
  ["Divorciado(a)", "divorciado"],
  ["Viúvo(a)", "viuvo"],
  ["União Estável", "uniao_estavel"]
]
Enter fullscreen mode Exit fullscreen mode

Perfeito, agora vamos lá as views aplicá-lo a partial do formulário...

5.1.4 - Aplicando o método de classe na partial

<div>
    <%= form.label :marital_status, style: "display: block" %>
    <%= form.select :marital_status,
                    options_for_select(
                      User.marital_status_to_select
                    )
     %>
  </div>
Enter fullscreen mode Exit fullscreen mode

5.1.5 - Deixando opção pré-selecionada em caso de edição

A partial _form.html.erb pode ser utilizada tanto no cadastro de um usuário novo como na edição de um usuário já existente.

Levando em consideração essa segunda possibilidade utilizaremos o  form.object.marital_status. Ele é usado para pré-selecionar a opção correta no dropdown quando você está editando um usuário existente.

A coisa toda vai ficar assim:

<div>
    <%= form.label :marital_status, style: "display: block" %>
    <%= form.select :marital_status,
                    options_for_select(
                      User.marital_status_to_select,
                      form.object.marital_status
                    )
     %>
  </div>
Enter fullscreen mode Exit fullscreen mode

5.2 - Lidando com o enum no _user.html.erb

Para além de dar a opções bonitinhas, bem legíveis e amigáveis lá para a pessoa na hora de marcar no select (menu) do formulário, devemos prover também ao usuário poder ver o seu status matrimonial de maneira linda e amigável depois que preencher e enviar o nosso formulário.

Para isso é que vamos mexer na partial _user.html.erb . Mas antes precisamos configurar algo no model dos usuários.

5.2.1 - Criando o método para a tradução no Model

Vamos até o user.rb (Ctrl + p para fazer a busca rápida).

Uma vez no model criaremos neste um método de instância (ou seja, que pertence a um objeto específico (uma instância da classe) para fazer essa tradução bonitinha pro usuário na hora que ele for ver o cadastro que acabou de criar.

'Image description'

Onde:

  1. marital_status - Esse marital_status é o valor do atributo da instância atual do User. Ou seja, ele vem da coluna marital_status e não do enum.
  2. MARITAL_STATUS_TRANSLATIONS[...] - Usa esse valor como chave no hash de traduções

Um exemplo do que acontece ao aplicar esse método:

  1. marital_status retorna "solteiro"
  2. MARITAL_STATUS_TRANSLATIONS["solteiro"] busca no hash
  3. Retorna "Solteiro(a)"

5.2.2 - Modificando na partial _user.html.erb

Uma vez que você esteja na partial, basta aplicar o método ao objeto de user e vòi la vai ficar com o textinho na tela!!

<div>
    <strong>Marital Status:</strong>
    <%= user.marital_status_humanized %>
</div>
Enter fullscreen mode Exit fullscreen mode

'Image description'

6 - Possibilidades de melhoria

Obviamente o conteúdo que cá está não esgota a temática dos enums no Rails, e nem pretende fazê-lo! Existem inúmeros tópicos que poderiam ser debatidos ainda nos enums, como por exemplo:

  • A aplicação do I18n para internacionalização e adaptação a outros idiomas.
  • A remoção do 'default' da migration podendo, consequentemente, colocar um placeholder no select
  • Falar de scopes
  • Entre outros temas...

Top comments (0)