Entendendo os contadores no Ruby on Rails
Leia em 2 minutos
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.