Jiří Hradil blog

o software


Active Record a transakce

Použití transakcí je v Active Record opravdu triviální. Samotná dokumentace k modulu ActiveRecord::Transactions::ClassMethods je jednoduchá a ovládnutelná za 5 minut. Zapomeňme na dlouhá studia románů typu Spring transaction management, zapomeňte na AOP, advisory a jiné ptákoviny. Nepoužíváme technologii pro technologii. Transakce potřebujeme jednoduše proto, aby data v databázi byla v každém okamžiku konzistentní. Pro polévku kolem odkazuji třeba na Wikipedii.

Automatické transakce pro create, save a destroy

Pokud voláme jen jednu metodu create, save nebo destroy, transakce nemusíme řešit, Active Record volání metod obalí transakcí automaticky:

Contact.create(:name=>'Jirka Hradil')

Vygeneruje:

BEGIN
INSERT INTO "contacts" ("name") VALUES('Jirka Hradil') RETURNING "id"
COMMIT

Je to logické - 1 volání create, save nebo destroy buď je nebo není provedeno. Toto volání už nemáme jak kouskovat. Samozřejmě pokud je insert, update nebo delete pouze 1, nebylo by třeba vůbec transakci používat, samostatný sql statement je atomický sám o sobě.

Stejně můžeme uložit více objektů najednou, pokud se ovšem vejdeme do volání 1 metody:

Contact.create(:name=>'Jirka Hradil', :addresses=>[Address.new(:address=>'Pod Valhallou 1')])

Vygeneruje:

BEGIN
INSERT INTO "contacts" ("name") VALUES('Jirka Hradil') RETURNING "id"
INSERT INTO "addresses" ("address", "contact_id") VALUES('Pod Valhallou 1', 4) RETURNING "id"
COMMIT

Ruční transakce přes více metod

Pokud potřebujeme zavolat více metod, které musí být provedeny všechny najednou nebo žádná z nich, obalíme je metodou transaction, která je jak metodou třídy, tak instance každého modelu (třídy jsou v Ruby také objekty):

c = Contact.new(:name=>'Jirka Hradil')
a = Address.new(:address=>'Pod Valhallou 1', :contact=>c)

Contact.transaction do #tady by klidně mohlo být c.transaction nebo Address.transaction
c.save!
a.save!
end

Vygeneruje stejné:

BEGIN
INSERT INTO "contacts" ("name") VALUES('Jirka Hradil') RETURNING "id"
INSERT INTO "addresses" ("address", "contact_id") VALUES('Pod Valhallou 1', 5) RETURNING "id"
COMMIT

Pokud používáme jen jedno databázové spojení (default nastavení Rails), je jedno, ze kterého modelu metodu transaction použijeme, tuto metodu každý model dědí z ActiveRecord::Base, který includuje modul ActiveRecord::Transactions::ClassMethods.

Transakce napříč databázovými spojeními

Tuto možnost jsem nikdy nezkoušel, ale dle dokumentace stačí vnořit volání metod transaction do sebe napříč modely, které jsou ukládány do různých databází:

c = Contact.new(:name=>'Jirka Hradil')
a = Address.new(:address=>'Pod Valhallou 1', :contact=>c)

Contact.transaction do
Address.transaction do
c.save!
a.save!
end
end

Plně distribuované transakce napříč různými databázemi Active Record nepodporuje. Já osobně jsem tohle nikdy nepotřeboval a dávám si moc záležet, abych o distribuované transakce ani nezavadil, ale pro někoho jejich absence může být omezením.

A co rollback a commit?

Commit neřešíme. Pokud žádné volání metod uvnitř transakčního bloku nevyhodí výjimku, na konci bloku se provede commit automaticky.

Rollback také nemusíme řešit. Vyvolá se sám, pokud některá z metod vyhodí výjimku.

Na co si dávat pozor při použití save namísto save!

Pokud voláme více metod, které obalíme do transakčního bloku a pro ukládání změn voláme save namísto save!, pak pozor - metoda save (bez vykřičníku) nevyhazuje výjimku a tedy se ani neprovede rollback v případě neuložení objektu (třeba když neprojde validace). Výsledkem je nekonzistentní stav, který je na konci potvrzen “sprostým” commitem:

Contact.transaction do
c.save #nevyhodí výjimku, ale nemusí se uložit do db, pokud např. neprojde validace
a.save #nevyhodí výjimku, ale nemusí se uložit do db, pokud např. neprojde validace
end

Pokud třeba selhala validace u adresy, vygeneruje se jen insert pro kontakt:

BEGIN
INSERT INTO "contacts" ("name") VALUES('Jirka Hradil') RETURNING "id"
COMMIT

…a databázi máme v nekonzistentním stavu. Což je logické, protože jak jsem uvedl, rollback se zavolá jen tehdy, pokud něco v transakčním bloku vyhodí výjimku.

Publikoval Jiří Hradil • 24.08.2010 v 10:08 • pod kategorií Ruby on Rails

No comments yet.

Komentovat

Security Code: