Associações entre modelos
Para testar as associações entre os nossos modelos, temos que criar um novo,
pois só temos um. Vamos criar um scaffold novo com o modelo Book, que terá
os seguintes atributos:
- title - Título
- published_at - Data de publicação
- text - Texto descritivo do livro
- value - Valor do livro
- person_id - Autor do livro
Criando o novo scaffold:
1 $ rails g scaffold Book title:string published_at:date text:text value:decima\
2 l person:references
3 invoke active_record
4 create db/migrate/20170311144612_create_books.rb
5 create app/models/book.rb
6 invoke test_unit
7 create test/models/book_test.rb
8 create test/fixtures/books.yml
9 ...
Agora é adaptar e rodar as migrations, alterar os testes unitários e funcionais, limitar o acesso ao controlador de livros para somente quem tiver feito o login, verificar o layout do controlador e rodar os testes para ver se está tudo ok.
Nem vamos escrever por aqui como faz isso, pois é basicamente o que fizemos com o controlador de pessoas, somente algumas observações para a migration, vista aqui já alterada:
1 class CreateBooks < ActiveRecord::Migration[5.0]
2 def change
3 create_table :books do |t|
4 t.string :title, limit: 100, null: false
5 t.date :published_at, null: false
6 t.text :text, null: false
7 t.decimal :value, precision: 10, scale: 2, null: false
8 t.references :person, foreign_key: true
9 t.timestamps
10 end
11 end
12 end
Dando uma olhada no que customizamos nas colunas da tabela do banco de dados:
- A coluna
titlevai ter um limite de 100 caracteres (limit: 100) e não pode ter o seu valor nulo (null: false). - A coluna
published_atnão pode ter o seu valor nulo. - A coluna
textnão pode ter o seu valor nulo. - A coluna
valuenão pode ter o seu valor nulo, tem que ter precisão de 10 dígitos (precision: 10) sendo que 2 digitos (scale: 2) são utilizados como casas decimais, ou seja, temos um tamanho de 8 dígitos antes da casa decimal. - Foi criada uma referência para outra tabela, através de
:person(ou seja, referenciando a tabelaPeople, de acordo com as convenções), sendo definida como chave estrangeira4. Isso leva a criar uma coluna chamadaperson_idna tabelaBook, que é a chave estrangeira que aponta para oidda tabelaPeopleque está referenciado emperson_id.
Vamos adaptar a nossa fixture de livro:
1 one:
2 title: Conhecendo Ruby
3 published_at: 2013-06-29
4 text: Livro prático sobre a linguagem Ruby
5 value: 1.00
6 person: admin
7
8 two:
9 title: Conhecendo o Git
10 published_at: 2013-06-24
11 text: Quer aprender Git de forma rápida e prática?
12 value: 10.00
13 person: admin
Reparem que utilizei, ao invés de person_id, o nome da associação,
person, e a chave da fixture de pessoas, admin, para indicar na fixture
que o livro está associado com a pessoa da outra fixture. Eu poderia ter
utilizado autor, mas fui xarope e utilizei admin para fazer propagandas dos
meus livros e ebooks. ;-)
Isso funciona pois automagicamente quando utilizamos references ao criar a
migration, o Rails já embutiu código dentro do modelo, que vamos ver logo
abaixo. Antes de mais nada vamos alterar nossos testes (que, após a adaptação da
fixture acima já devem estar rodando de boa) para refletir o comportamento
que queremos que o modelo do livro apresente, seja baseado no que definimos no
banco de dados (que é o que vamos fazer aqui, limitar pelas constraints
inseridas no banco) ou em alguma regra específica para ele.
Vamos alterar nosso teste do modelo Book, presente em
test/models/book_test.rb para:
1 require 'test_helper'
2
3 class BookTest < ActiveSupport::TestCase
4 setup do
5 @book = books(:one)
6 end
7
8 # título
9 test 'deve ter um título' do
10 @book.title = nil
11 assert !@book.valid?
12 end
13
14 test 'não pode ter mais que 100 caracteres' do
15 @book.title = '*' * 101
16 assert !@book.valid?
17 end
18
19 # data de publicação
20 test 'deve ter data de publicação' do
21 @book.title = nil
22 assert !@book.valid?
23 end
24
25 # texto
26 test 'deve ter texto' do
27 @book.text = nil
28 assert !@book.valid?
29 end
30
31 # valor
32 test 'deve ter valor' do
33 @book.value = nil
34 assert !@book.valid?
35 end
36
37 test 'deve ser um número' do
38 @book.value = 'bla'
39 assert !@book.valid?
40 end
41
42 test 'não deve ser maior que o permitido' do
43 @book.value = 100000000.00
44 assert !@book.valid?
45 end
46
47 # pessoa
48 test 'deve ter pessoa' do
49 @book.person = nil
50 assert !@book.valid?
51 end
52 end
Associação do tipo um-para-um
Nosso livro (o da aplicação, não esse que você está lendo) precisa de uma pessoa
que seja o autor, que desejamos acessar como um método chamado person, em um
objeto representando um livro. Esse foi um dos testes que inserimos acima, o
último, por sinal, no arquivo de teste unitário de livro.
Vamos dar uma olhada em arquivo do modelo de Book, ainda sem as validações
necessárias para passar nos testes, mas já com o resultado da automagicamente
criada coluna especificada na migration através de references:
1 class Book < ApplicationRecord
2 belongs_to :person
3 end
Ali no modelo foi indicado automagicamente (lógico que podemos inserir e alterar isso na hora que quisermos) que um livro pertence à uma pessoa, utilizando o método belongs_to, ou seja pertence à. Dessa forma, foi criada uma associação no ORM indicando uma dependência (forte, por sinal, é uma foreign key do livro para a pessoa.
Essa associação, por padrão, a partir do Rails 5, não pode ficar vazia, ou
seja, se um livro pertence à uma pessoa, deve obrigatoriamente conter um
registro válido, uma foreign key válida, ali. Se por acaso desejarmos por
algum motivo que essa verificação seja afrouxada, podemos especificar no final a
Hash optional: true, dessa forma:
1 class Book < ApplicationRecord
2 belongs_to :person, optional: true
3 end
Isso é útil para migrar aplicações de versões anteriores que permitiam esse
comportamento. Podemos até definir esse comportamento para a aplicação inteira,
utilizando o seguinte código em um initializer:
1 Rails.application.config.active_record.belongs_to_required_by_default = false
Mas vamos deixar sem o optional e manter o que já vem definido por padrão.
Vamos terminar de inserir as validações necessárias para o modelo passar nos testes:
1 class Book < ApplicationRecord
2 validates :title, presence: true, length: { maximum: 100 }
3 validates :published_at, presence: true
4 validates :text, presence: true
5 validates :value, presence: true, numericality: { less_than_or_equal_to: 99\
6 999999.99 }
7 validates :person, presence: true
8
9 belongs_to :person
10 end
Vamos ver como funcionam essas validações logo mais no livro, mas tem algumas ali que até já são auto-explicativas. Se rodarmos nossos testes agora, todos devem passar.
Como temos a associação com pessoa, em nossos objetos de livros, ela já pode ser testada no console ou no navegador:
1 book = Book.new
2 => #<Book id: nil, title: nil, published_at: nil, text: nil, value: nil, pers\
3 on_id: nil, created_at: nil, updated_at: nil>
4
5 book.title = "Ruby - Conhecendo a Linguagem"
6 => "Ruby - Conhecendo a Linguagem"
7
8 book.published_at = "2006-03-01"
9 => "2006-03-01"
10
11 book.text = "Tinha, mas acabou."
12 => "Tinha, mas acabou."
13
14 book.value = 40.00
15 => 40.0
16
17 book.person = Person.last
18 Person Load (0.3ms) SELECT "people".* FROM "people" ORDER BY name DESC LIMIT\
19 1
20 => #<Person id: 2, name: "Eustáquio Rangel", email: "taq@bluefish.com.br",
21 password: "9733340c840c719779f234407ee0bac26ae8904b", born_at: "1971-04-06",
22 admin: true, created_at: "2013-07-06 22:51:34", updated_at: "2013-07-07
23 22:48:20">
24
25 book.person.name
26 => "Eustáquio Rangel"
27
28 book.save
29 (0.1ms) begin transaction
30 SQL (28.9ms) INSERT INTO "books" ("created_at", "person_id", "published_at",
31 "text", "title", "updated_at", "value") VALUES (?, ?, ?, ?, ?, ?, ?)
32 [["created_at", Tue, 09 Jul 2013 15:15:46 UTC +00:00], ["person_id", 2],
33 ["published_at", Wed, 01 Mar 2006], ["text", "Tinha, mas acabou."], ["title",
34 "Ruby - Conhecendo a Linguagem"], ["updated_at", Tue, 09 Jul 2013 15:15:46 UTC
35 +00:00], ["value", #<BigDecimal:ae70608,'0.4E2',9(45)>]]
36 (114.1ms) commit transaction
37 => true
Ficando assim:
Associação do tipo um-para-muitos
Já que um livro pertence à uma pessoa, agora podemos especificar que uma pessoa pode ter vários livros. Primeiro, no teste unitário:
1 test "deve ter uma coleção de livros" do
2 person = people(:admin)
3 assert_respond_to person, :books
4 assert_kind_of Book, person.books.first
5 end
E no modelo, estabelecendo a associação, usando has_many:
1 has_many :books
Através do has_many temos uma coleção de livros no objeto da pessoa, como
podemos verificar pelo console:
1 p = Person.last
2 Person Load (0.3ms) SELECT "people".* FROM "people" ORDER BY name DESC LIMIT\
3 1
4 => #<Person id: 2, name: "Eustáquio Rangel", email: "taq@bluefish.com.br",
5 password: "9733340c840c719779f234407ee0bac26ae8904b", born_at: "1971-04-06",
6 admin: true, created_at: "2013-07-06 22:51:34", updated_at: "2013-07-07
7 22:48:20">
8
9 p.books
10 Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."person_id" = 2
11 => [#<Book id: 1, title: "Ruby - Conhecendo a Linguagem", published_at:
12 "2006-03-01", text: "Tinha, mas acabou.", value:
13 #<BigDecimal:aaae9c0,'0.4E2',9(36)>, person_id: 2, created_at: "2013-07-09
14 15:15:46", updated_at: "2013-07-09 15:15:46">]
15
16 p.book_ids
17 => [1]
18
19 p.books.last.title
20 => "Ruby - Conhecendo a Linguagem"
Ficando assim:
Fica aqui uma dica sobre o que acontece quando apagamos o registro da pessoa com
vários livros. O comportamento padrão é não-intrusivo e não faz nada, mas
podemos especificar o que deve ser feito através de :dependent, da seguinte
forma:
1 has_many :books, dependent: :destroy
Os seguintes comportamentos estão disponíveis para :dependent:
- destroy - Os objetos associados são destruídos junto com o objeto corrente, chamando os métodos destroy de cada objeto.
-
delete_all - Os objetos associados são apagados sem chamar o método
destroyde cada um. - nullify - Todas as chaves estrangeiras dos objetos associados são transformadas em nulo, sem chamar os callbacks de atualização dos objetos.
-
restrict_with_error - Evita que o objeto seja apagado, retornando
falsese tem mais objetos associados. - restrict_with_exception - Evita que o objeto seja apagado, disparando uma exceção se tem mais objetos associados.
Podemos fazer testes unitários em Person para verificar que o comportamento de
:restrict_with_exception ou :destroy são obedecidos, e para termos certeza
de que o modelo está configurado corretamente de acordo com a nossa escolha.
Para o teste de :restrict_with_exception, nesse caso alterando o modelo para
1 has_many :books, dependent: :restrict_with_exception
podemos utilizar:
1 test "não pode apagar a pessoa se ela tiver livros" do
2 person = people(:admin)
3 assert person.books.size > 0
4
5 assert_no_difference('Person.count') do
6 assert_no_difference('Book.count') do
7 assert_raise ActiveRecord::DeleteRestrictionError do
8 assert !person.destroy, "não deveria apagar com #{person.books.size} \
9 livro(s)"
10 end
11 end
12 end
13 end
Rodando os testes, podemos ver que tudo está funcionando de acordo. Agora, para
o teste de :destroy, vamos alterar o modelo para
1 has_many :books, dependent: :destroy
e nos testes podemos comentar o teste anterior e inserir o seguinte código:
1 test "deve apagar os livros quando apagar a pessoa" do
2 person = people(:admin)
3 assert person.books.size > 0
4
5 assert_difference('Person.count', -1) do
6 assert_difference('Book.count',person.books.size * -1) do
7 assert person.destroy, "deveria apagar a pessoa"
8 end
9 end
10 end
Podemos notar que o primeiro comportamento e teste é para evitar que a pessoa
seja apagada se tiver itens na coleção, chutando o pau da barraca e disparando
uma Exception se isso ocorrer, enquanto que o segundo é para garantir que os
itens da coleção sejam apagados se a pessoa for apagada.
Associações de muitos para muitos
Vamos criar um novo scaffold para cadastrarmos as categorias que nossos livros
pertencem. Os livros cadastrados até aqui já pertencem à duas categorias que
podemos definir como “tecnologia” e “programação”. Vamos criar mais um
scaffold bem simples para gerenciar as categorias, e já rodar as migrations:
1 $ rails g scaffold Category name:string
2 invoke active_record
3 create db/migrate/20170312135948_create_categories.rb
4 create app/models/category.rb
5 invoke test_unit
6 $ rails db:migrate
Após o scaffold criado, podemos acessar http://localhost:3000/categories e
criar as categorias mencionadas acima. Se quiserem adicionar outras para ver
como fica a seleção, fiquem à vontade. Vamos adaptar também as fixtures
criadas para refletir nas chaves algo mais identificável nas categorias, não
esquecendo de trocar nos testes funcionais as chaves onde está sendo
referenciado one para alguma das que vamos criar agora:
1 tech:
2 name: Tecnologia
3
4 dev:
5 name: Desenvolvimento
Para deixar as categorias disponíveis no gerenciamento de livros, no controlador
de livros devemos pedir para que sejam carregadas, através do método
before_action:
1 class BooksController < ApplicationController
2 before_action :set_book, only: [:show, :edit, :update, :destroy]
3 before_action :load_categories, only: [:new, :edit, :create, :update]
4 ...
5
6 private
7 def load_categories
8 @categories = Category.all
9 end
10 ...
11 end
Agora que temos disponíveis as categorias, precisamos de um meio de indicar que
um livro tem várias categorias, e que a categoria tem vários livros. Para isso
vamos utilizar uma tabela auxiliar, ou join table, para armazenar o id do
livro e os ids (zero ou mais) das categorias dos livros. Vamos utilizar o método
has_and_belongs_to_many, que até o Rails 4 ficou na controvérsia se seria
ainda utilizado ou se seria marcado como deprecated, mas que até agora está
funcionando muito bem, e é bem simples de implementar.
Vamos criar a join table com os nomes dos dois modelos, em ordem alfabética, isso é muito importante, criando a seguinte migration:
1 $ rails g migration CreateJoinTableBookCategory book category
2 invoke active_record
3 create db/migrate/20170312145600_create_join_table_book_category.rb
Ela vai ter um conteúdo como esse, já removidos os comentários:
1 class CreateJoinTableBookCategory < ActiveRecord::Migration[5.0]
2 def change
3 create_join_table :books, :categories do |t|
4 t.index [:book_id, :category_id]
5 t.index [:category_id, :book_id]
6 end
7 end
8 end
Rodando a migration, vai ser criada a tabela books_categories, que não vai
ter um modelo associado, e nem um id. Esses são detalhes muito importantes em
uma join table desse tipo:
1 $ rails db:migrate
2 == 20170312145600 CreateJoinTableBookCategory: migrating ====================\
3 ==
4 -- create_join_table(:books, :categories)
5 -> 0.0076s
6 == 20170312145600 CreateJoinTableBookCategory: migrated (0.0076s)
7 =============
Agora vamos indicar no modelo de livro que ele tem vários relacionamentos com esse modelo novo, e que tem várias categorias. Antes de mais nada, testes no modelo de livro:
1 # categorias
2 test 'deve ter categorias' do
3 assert_respond_to @book, :categories
4 end
5
6 test 'deve ter categorias do tipo correto' do
7 @book.categories << categories(:one)
8 assert_kind_of Category, @book.categories.first
9 end
E agora sim podemos alterar o modelo de livro indicando que ele pertence à várias categorias:
1 class Book < ApplicationRecord
2 ...
3 has_and_belongs_to_many :categories
4 ...
5 end
Isso nos dá os seguintes métodos em Book:
1 > b = Book.first
2 Book Load (0.1ms) SELECT "books".* FROM "books" ORDER BY "books"."id" ASC
3 LIMIT ? [["LIMIT", 1]] => #<Book id: 14, title: "Conhecendo Ruby",
4 published_at: "2013-06-29", text: "Livro prático sobre a linguagem Ruby",
5 value: #<BigDecimal:1701a10,'0.1E1',9(18)>, person_id: 15, created_at:
6 "2017-03-12 15:22:07", updated_at: "2017-03-12 15:22:07">
7
8 > b.categories
9 Category Load (0.2ms) SELECT "categories".* FROM "categories" INNER JOIN
10 "books_categories" ON "categories"."id" = "books_categories"."category_id"
11 WHERE "books_categories"."book_id" = ? [["book_id", 14]] =>
12 #<ActiveRecord::Associations::CollectionProxy [#<Category id: 1, name:
13 "Tecnologia", created_at: "2017-03-12 14:02:51", updated_at: "2017-03-12
14 14:02:51">, #<Category id: 2, name: "Programação", created_at: "2017-03-12
15 14:02:51", updated_at: "2017-03-12 14:02:51">]>
16
17 > b.category_ids
18 => [1, 2]
E agora podemos fazer a mesma coisa no modelo de categoria:
1 class Category < ApplicationRecord
2 has_and_belongs_to_many :books
3 end
Que nos dá os métodos:
1 > c = Category.first
2 Category Load (0.4ms) SELECT "categories".* FROM "categories" ORDER BY
3 "categories"."id" ASC LIMIT ? [["LIMIT", 1]]
4 => #<Category id: 1, name: "Tecnologia", created_at: "2017-03-12 14:02:51",
5 updated_at: "2017-03-12 14:02:51">
6
7 > c.books
8 Book Load (0.3ms) SELECT "books".* FROM "books" INNER JOIN "books_categori\
9 es"
10 ON "books"."id" = "books_categories"."book_id" WHERE
11 "books_categories"."category_id" = ? [["category_id", 1]] =>
12 #<ActiveRecord::Associations::CollectionProxy [#<Book id: 14, title:
13 "Conhecendo Ruby", published_at: "2013-06-29", text: "Livro prático sobre a
14 linguagem Ruby", value: #<BigDecimal:3799958,'0.1E1',9(18)>, person_id: 15,
15 created_at: "2017-03-12 15:22:07", updated_at: "2017-03-12 15:22:07">, #<Bo\
16 ok
17 id: 15, title: "Conhecendo o Git", published_at: "2013-06-24", text: "Quer
18 aprender Git de forma rápida e prática?", value:
19 #<BigDecimal:378fef8,'0.1E2',9(18)>, person_id: 15, created_at: "2017-03-12
20 15:22:07", updated_at: "2017-03-12 15:22:07">]>
21
22 > c.book_ids
23 => [14, 15]
Ficando assim:
Vamos aproveitar o método category_ids de Book para fazer a nossa escolha de
categorias. Primeiro, vamos liberar nos strong parameters do controlador dos
livros o envio desse atributo, dessa forma:
1 def book_params
2 params.require(:book).permit(:title, :published_at, :text, :value, :person_\
3 id, category_ids: [])
4 end
E listamos as categorias disponíveis no formulário dos livros em
app/views/books/_form.html.erb através da coleção já presente no controlador
na variável @categories, com o método collection_check_boxes:
1 <div class="field">
2 <h2>Categorias</h2>
3 <%= collection_check_boxes :book, :category_ids, @categories, :id, :name \
4 %>
5 </div>
O que vai nos dar uma lista com as categorias cadastradas na forma de
checkboxes no formulário de edição dos livros. Ficou legal, mas podemos
melhorar para deixar com uma semântica mais adequada. Particularmente, eu
prefiro gerar um elemento ul com um elemento li para cada categoria. Podemos
personalizar da seguinte forma:
1 <ul>
2 <%= collection_check_boxes :book, :category_ids, @categories, :id, :name do |\
3 builder| %>
4 <li>
5 <%= builder.label { builder.check_box + builder.text } %>
6 </li>
7 <% end %>
8 </ul>
Agora sim ficou legal. Vamos alterar a view
app/views/books/show.html.erb para listar as categorias cadastradas do livro:
1 <h2>Categorias</h2>
2 <ul>
3 <% for category in @book.categories %>
4 <li><%= category.name %></li>
5 <% end %>
6 </ul>
Associações de muitos para muitos, através
Agora que temos uma pessoa com vários livros, um livro que pertence à uma pessoa
e tem várias categorias, sendo que uma categoria tem vários livros, podemos
pensar o seguinte: e se quisermos saber quais as categorias de livros que
uma pessoa tem? Podemos ver que está tudo conectado na modelagem conforme
descrito, só precisamos de um jeito de, quando estivemos na pessoa, recuperarmos
a categoria através (essa palavra é muito importante) aqui dos livros. Para
isso, vamos usar a associação has_many :through. Primeiro, vamos alterar a
fixture dos livros para incluírem as categorias, enviadas como um Array,
dessa forma:
1 one:
2 title: Conhecendo Ruby
3 published_at: 2013-06-29
4 text: Livro prático sobre a linguagem Ruby
5 value: 1.00
6 person: admin
7 categories: [ tech, dev ]
8
9 two:
10 title: Conhecendo o Git
11 published_at: 2013-06-24
12 text: Quer aprender Git de forma rápida e prática?
13 value: 10.00
14 person: admin
15 categories: [ tech, dev ]
Agora um teste de pessoa:
1 # categorias
2 test 'deve ter várias categorias, através de livros' do
3 assert_respond_to @person, :categories
4 end
5
6 test 'deve ter uma categoria do tipo correto' do
7 assert_kind_of Category, @person.categories.first
8 end
9
10 test 'deve ter apenas duas categorias' do
11 assert_equal 2, @person.categories.count
12 end
Convém atentar para o último teste. Ele procura garantir que são encontradas apenas 2 categorias, mas temos 2 livros com 2 categorias cada um, de modo que vão ser retornadas 4 categorias. Vamos resolver isso já, mas antes vamos ver o método tradicional de indicar que pessoa tem várias categorias através de livro, inserindo no arquivo do modelo de pessoa a seguinte linha:
1 ...
2 has_many :categories, through: :books
3 ...
Novamente, uma configuração bem simples, que nos dá as categorias da pessoa:
1 > Person.last.categories
2 Person Load (0.4ms) SELECT "people".* FROM "people" ORDER BY "people"."na\
3 me"
4 DESC LIMIT ? [["LIMIT", 1]] Category Load (0.1ms) SELECT "categories".* F\
5 ROM
6 "categories" INNER JOIN "books_categories" ON "categories"."id" =
7 "books_categories"."category_id" INNER JOIN "books" ON
8 "books_categories"."book_id" = "books"."id" WHERE "books"."person_id" = ?
9 [["person_id", 15]]
10 => #<ActiveRecord::Associations::CollectionProxy [#<Category id: 1, name:
11 "Tecnologia", created_at: "2017-03-12 14:02:51", updated_at: "2017-03-12
12 14:02:51">, #<Category id: 2, name: "Programação", created_at: "2017-03-12
13 14:02:51", updated_at: "2017-03-12 14:02:51">, #<Category id: 1, name:
14 "Tecnologia", created_at: "2017-03-12 14:02:51", updated_at: "2017-03-12
15 14:02:51">, #<Category id: 2, name: "Programação", created_at: "2017-03-12
16 14:02:51", updated_at: "2017-03-12 14:02:51">]>
Só que, como já identificamos no teste, vai trazer as categorias repetidas. Para
evitar isso, precisamos pegar os resultados distintos, e se alguém conhece SQL
por aí, sabe que isso podemos recuperar com uma cláusula DISTINCT, que é o que
vamos utilizar enviando através de uma lambda para a associação:
1 has_many :categories, -> { distinct }, through: :books
Pronto! Agora rodando os testes, vamos constatar que as categorias estão retornando corretamente, sem duplicação.
Essa associação ficou da seguinte forma, representada pelo traço pontilhado:
Acelerando as consultas nas associações
Temos alguns jeitos de utilizarmos as associações, sendo que estamos vendo até
agora o jeito padrão que já vem com o Rails, onde o ORM cuida de bastante
coisas para nós, mas onde também podemos dar uma mãozinha para que as coisas
fiquem mais eficientes.
Joins
Vamos imaginar que queremos descobrir as pessoas que tem livros associados. Um jeito de não fazer isso seria assim:
1 > Person.select { |person| person.books.count > 0 }
2 Person Load (0.1ms) SELECT "people".* FROM "people" ORDER BY "people"."nam\
3 e" ASC
4 (0.1ms) SELECT COUNT(*) FROM "books" WHERE "books"."person_id" = ? [["pe\
5 rson_id", 4]]
6 (0.1ms) SELECT COUNT(*) FROM "books" WHERE "books"."person_id" = ? [["pe\
7 rson_id", 3]]
Vejam que eu percorremos toda a tabela de pessoas e estamos fazendo várias
outras consultas para pegar cada pessoa e selecionar os livros de cada uma
delas. Ficou uma mistura de iteradores Ruby (o método select) com código SQL
(o where dentro de cada bloco). Podemos fazer bem melhor utilizando joins:
1 Person.joins(:books).distinct
2 Person Load (0.2ms) SELECT DISTINCT "people".* FROM "people" INNER JOIN
3 "books" ON "books"."person_id" = "people"."id" ORDER BY "people"."name" ASC
4 => #<ActiveRecord::Relation [#<Person id: 3, name: "Eustáquio Rangel de
5 Oliveira Jr.", email: "taq@bluefish.com.br", password_digest: ...
Bem melhor, não? Ali o INNER JOIN produzido pelo método joins já relacionou
as duas tabelas, fazendo o filtro de uma vez só no comando SQL, procurando
obrigatoriamente pessoas que tem livros. Vejam que utilizamos distinct no
final, para evitar registros duplicados se a pessoa tiver mais de um livro.
Includes
Temos um problema ali: e se precisarmos saber os títulos dos livros encontrados?
O resultado nos trouxe somente uma coleção de objetos tipo Person.
Vamos fazer um teste:
1 > Person.joins(:books).distinct.map { |person| { person.name => person.books.\
2 pluck(:title) } }
3 Person Load (0.3ms) SELECT DISTINCT "people".* FROM "people" INNER JOIN
4 "books" ON "books"."person_id" = "people"."id" ORDER BY "people"."name" ASC
5 (0.1ms) SELECT "books"."title" FROM "books" WHERE "books"."person_id" = ?\
6 [["person_id", 3]]
7 => [{"Eustáquio Rangel de Oliveira Jr."=>["Conhecendo Ruby", "Conhecendo o G\
8 it"]}]
Reparem que para cada pessoa, foi executada outra consulta para recuperar os
livros (e também o nome da pessoa, como chave da Hash) e retornar somente o
título. O que ocorre é o método joins estabelece o relacionamento, mas não
deixa disponivel os dados da outra associação, não faz eager
loading. Para
remediar isso, podemos utilizar o método includes.
Antes de aplicarmos o método includes na nossa consulta, vale mencionar que
ele também pode ser utilizado, além do eager loading, como um LEFT OUTER
JOIN:
1 Person.includes(:books).distinct.map { |person| { person.name => person.books\
2 .pluck(:title) } }
3 Person Load (0.1ms) SELECT DISTINCT "people".* FROM "people" ORDER BY "peo\
4 ple"."name" ASC
5 Book Load (0.4ms) SELECT "books".* FROM "books" WHERE "books"."person_id" \
6 IN (4, 3)
7 => [{"Ana Carolina"=>[]}, {"Eustáquio Rangel de Oliveira Jr."=>["Conhecendo \
8 Ruby", "Conhecendo o Git"]}]
Podemos ver que para as pessoas que não tinham livros, foi retornado uma coleção vazia e que ao invés de várias consultas aos livros (o que ocorreria se tivéssemos várias pessoas com livros) foi feita uma, especificando os ids de todas as pessoas.
Juntando joins e includes
Mas vamos voltar para resolver nosso problema de performance. Se pensarmos que
joins faz a associação com a outra tabela e includes faz o eager loading,
é só juntarmos os dois:
1 > Person.joins(:books).includes(:books).distinct.map { |person| { person.name\
2 => person.books.pluck(:title) } }
3 SQL (0.2ms) SELECT DISTINCT "people"."id" AS t0_r0, "people"."name" AS t0_\
4 r1,
5 "people"."email" AS t0_r2, "people"."password_digest" AS t0_r3,
6 "people"."born_at" AS t0_r4, "people"."admin" AS t0_r5, "people"."created_a\
7 t"
8 AS t0_r6, "people"."updated_at" AS t0_r7, "books"."id" AS t1_r0,
9 "books"."title" AS t1_r1, "books"."published_at" AS t1_r2, "books"."text" AS
10 t1_r3, "books"."value" AS t1_r4, "books"."person_id" AS t1_r5,
11 "books"."created_at" AS t1_r6, "books"."updated_at" AS t1_r7, "books"."stoc\
12 k"
13 AS t1_r8, "books"."lock_version" AS t1_r9 FROM "people" INNER JOIN "books" \
14 ON
15 "books"."person_id" = "people"."id" ORDER BY "people"."name" ASC
16 => [{"Eustáquio Rangel de Oliveira Jr."=>["Conhecendo Ruby", "Conhecendo o G\
17 it"]}]
Agora sim! Temos uma consulta única de onde são extraídas todas as informações
que precisamos. Vejam como o ORM altera o nome dos campos e sabe acessar cada
um de maneira transparente.
Pluck versus select
Vimos que utilizamos o método pluck. Esse método extrai somente os atributos
que indicamos, sem retornar um objeto da associação. Assim:
1 > Person.pluck(:name)
2 (0.3ms) SELECT "people"."name" FROM "people" ORDER BY "people"."name" ASC
3 => ["Ana Carolina", "Eustáquio Rangel de Oliveira Jr."]
Podemos indicar mais de um atributo:
1 > Person.pluck(:name, :email)
2 (0.2ms) SELECT "people"."name", "people"."email" FROM "people" ORDER BY "\
3 people"."name" ASC
4 => [["Ana Carolina", "carol@bluefish.com.br"], ["Eustáquio Rangel de Oliveir\
5 a Jr.", "taq@bluefish.com.br"]]
Vejam que são retornados POROs (Plain Old Ruby Objects), no formato de
Arrays.
O método select, por sua vez, retorna um objeto com apenas os atributos
selecionados preenchidos, ou seja, não vamos conseguir acessar os outros.
Assim, isso funciona:
1 > Person.select(:name, :email)
2 Person Load (0.1ms) SELECT "people"."name", "people"."email" FROM "people"
3 ORDER BY "people"."name" ASC => #<ActiveRecord::Relation [#<Person id: nil,
4 name: "Ana Carolina", email: "carol@bluefish.com.br">, #<Person id: nil, na\
5 me:
6 "Eustáquio Rangel de Oliveira Jr.", email: "taq@bluefish.com.br">]>
7
8 > Person.select(:name, :email).first.name
9 Person Load (0.3ms) SELECT "people"."name", "people"."email" FROM "people"
10 ORDER BY "people"."name" ASC LIMIT ? [["LIMIT", 1]]
11 => "Ana Carolina"
12
13 > Person.select(:name, :email).first.email
14 Person Load (0.2ms) SELECT "people"."name", "people"."email" FROM "people"
15 ORDER BY "people"."name" ASC LIMIT ? [["LIMIT", 1]]
16 => "carol@bluefish.com.br"
Já isso, não:
1 > Person.select(:name, :email).first.admin
2 Person Load (0.1ms) SELECT "people"."name", "people"."email" FROM "people"
3 ORDER BY "people"."name" ASC LIMIT ? [["LIMIT", 1]]
4 ActiveModel::MissingAttributeError: missing attribute: admin
Criando um outro tipo de associação um-para-um
Podemos utilizar um outro tipo de associação um-para-um, com uma semântica
diferente, utilizando has_one. Enquanto que em belongs_to a foreign key
estava no modelo que especificava a relação, em has_one está no outro
modelo.
Podemos, por exemplo, dizer que as pessoas da nossa aplicação tem cada uma, uma imagem. Para isso, vamos aprender como fazer upload de arquivos.