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.


Gravatar-aktivert. Les mer om gravatar.
E-postadressen vil ikke vises på siden