Habtm i counter_cache

Mam dwa modele: Category i Article w relacji habtm. Ponieważ w wielu miejscach aplikacji, przy wyświetlaniu kategorii potrzebuję informacji o ilości artykułów związanych z tą kategorią, pomyślałem o counter_cache ale przy habtm to nie działa. W przypadku dodawania nowego artykułu poradziłem sobie choć nie do końca mnie to rozwiązanie zadowala ale działa. Ale jak aktualizować ilość artykułów w przypadku kasowania artykułu? after_destroy nie nadaje się bo już nie znam kategorii, before_destroy też nie bo kasowany artykuł nadal będzie wliczany.

To co mam do tej pory:

[code=ruby]class Article < ActiveRecord::Base
has_and_belongs_to_many :categories

after_save :update_categories_counter_cache

def update_categories_counter_cache
self.categories.each { |c| c.update_count() } unless self.categories.empty?
end
end

class Category < ActiveRecord::Base
has_and_belongs_to_many :articles

def update_count()
update_attribute(:articles_count,self.articles.length)
end
end[/code]

Najpiękniej nie jest, ale zawsze coś :wink:

[code=ruby]class Category < ActiveRecord::Base
has_and_belongs_to_many :articles

def update_articles_count
update_attribute :articles_count, self.articles.count
end
end

class Article < ActiveRecord::Base
has_and_belongs_to_many :categories

after_save :update_counters

before_destroy :cache_categories
after_destroy :update_counters

private

def cache_categories
@cached_categories = self.categories.all
end

def update_counters
(@cached_categories || self.categories).each(&:update_articles_count)
end
end[/code]
przechodzące testy:

[code=ruby] it “should update counters of categories after save” do
category = Category.create!(:name => “Category”)
article = Article.create!(@valid_attributes)

article.categories = [category]
article.save!

category.reload.articles_count.should == 1

end

it “should update counters of categories after destroy” do
category = Category.create!(:name => “Category”)
article = Article.create!(@valid_attributes)

article.categories = [category]
article.save!
article.destroy

category.reload.articles_count.should == 0

end[/code]

Fajne rozwiązanie, ja bym to jeszcze wyciągnął do modułu :slight_smile:

A pewnie, można by było się zabawić w przesłonięcie habtm i dołożyć dodatkową opcję.

Jesteś chętny ;)?


Tylko, żeby te 2 testy przechodziły! :slight_smile: Miłego forkowania :wink:

[quote=sevos] (@cached_categories || self.categories).each(&:update_articles_count)
[/quote]
Nie znam takiej konstrukcji, mógłbyś wyjaśnić co ona robi albo dać linka gdzie można poczytać o takich podobnych Ruby’owych sztuczkach ?

Tutaj są dwa zaklęcia właściwie.
Pierwsze to zwykły or:

(@cached_categories || self.categories)

Czyli: weź do łapy @cached_categories lub (gdy jest nilem, a tak jest gdy jest on niezainicjowany) weź do łapy wynik metody self.categories.

drugie to wywołanie each na wybranym obiekcie i omawiany miliony razy na tym forum symbol#to_proc - wystarczy poguglać.

A jak ktoś chce sztuczki, to polecam użyć google readera - po dodaniu kilku oficjalnych blogów po jakimś czasie sam zacznie proponować inne blogi do subskrybcji.

A patrząc na całą linijkę globalnie:
Uruchamia metodę update_articles_count na każdym obiekcie należącym do arraya zapisanego w @cached_categories lub, gdy ta zmienna instancyjna jest niezainicjowana (albo jest po prostu nilem), na każdym obiekcie zwróconym przez self.categories.

dzieki

Dziękuję za tą odpowiedź.

Pozostaje jeszcze problem after_save. Zastosowanie takiego rozwiązania spowoduje przeliczenie ilości artykułów po każdej modyfikacji artykułu, zarówno po dodaniu jak też aktualizacji. after_create nie rozwiązuje problemu bo artykułu jeszcze nie ma w bazie danych. W jaki sposób sprawdzić, że metoda wywołana przez after_save dotyczy nowego rekordu a nie aktualizacji (bo tylko wówczas trzeba przeliczyć artykuły)?

