Primeirona: Mais de uma tabela

Table of Contents

1 Primeirona (continuação)

1.1 Mais de uma tabela

Até agora usamos uma única tabela, no caso geral temos várias tabelas relacionadas. Vamos incluir uma tabela com categorias de pessoas e ligar os nomes às categorias.

Rails usa o nome do modelo para definir a tabela e os objetos. O nome de uma tabela deve estar no singular (ex. Apelido) e o plural é inferido pelo sistema. A palavra Categoria é considerada plural de Categorium, porisso usaremos Tipo.

Tipo:

cat descr
string string

Precisamos criar todos os elementos para esta nova tabela:

  • Model
  • Views
  • Controller
  • Tabela no banco (e fazer a migração)

Depois criar o código de "amarração" entre as tabelas.

1.2 Criação da estrutura

Rails permite criar a estrutura toda de uma vez com o gerador scaffold.

rails g scaffold Tipo cat:string descr:string
rake db:migrate

Um pouco mais rápido do que fazer passo a passo….

Observe as rotas: rails routes

Veja também os novos arquivos criados em app/controllers, app/views e app/models.

Experimente com as novas rotas:

  • tipos/new
  • tipos/
  • etc.

1.3 Ligando as tabelas

A criação de ligações entre as tabelas é feita por migrações (migrations). E, respeitanto o princípio de convenção sobre configuração, as migrações seguem padrões de nomes:

add_<xxx>_to_<yyy>
Adiciona campos no modelo <yyy>
remove_<xxx>_from_<yyy>
Remove campos do modelo <yyy>

Nos dois casos <xxx> serve como uma descrição.

Para criar uma referência a tipo dentro de Apelido, basta fazer

rails g migration add_tipo_ref_to_apelido tipo:references
rake db:migrate

Isto altera o banco. A tabela apelidos era:

id nome apelido created_at updated_at
int string string datetime datetime

E passou a ser:

id nome apelido created_at updated_at tipo_id
int string string datetime datetime int

O tipo_id indicará a qual categoria o apelido pertence.

Precisamos agora fazer com que o modelo utilize esta ligação. Alteramos apelido.rb com metaprogramação:

class Apelido < ApplicationRecord
  # ligação
  belongs_to :tipo

  # validações
  validates :nome, presence: true, length: {minimum: 3}
  validates :apelido, presence: true, uniqueness: true
end

belongs_to constrói os métodos e chamadas para garantir consistências.

De modo similar, alteramos tipo.rb para declarar que um Tipo corresponde a varios apelidos.

class Tipo < ApplicationRecord
  has_many :apelidos
end

No entanto, ainda precisamos decidir o que fazer quando um tipo é removido. O que acontece com os apelidos que referenciam este tipo? Se não fizermos nada, teremos apelidos "órfãos", com referências para valores inexistentes.

Temos algumas possibilidades:

  • Conviver com a inconsistência, neste caso não precisamos fazer nada, mas é uma má ideia. Não queremos inconsistências no banco.
  • Se um tipo for removido, os apelidos correspondentes também são removidos. Isto é facilmente implementado no próprio modelo, passando uma opção a mais para o has_many:
    class Tipo < ApplicationRecord
      has_many :apelidos, dependent: :destroy
    end
    
  • Impedir que um tipo seja removido caso ainda exista algum apelido "pendurado" nele. Também é bem simples, basta mudar o tratamento para :dependent
    class Tipo < ApplicationRecord
      has_many :apelidos, dependent: :restrict_with_exception
    end
    

    Na visão trataremos a exceção para avisar o usuário.

1.4 Resetando o banco

Se quiser testar as várias possibilidades, é interessante apagar o banco e recomeçar do zero.

Em db/seeds.rb você pode colocar comandos para popular o banco inicialmente. Assim, quando quiser começar do zero com alguns registros pré-definidos basta executar os comandos abaixo

rake db:drop
rake db:seed

Depois reinicie o servidor, para evitar que ele use páginas em cache.

Este é um exemplo de conteúdo para seeds.rb

# coding: utf-8
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
#   movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
#   Character.create(name: 'Luke', movie: movies.first)

tipos = Tipo.create([
                      {cat: 'Professor', descr: 'Dá aulas'},
                      {cat: 'Aluno', descr: 'Sofre'},
                      {cat: 'Funcionário', descr: 'Trabalha'},
                    ])

Apelido.create(nome: 'Alfredo', apelido: 'Teimoso', tipo_id: 1)
Apelido.create(nome: 'Jonas', apelido: 'Baleia', tipo_id: 3)

1.5 Controladores e visões

Acertamos o modelo, precisamos preparar o restante. Na verdade só são necessários alguns ajustes. Na criação e edição precisamos um campo de entrada a mais, para o tipo.

No controlador, precisamos permitir que o tipo_id seja lido e precisamos tomar cuidado com o destroy.

