Propojení objektů je v Active Record velmi jednoduché a pokud znáte ORM, zabere vám pochopení několik minut. Nemá smysl přepisovat napsané a zájemce odkazuji na ActiveRecord::Associations::ClassMethods. Active Record má pro asociace opravdu hodně možností, ale pro základní použití vám bude stačit jen minimum z nich.
Pro jednoduchost si projdeme vztahy one-to-one a one-to-many.
One to one
“Kontakt má jednu adresu” definujeme pomocí metod modulu ActiveRecord::Associations::ClassMethods a to has_one a belongs_to. Netřeba se obávat, nic nemusíme importovat, includovat, requirovat ani nic podobného. Vše potřebné již udělal Active Record. Rozdíl mezi has_one a belongs_to metodami je v tom, že strana, která má belongs_to obsahuje cizí klíč v db tabulce. Teoreticky nemusíme vůbec has_one definovat, ale potom se připravíme o možnost jednoduše objektově dosáhnout na adresu z kontaktu.
class Contact < ActiveRecord::Base
has_one :address
end
class Address < ActiveRecord::Base
belongs_to :contact
end
Databázové tabulky:
CREATE TABLE contacts (
id integer NOT NULL PRIMARY KEY,
name varchar(255)
);
CREATE TABLE addresses (
id integer NOT NULL PRIMARY KEY,
contact_id integer NOT NULL,
address varchar(255)
);
Ukládáme:
c = Contact.new(:name=>'Jirka Hradil', :address=>Address.new(:address=>'Valhalla'))
c.save
Tento kód nám vygeneruje potřebné inserty:
BEGIN
INSERT INTO "contacts" ("name") VALUES('Jirka Hradil') RETURNING "id"
INSERT INTO "addresses" ("address", "contact_id") VALUES('Valhalla', 1) RETURNING "id"
COMMIT
Všimněte si automatické transakce okolo obou insertů. Vzpomínáte si, že bychom je někde definovali? Ne, Active Record to za nás udělal automaticky. Na kontaktu jsme zavolali metodu save, což je 1 “atomické” volání metody, tudíž se vše obalilo do 1 transakce.
Samozřejmě bychom mohli nejdřív uložit kontakt a teprve pak adresu, pak bychom si ovšem hranice transakce museli řídit sami. To si ukážeme příště.
A teď vyhledáváme:
c = Contact.first #najdeme první kontakt
puts c.address #a vypíšeme jeho adresu
Což nám generuje selecty:
SELECT * FROM "contacts" LIMIT 1
SELECT * FROM "addresses" WHERE ("addresses".contact_id = 1) LIMIT 1
Tady si všimněte, že nikde nedefinuji eager, lazy ani nic podobného. Active Record standardně používá lazy inicializaci a prostě pokud mu chybí adresa, tak si ji při prvním použití dotáhne přes samostatný select. Podle mě v Active Record 2.3.x neexistuje default způsob, jak zajistit, aby se při výběru kontaktu VŽDY joinovala automaticky jedna jeho adresa (lze však použít Contact.find_by_sql a select si napsat dle libosti). Eager inicializace se používají při vztahu has_many a has_and_belongs_to_many a fungují na principu dotažení všech podřízených entit najednou v samostatném subselectu, jak si ukážeme.
One to many
“Kontakt má hodně adres” definujeme pomocí has_many a adresy přepíšeme do množného čísla na addresses.
class Contact < ActiveRecord::Base
has_many :addresses
end
Třída Address zůstává stejná (má jen belongs_to), stejně tak databázové schéma.
Teď uložíme kontakt s více adresami najednou. Všimněte si použití slova addresses jako množného čísla. Adresy jsou rovněž pole, nikoli samostatný objekt:
c = Contact.new(:name=>'Jirka Hradil', :addresses=>[Address.new(:address=>'První adresa'), Address.new(:address=>'Druhá adresa')])
c.save
Vygenerované inserty:
BEGIN
INSERT INTO "contacts" ("name") VALUES('Jirka Hradil') RETURNING "id"
INSERT INTO "addresses" ("address", "contact_id") VALUES('První adresa', 2) RETURNING "id"
INSERT INTO "addresses" ("address", "contact_id") VALUES('Druhá adresa', 2) RETURNING "id
COMMIT
Opět, vše automaticky zabaleno do transakce, protože jsme volali jen 1 “atomické” save.
Vyhledáváme:
c = Contact.first #najdeme první kontakt
puts c.addresses #pole adres
Vygenerované selecty:
SELECT * FROM "contacts" LIMIT 1
SELECT * FROM "addresses" WHERE ("addresses".contact_id = 1)
Lazy vs eager
Active Record je při načítání podřízených objektů velmi chytrý. Zapomeňte na nějakou LazyInitializationException nebo podobné ptákoviny, které vás jen stojí spoustu času. Podřízené objekty se natahují vždy přes samostatný select.
Contact.all.each {|c| puts c.addresses} # vypíšeme si všechny adresy
Generované selecty:
SELECT * FROM "contacts"
SELECT * FROM "addresses" WHERE ("addresses".contact_id = 1)
SELECT * FROM "addresses" WHERE ("addresses".contact_id = 2)
…což nám vede ke známému “N+1″ select problému - máme 2 adresy a udělají se celkem 3 dotazy.
Eliminace je jednoduchá pomocí atributu :include
Contact.all(:include=>:addresses).each {|c| puts c.addresses}
Pak se selecty transformují do 1+1, kdy je jeden select na všechny kontakty a další select na všechny adresy kontaktů, načtených v předchozím selectu:
SELECT * FROM "contacts"
SELECT "addresses".* FROM "addresses" WHERE ("addresses".contact_id IN (1,2))
Jednoduché a efektivní.