można by if self.new_record?

Tak naprawdę trzeba sprawdzać, czy dodano/usunięto przypisanie artykułu do kategorii. HATBM jest tutaj niewystarczający i skłoniłbym się do użycia hasm_nay … :through => …

Rozwinięcie przez Ciebie problemu uświadomiło mi, że tak naprawdę potrzebujesz modelu pośredniego (łączącego) zawierającego swoją logikę biznesową:

[code=ruby]class ArticleCategorization < ActiveRecord::Base
belongs_to :article
belongs_to :category

after_create do |r|
r.category.update_articles_count
end

after_destroy do |r|
r.category.update_articles_count
end
end[/code]
Pełny kod z testami tutaj:

Czy są jakieś znaczne różnice w wydajności między HABTM a has many :through ?

Myślę, że nie ma. HABTM jest po prostu prostszym zapisem (bez zdefiniowanego explicite modelu łączącego) relacji obustronnej has_many :through. Zauważ, że w obu implementacjach liczba tabel jest taka sama, więc zapytania do bazy są IMHO bardzo podobne.
W przypadku jakichkolwiek bardziej skomplikowanych relacji należy po prostu używać modelu łączącego i tam umieszczać logikę.

Mam problem z powyzszym rozwiązaniem mianowicie modele Article oraz Category są w relacji habtm.
Article:

[code]class Article < ActiveRecord::Base

before_destroy :cache_categories
after_save :update_counters
after_destroy :update_counters

private

def cache_categories
@cached_categories = self.categories.all
end

def update_counters
(@cached_categories || self.categories).each(&:update_articles_count)
end

end[/code]
model category:

[code]class Category < ActiveRecord::Base
has_and_belongs_to_many :articles

def update_articles_count
update_attribute :articles_count, self.articles.count
end
end[/code]
Przy tworzeniu artykułu i modyfikacji kategorii (można dodać artykul max do 3 kategorii a min do 1)

[code]<% semantic_form_for(@article, :html => {:multipart => true}) do |f| %>

<% f.inputs :name => "Treść i kategoria artykułu" do %>
  <%= f.input :title, :label => "Tytuł" %>
  <%= f.label(:wstęp_do_artykułu, :synopsis, :required => true) %>
  <%= f.tinymce_managed :synopsis, :width => '600px', :height => '200px' %>
  <%= f.label(:treść_artykułu, :body, :required => true) %>
  <%= f.tinymce_managed :body, :width => '600px', :height => '400px' %>
  <%= f.input :published, :label => "Publikować?", :as => :radio %>
  <%= f.input :categories, :label => "Kategoria", :as => :check_boxes, :collection => nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" }, :for => :categories %>
<% end %>

<% f.inputs :name => "Autorzy i Fotografowie Artykułów" do %>
  <%= f.input :authors, :label => "Autorzy", :as => :check_boxes, :collection => Author.all %>
  <%= f.input :photographers, :label => "Fotografowie", :as => :check_boxes, :collection => Photographer.all %>
<% end %>
<% f.buttons do %>
  <%= f.commit_button :label => "Zapisz" %>
<% end %>

<% end %>[/code]
Występuje przy modyfikacjach kategorii artykulu pewien problem w wierszu articles_count ponieważ nie aktualizuje się on prawidłowo. Po pierwsze licznik ten się nie zmniejsza ale tylko zwiększa i przy np. przy zmianie z jednej kategorii na np 3 aktualizuje sie np tylko jeden wiersz a nie trzy. W czym może istnieć problem?

Witam, czy jest ktoś w stanie mi pomóc w tej kwestii?

@wlodi: wykorzystaj podane wyżej przez @sevos’a testy. Jeśli przechodzą to zobacz jak te dane wyglądają w bazie, zainteresuj się też screencastem:
http://railscasts.com/episodes/17-habtm-checkboxes