Wczoraj pisząc projekt, wpadłem na pomysł wersjonowania tekstów. Postanowiłem że każda zmiana zawartości musi być równoznaczna ze stworzeniem kopii zapasowej. Funkcjonalność tą postanowiłem wydzielić do pluginu, z racji tego że może przydać się w innych modelach.
Jak to zrealizowałem? Postanowiłem skorzystać z pluginu acts_as_tree. Implikuje to niestety to, że jeśli chcemy skorzystać ze struktury drzewa i backupu to niestety musimy poszukać innego pluginu ;)
Struktura całego mechanizmu ma następującą postać:
Tekst (czy też inny obiekt)
- Kopia 1
- Kopia 2
- Kopia 3
- etc
Kopie wykonywane są w momencie aktualizacji modelu. Wykorzystany jest do tego callback before_update.
Interfejs pluginu składa się z trzech metod instancyjnych i jednej klasowej. Metody instancyjne to:
- backup! - Robi kopie samego siebie do archiwum
- restore!(id) - przywraca kopię o wybranym id lub też jesli wywołane na kopii to przywraca kopię z której wywołaliśmy
- cleanup! - usuwa wszystkie kopie poza najnowszą
Metoda klasowa:
- cleanup! - usuwa wszystkie kopie ze wszystkich elementów, pozostawiając tylko po 1 na element
Problemem na jaki natrafiłem, przy tworzeniu tego pluginu, był Paperclip. Wywołując metodę clone lub po prostu:
target.update_attributes(source.attributes)
Obiekt paperclipa nie było kopiowany. Aby to obejść zastosowałem inne rozwiązanie które wykrywa czy dany obiekt posiada w bazie elementy charakterystyczne dla paperclipa, czyli:
- _content_type
- _file_size
- _file_name
Na podstawie przedrostka rozpoznaje jak te obiekty się nazywają, następnie wywołując dla nich metodę przypisania:
paperclip_obj.each { |obj| sym = "#{obj}=".to_sym target.send(sym, self.send(obj.to_s)) }
Dzięki czemu, kopiowane są wszystkie atrybuty oraz obiekty paperclipa (niezależnie od ich ilości). Dzięki czemu archiwizujemy nie tylko sam przykładowo tekst, ale i powiedzmy miniaturkę. Oczywiście ma to swoje minusy - ilość miejsca zajmowanego przez kopie rośnie bardzo szybko. Na szczęście jest wyżej wspomniana metoda cleanup!
A oto jak zastosować plugin:
Przykładowa klasa:
class CoolClass < ActiveRecord::Base acts_as_tree acts_as_archivable end
Następnie zrobienie 10 kopii w odstępach 1sekundowych:
model = CoolClass.create 10.times { model.content += "a" sleep(1) model.save }
I już mamy nasz model + kopię zapasową każdej wersji.
Teraz przywróćmy powiedzmy drugą wersję (czyli tą "prawie" najdawniejszą):
model.children.second.restore! # Musimy przeładować model bo zmieniają się jego parametry model.reload
Z racji tego że zmieniają się parametry obiektu w bazie, musieliśmy wykonać przeładowanie.
Przywrócenie można zrobić także na drugi sposób, wywołując metodę restore! na obiekcie w stosunku do którego chcemy wykonać przywrócenie:
model.restore(model.children.second.id)
Jeśli tak wywołamy przywracanie, nie musimy wywoływać metody reload, ponieważ wywoływana jest automatycznie z poziomu metody restore!
Aby wyczyścić kopie zapasowe, pozostawiając tylko najnowszą, wykonamy:
model.cleanup!
Lub też jeśli chcemy posprzątać wszystkie modele:
CoolClass.cleanup!
Tyle :) aby wyświetlić kopie zapasowe, wystarczy przeiterować dzieci w taki sposób:
model.children.each { # Jakiś kod :) }
Plugin do pobrania tutaj: acts_as_archivable