Magi i Rails med composed_of
Rails lar deg lagre sammensatte dataobjekter i en modell med metoden composed_of. Denne artikkelen gir noen eksempler på hvordan du kan bruke dette til å få til ganske avanserte ting i Rails-programmet ditt.
I dokumentasjonen for composed_of står det
Adds reader and writer methods for manipulating a value object
Det helt grunnleggende tilfellet er der du har en Person-klasse som har et adresseobjekt tilknyttet seg. Man kan selvsagt ha en egen addresses-tabell der man lagrer adressen og så bruker has_one :address for å koble personen med adressen.
En annen løsning er å ha et Address-objekt som er en helt vanlig klasse, men som (tilfeldigvis) lagrer dataene sine i people-tabellen. La oss si at en adresse består av en gateadresse og et postnummer. Dette kan man kode slik i Rails:
class Person < ActiveRecord::Base
composed_of :address, :class_name => "Address",
:mapping => [
[:address_street, :street],
[:address_postal_code, :postal_code]]
end
class Address
attr_reader :postal_code, :address
def initialize(street, postal_code)
@street = street
@postal_code = postal_code
end
end
Det som her skjer er at to kolonner i people-tabellen din, address_street og address_postal_code settes sammen til et Address-objekt som du bruker i Ruby-koden din istedet for å manipulere disse feltene direkte:
me = Person.find_by_name("marius")
me.address = Address.new("Nonnegata 21","0656")
me.save
Okay, dette virker kanskje som pirkete objektorientert idealisme, men du kan faktisk bruke dette til noe nyttig også. La oss si at du ønsker å også kunne hente ut poststed til en norsk adresse. La oss sette opp følgende krav:
- Address-klassen din skal ikke lagre poststedet, men hente dette fra en egen tabell i databasen
- Person-objekter skal ikke være gyldige med mindre de har et gyldig postnummer (eller riktigere: et Address-objekt med et gyldig postnummer)
For å få dette til lager vi en egen ActiveRecord-klasse (med en tilhørende tabell) for poststeder (Zip). Vi kan laste ned en fil som inneholder alle norske postnummer og -steder fra Posten og så lage en Rake-task som bruker denne fila til å oppdatere zips-tabellen. Du har da en Zip-klasse som kan se omtrent sånn ut:
class Zip < ActiveRecord::Base validates_uniqueness_of :number end
Om du så kobler sammen Address og Zip-modellene dine for eksempel slik:
class Address < ActiveRecord::Base
def postal_place
z = Zip.find_by_number(postal_code)
return z.place.chars
end
end
vil du kunne hente ut poststed for en adresse slik:
me = Person.find_by_name("marius")
me.address = Address.new("Nonnegata 21","0656")
me.address.postal_place => # OSLO
Vi sukrer pillen litt til ved å la deg få en snasen måte å representere adresser på i viewet ditt:
class Address < ActiveRecord::Base
def to_s
"#{street}, #{postal_code} #{postal_place}"
end
end
Så vil du kunne skrive ut en adresse i viewet ditt på den mest naturlige måten:
me = Person.find_by_name("marius")
me.address = Address.new("Nonnegata 21","0656")
puts me.address => # Nonnegata 21, 0656 Oslo
Men vi lovet mer: nemlig at en Person måtte ha en gyldig adresse for å kunne lagres. Her skal vi rett og slett basere oss på duck-typingen som ligger i Ruby. Vi skal først bruke validates_associated metoden for å si at en Person skal validere sin tilhørende Address:
class Person < ActiveRecord::Base validates_associated :address end
Det har ikke noe å si om Address-klassen ikke er en ActiveRecord-klasse, det som skjer er at metoden valid? blir kalt på det tilhørende Address-objektet. Dette kan vi implementere slik:
class Address
def valid?
return !Zip.find_by_number(postal_code).nil?
end
end
Og dermed har vi sørget for at en Person bare er gyldig dersom dens tilhørende Address er gyldig også.
Neste gang vil jeg skrive om hvordan du kan bruke composed_of til å lagre penger i databasen på en fornuftig måte ved å bruke et eget Money-objekt.