Primeiro as visões. Basta mudar o _formulario.html.haml e o index.html.haml (para mostrar o tipo de cada apelido na lista).

No _formulario.html.haml vamos incluir uma entrada do tipo dropdown que apresentará os tipos cadastrados.

= form_with model: @apelido, local: true  do |f|
  - if @apelido.errors.any?
    %ul
      - @apelido.errors.full_messages.each do |msg|
        %li= msg
  %table
    %tr
      %td
        = f.label :nome
      %td
        = f.text_field :nome
    %tr
      %td
        = f.label :apelido
      %td
        = f.text_field :apelido
  -# Aqui entra o tipo
  = select('apelido', :tipo_id,  Tipo.all.collect {|p| [p.cat,p.id]})
  = f.submit 'Salva'

select gera o dropdown, o primeiro parâmetro é o modelo, o segundo é o campo, depois vem uma lista com pares do nome e valor para cada opção. No presente caso, geramos os nomes e valores diretamente do modelo Tipo.

Para mostrar o tipo na lista é bem simples:

%center
  %h1.titulo=t(:list)
  %table.table-hover.table-bordered
    %thead
      %tr
        %th Nome
        %th Apelido
        -# Mais uma coluna para tipo
        %th Tipo
    - @apelidos.each do |ap|
      %tr
        %td= ap.nome
        %td= ap.apelido
        -# Pega o tipo correspondente e mostra
        -# pode ser que não tenha tipo cadastrado!
        - begin
          - c = Tipo.find(ap.tipo_id).cat
        - rescue
          - c= '???'
        %td= c
        -# link para edição
        %td
          = link_to edit_apelido_path(ap) do
            %span.glyphicon.glyphicon-pencil
        %td
          = link_to  apelido_path(ap), method: :delete,
               data: {confirm: t('sure')} do
            %span.glyphicon.glyphicon-remove
  = link_to t(:back), new_apelido_path

No controlador, como vimos, são duas modificações: permitir o uso de tipo_id em apelidos e o destroy do controlator de tipos. Como vimos, o problema do destroy é se quisermos remover um tipo que ainda possui apelidos "pendurados".

Em apelidos_controller.rb

private
def apelidos_params
  params.require(:apelido).permit(:nome, :apelido, :tipo_id)
end

Em tipos_controller.rb colocamos a verificação no método destroy.

Esta é a função gerada.

# DELETE /tipos/1
# DELETE /tipos/1.json
def destroy
  @tipo.destroy
  respond_to do |format|
    format.html { redirect_to tipos_url, notice: 'Tipo was successfully destroyed.' }
    format.json { head :no_content }
  end
end

Esta é a versão com modificações, mantive as mensagens em inglês, o ideal é colocar as tradulções em pt.yml:

# DELETE /tipos/1
# DELETE /tipos/1.json
def destroy
  begin 
    @tipo.destroy
    respond_to do |format|
      format.html { redirect_to tipos_url, notice: 'Tipo was successfully destroyed.' }
      format.json { head :no_content }
    end
  rescue
    respond_to do |format|
      format.html { redirect_to tipos_url, notice: 'Tipo was still referenced.' }
      format.json { head :no_content }
    end
  end
end

Na visão de tipos, ajustamos o index.html.haml para colocar um parágrado com identificador notice (%p#notice) que receberá as notificações.

%h1 Listing tipos

%p#notice
  = notice

%table
  %thead
    %tr
      %th Cat
      %th Descr
      %th
      %th
      %th

  %tbody
    - @tipos.each do |tipo|
      %tr
        %td= tipo.cat
        %td= tipo.descr
        %td= link_to 'Show', tipo
        %td= link_to 'Edit', edit_tipo_path(tipo)
        %td= link_to 'Destroy', tipo, method: :delete, data: { confirm: 'Are you sure?' }
%br

= link_to 'New Tipo', new_tipo_path

Claro que podemos aproveitar e deixá-lo compatível com a lista de apelidos:

%center
  %h1.titulo=t(:list)

  %p#notice
    = notice

  %table.table-hover.table-bordered
    %thead
      %tr
        %th Categoria
        %th Descrição
        %th 
    %tbody
      - @tipos.each do |tipo|
        %tr
          %td= tipo.cat
          %td= tipo.descr
          %td
            = link_to edit_tipo_path(tipo) do
               %span.glyphicon.glyphicon-pencil
          %td
            = link_to  tipo_path(ap), method: :delete,
                  data: {confirm: t('sure')} do
                 %span.glyphicon.glyphicon-remove
  = link_to t(:back), new_tipo_path

1.6 Outros ajustes

Deixe as páginas compatíveis e arrume o scss para acertar a estética.

Coloque todas as traduções.

Author: Marco Gubitoso

Created: 2019-04-13 sáb 11:52

Emacs 25.2.2 (Org mode 8.2.10)

Validate