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)

  1. Kopia 1
  2. Kopia 2
  3. Kopia 3
  4. 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:

  1. _content_type
  2. _file_size
  3. _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