Active Record a propojení objektů
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í.
(Začal jsem se RoR zabývat a na googlu jsem hledal nějaké blogery co o RoR píší, a našel jsem tebe. Píšeš dobře. Budu-li mít možnost, doporučím tě :-))
Áááá, chtěl bych si vyplakat srdce!!! Udělám si model nějaké tabulky, kupříkladu post. Vytvoří se mi post.rb což je v pořádku a v migračním souboru se mi vytvoří create_table :posts |t|. Co je to za krávovinu? Proč mi tam rails cpou to s? proč mi i mění na y aby to bylo správně anglicky? Nedá se to nějak vypnout?
Přináší mi to (zatím) spoustu komplikací. Migrační soubory si zatím edituji sám, protože jsem do hloubky migrace zatím neproniknul…
Comment od smonty on 15. 8. 2010 at 20:09
@smonty: Pokud uděláš model post (příspěvek), slovo post se převede na množné číslo přes pluralize a výsledkem je tabulka posts (příspěvky). To je přece (anglicky) správně - “příspěvek” uložíš do “příspěvků”. Myslím, že vím, co řešíš a zkoušel jsem si taky Rails ohnout podle sebe - přinutit je, aby nepoužívaly v názvu tabulky množné číslo, apod. Jenže je s tím pak víc potíží než užitku. Railsy jsou o konvencích. Pokud si na ně zvykneš, vše pak funguje neuvěřitelně dobře a rychle. Zkus se ohnout sám, ne si ohýbat Rails.
Přeji hodně zdaru, dej pak vědět, jak pokračuješ.
Comment od Jiří Hradil on 15. 8. 2010 at 20:54
jsem to jelen, nemohl jsem si vzpomenou jak se ten gramatický jev jmenuje.
:-D ale mám-li to celé vysvětlit. toto je ta druhá část celého příběhu.
ta první polovina je o tom že názvy migračních souborů začínají datem a časem vytvoření. než jsem zjistil že se to dá změnit a že s kolegy sdílím db a že jsme dohodnuti na konvenci datumčas tak jsem zešílel. představ si že, přepínám mezi BlueFish a klasickým terminálem, který mám vytuněný tildou (gnome) ve kterým standrdně vimuju. a pak tohle…
Ohýbání mne baví :-D. Proto jsem vzal ten džob který podmiňoval naučení se RoR.
Comment od smonty on 16. 8. 2010 at 00:08
Na joiny neni potreba pouzivat find_by_sql. Pouziti je podobne jako u include:
>> Contact.find :first, :include => :addresses
SELECT * FROM `contacts` ORDER BY ordered_full_name ASC LIMIT 1
SELECT `addresses`.* FROM `addresses` WHERE (`addresses`.contact_id = 666)
>> Contact.find :first, :joins => :addresses
SELECT `contacts`.* FROM `contacts` INNER JOIN `addresses` ON addresses.contact_id = contacts.id ORDER BY ordered_full_name ASC LIMIT 1
Comment od boblin on 18. 8. 2010 at 22:59
@boblin: Máš pravdu, dík za doplnění.
Comment od Jiří Hradil on 19. 8. 2010 at 09:45