Entendendo os contadores no Ruby on Rails
02 de Dezembro de 2008
Aparentemente, os counters são as coisas que mais geram confusão para quem está começando no Ruby on Rails. Afinal, existem diversas maneiras de se obter o total de itens de uma coleção. Imagine que você possua dois modelos:
class User < ActiveRecord::Base
has_many :things
end
class Thing < ActiveRecord::Base
belongs_to :user
end
Vamos popular o banco de dados com alguns registros. À partir do console, execute os comandos abaixo.
user = User.create
100.times { user.items.create }
A primeira maneira e que poderá causar problemas é carregando todos os itens da coleção em memória,
para depois usar o método size. Ela é ruim se você possuir uma base de dados com muitos registros.
Pensando bem, ela é ruim sempre!
user.things.all.size
# => SELECT * FROM "things" WHERE ("things".user_id = 1)
Você também pode utilizar o método length. Ele funciona como o método acima, só que de maneira mais automática.
user.things.length
# => SELECT * FROM "things" WHERE ("things".user_id = 1)
Outra maneira é utilizar o método size diretamente na associação.
Ela irá gerar uma consulta SQL para fazer a contagem.
user.things.size
# => SELECT count(*) AS count_all FROM "things" WHERE ("things".user_id = 1)
Assim como o método size, o método count também irá gerar uma consulta
SQL para fazer a consulta. A diferença é que você pode enviar condições, já que o método size
não permite que você faça isso.
user.things.count
# => SELECT count(*) AS count_all FROM "things" WHERE ("things".user_id = 1)
user.things.count(:conditions => ["created_at > ?", 2.days.ago])
# => SELECT count(*) AS count_all FROM "things" WHERE ("things".user_id = 1 AND (created_at > '2008-11-30 10:26:05'))
Estamos no caminho. A saída, pelos exemplos acima, é utilizar sempre o método count.
Uma grande desvantagem, no entanto, é que isso trará problemas de performance quando você possuir uma
base de milhões de registros fazendo counts para todo lado. Para nossa sorte, o Rails possui
um recurso chamado counter cache.
O counter cache nada mais é que um campo na tabela que irá refletir o total de itens de um relacionamento, evitando uma consulta SQL para isso. Toda vez que um registro é criado, este campo é incrementado; quando um registro é destruído, esse campo é decrementado. A única desvatangem do counter cache é que ele só pode ser utilizado em relacionamentos.
Para adicionar um counter cache, basta adicionar um campo que segue o padrão [associação]_count.
Lembre-se de atualizar a contagem após adicionar o campo.
class AddThingsCountToUser < ActiveRecord::Migration
def self.up
add_column :users, :things_count, :integer, :default => 0, :null => false
User.all.each do |user|
User.update_counters user.id, :things_count => user.things.count
end
end
def self.down
remove_column :users, :things_count
end
end
Para que o Rails saiba que este campo existe, você deve adicionar a opção :counter_cache => true.
class Thing < ActiveRecord::Base
belongs_to :user, :counter_cache => true
end
Agora, toda vez que um registro for criado ou destruído, o Rails irá incrementar/decrementar este campo automaticamente.
E toda vez que você utilizar o método count, ele irá utilizar o valor armazenado neste campo. Vamos criar um
novo registro para ver a consulta gerada:
user.things.create
# => INSERT INTO "things" ("updated_at", "user_id", "created_at") VALUES('2008-12-02 10:59:25', 1, '2008-12-02 10:59:25')
# => UPDATE "users" SET "things_count" = COALESCE("things_count", 0) + 1 WHERE ("id" = 1)
Para saber quantas coisas um usuário possui, você deve utilizar o método size.
Se você utilizar o método count, a consulta será feita diretamente no banco.
Na dúvida, utilize o método things_count.
user.things.size
# => 101
user.things.count
# => 101
# => SELECT count(*) AS count_all FROM "things" WHERE ("things".user_id = 1)
user.things_count
# => 101
Se precisa trabalhar com contadores personalizados (que dependem de condições para
serem incrementados ou não), pode utilizar callbacks no seu modelo juntamente
com os métodos increment_counter e decrement_counter.
Imagine que o modelo User possua um contador chamado cool_things_count
que refletirá o total de registros com o atributo kind com o valor cool.
class Thing < ActiveRecord::Base
belongs_to :user, :counter_cache => true
after_save :increment_cool_things_count
after_destroy :decrement_cool_things_count
private
def increment_cool_things_count
User.increment_counter(:cool_things_count, user_id) if kind == 'cool'
end
def decrement_cool_things_count
User.decrement_counter(:cool_things_count, user_id) if kind == 'cool'
end
end
É importante notar que você não pode usar nenhum relacionamento quando estiver tratando o
callback after_destroy, já que o objeto não existe mais.
- Permalink
- Trackback
- Feed dos comentários
- Ao som de: The Killers – Mr. Brightside
- Posts relacionados
- Conhecendo as opções de cache do Rails 2.1
Textos escritos por
Comentários #
Assim fica difícil acrescentar algo ao texto, ele está completo! ;)
Obrigado pelo artigo!
Abraços
Muito massa e muito bem explicado como sempre, valeu Nando.
Oi,
Achei muito interessante, rails sempre pensando num passo a frente.
Mas eu sempre fico meio com medo de usar essas propriedades de counter, pois as vezes eu não sei se ela usa exclusão mútua para essa propriedade.
Eu ainda não tive tempo de ver no código rails se ele faz exclusão mútua, vc sabe me dizer?
[...] Leia mais direto na fonte: simplesideias.com.br [...]
Muito bom! Poderia dar um exemplo simples de como utilizar essa técnica para counters dependentes de condição? por exemplo dependendo do status de "thinks",
Valeu!
Antonio, atualizei o post com um exemplo.
Show de bola Nando, no entanto, existe um pequeno problema (nao no seu código) mas como o rails trabalha com o counter caso nao seja o padrao.
Vou colocar aqui o texto com o código que mandei para os desenvolvedores do core do Rails:
Problems with :counter_cache
I found this 'problem' (I think) when was having two self relation on the table with the same table, for instance:
Having a model like that:
create_table :codes do |t|
t.string :code_key, :null => false, :limit => 25
t.string :file_name, :null => false, :limit => 200, :default => ""
t.integer :language_id, :null => false, :default => 10
t.text :code, :null => false
t.string :comment, :limit => 250, :default => ""
t.boolean :private, :null => false, :default => false
t.integer :reply_id
t.integer :reply_id_count, :integer, :default => 0, :null => false
t.timestamps
end
In the class:
# this is a self rellation….
belongs_to :reply, :class_name => "Code", :foreign_key => "reply_id", :counter_cache => true
I'm willing to have with the self rellation the counter for that.
But a problem occours when I'm try to make an insert:
SQLite3::SQLException: no such column: codes_count: UPDATE "codes" SET "codes_count" = COALESCE("codes_count", 0) + 1 WHERE ("id" = 1)
I was surprised that the rails is trying to update a column called: 'codes_count', but I don't have a column with this name!
It is using the :class_name as a prefix of the collumn _count.
[...] Leia mais deste post no blog de origem: Clique aqui e prestigie o autor [...]
Muito bom, parabéns pelo post. Muito bem explicado.
Muito bom… Tirou uma pulga atrás da minha orelha!
Falou tudo que eu queria saber e ninguem contava..
Deixe um comentário