Datamapper - asocjajcje polimorficzne (i nie tylko)
04 marca 2009Większości znających Ruby on Rails temat "Polymorphic Associations" nie jest zapewne obcy ( z wiki railsów nie działa). W skrócie chodzi o relacje odnoszącą się do obiektów z różnych klas. (Nie umiem tłumaczyć, ale skoro nadal to czytasz to pewnie wiesz o.c.b.)
Przejdźmy do konkretów. O ile w ActiveRecord wystarczy dodac :polymorphic => true to w api Datamappera nie znalazłem takie opcji. Jednak nic nie stoi na przeszkodzie aby napisać to samemu.
Posłużę się pewnym przykładem: chcę aby każda operacja stworzenia nowego obiektu została zapisana. Do tego celu zostanie wykorzystany model Event.
Na początek dwa proste modele, których tworzenie będziemy logować.
class Project include DataMapper::Resource property :id, Serial property :name, String after :create do Event.create :comment => "Project created", :related => self end end class Ticket include DataMapper::Resource property :id, Serial property :title, String after :create do Event.create :comment => "Ticket created", :related => self end end
W każdym z nich został dodany hook (after :create ... ), który będzie wywołany w momencie utworzenia obiektu.
Teraz czas na model Event
class Event include DataMapper::Resource property :id, Serial property :comment, String property :created_at, DateTime property :related_id, Integer property :related_class, String def related=(object) @related = object self.related_id = @related.id self.related_class = @related.class.to_s end def related @related ||= Kernel.const_get(related_class).get(related_id) end end
Pole related_id przechowuje id obiektu a related_class nazwę jego klasy. Istotnym elementem jest tutaj
Kernel.const_get(related_class).get(related_id)Metoda Kernel#const_get zwraca stałą (w naszym przypadku klasę) na podstawie podanej nazwy. (Innym sposobem byłoby użycie eval)
Zobaczmy co nam to wszystko dało:
@project1 = Project.create :name => 'Project 1' # => #<Project id=1 name="Project 1"> @ticket1 = Ticket.create :title => 'Ticket 1' # => #<Ticket id=1 title="Ticket 1"> Event.all # => [#<Event id=1 comment="Project created" related_id=1 related_class="Project">, #<Event id=2 comment="Ticket created" related_id=1 related_class="Ticket">] Event.get(1).related # => #<Project id=1 name="Project 1"> Event.get(1).related.name # => "Project 1" Event.get(2).related # => #<Ticket id=1 title="Ticket 1"> Event.get(2).related.title # => "Ticket 1"
Aby umilić sobie trochę życie możemy dorzucić jeszcze method_missing do klasy Event
def method_missing(method_name, *args, &block) if method_name.to_s == self.related_class.downcase return related end super end
(Więcej na temat method_missing u radarka)
Pozwoli to na użycie:
Event.get(1).project # => #<Project id=1 name="Project 1"> # ale już nie Event.get(1).ticket # => undefined method `ticket' for #<Event:0x111a2c8> (NoMethodError)
Jednej rzeczy brakuje - lista eventów dla danego obiektu:
class Project include DataMapper::Resource property :id, Serial property :name, String after :create do Event.create :comment => "Project created", :related => self end def events Event.all(:related_id => self.id, :related_class => "Project") end end class Ticket include DataMapper::Resource property :id, Serial property :title, String after :create do Event.create :comment => "Ticket created", :related => self end def events Event.all(:related_id => self.id, :related_class => "Ticket") end end @project1.events # => [#<Event id=1 comment="Project created" related_id=1 related_class="Project">] @ticket1.events # => [#<Event id=2 comment="Ticket created" related_id=1 related_class="Ticket">]
Wszystko pięknie działa, jednak niektórzy mogli zauważyć, że część kodu się powtarza - zróbmy coś z tym!
Hook after :create oraz metode events można by wyrzucić do osobnego modułu a następnie dołączyć go do naszych modeli
module Eventable def self.included(base) base.class_eval do after :create do Event.create :comment => event_comment, :related => self end end end def events Event.all(:related_id => self.id, :related_class => self.class.to_s) end end class Project include DataMapper::Resource include Eventable property :id, Serial property :name, String def event_comment "Project created" end end class Ticket include DataMapper::Resource include Eventable property :id, Serial property :title, String def event_comment "Ticket created" end end
Metoda event_comment została dodana aby było możliwe podanie rożnego komentarza do eventu dla każdego modelu z osobna.
I takim oto dość prostym sposobem otrzymujemy przejrzysty i całkiem sprytny kawałek kodu :)
1 komentarz
Jest plugin dm_is_polymorphic (czy jakoś tak), ale w momencie kiedy go sprawdzałem, to nie bardzo chciał działać - próbowałem go poprawić, ale poddałem się jak się okazało, że nie działa z przyczyny jakichś błędów w datamapperze. Zastanawiam się czy teraz działa (jak będziesz coś robił z powiązaniami polimorficznymi i go sprawdzisz to daj znać, nie będę musiał sam sie wysilać ;-)
Jeżeli chodzi o polimorfizm, to w światku datamappera nie bardzo lubią tą technikę, pytałem się o to kiedyś na grupie datamappera: http://www.mail-archive.com/datamapper@googlegroups.com/msg00798.html
Jest tam link do dm-is-commentable i w opisie możesz poczytać o dm-is-remixable - jednej z alternatyw polumorfizmu. Całkiem fajne podejście ale ma kilka ograniczeń i czasami tak czy siak trzeba użyć polimorfizmu.