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.