Jiří Hradil blog

o software


Rails and the Enterprise

Posílám skvělý článek Rails and the Enterprise, který je povinností pro každého programátora v Ruby on Rails.

Zajímá mě:

  1. jak byste s ohledem na obsah článku definovali “enterprise”?
  2. kde je hranice, od které považujete systém za “velký”?
  3. proč a jak jsou některé jazyky či frameworky vhodnější pro vývoj “velkých” systémů?
  4. kdo či co je autorita, která definuje tuto “vhodnost”?
Publikoval Jiří Hradil • 25.08.2010 v 22:08 • pod kategorií Ruby on RailsŽádné komentáře

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Žádné komentáře

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í.

Publikoval Jiří Hradil • 15.08.2010 v 13:08 • pod kategorií Ruby on Rails5 komentářů

Active Record a automatické findery

Jako další pěknou vlastnost Active Record vypíchnu automatické findery.

Zděděním modelu (Ruby třídy, která reprezentuje “business object”) od ActiveRecord::Base získáváme automatické findery na všechny atributy, načtené reflexí z databázové tabulky i včetně jejich kombinací.

Příklad:

Stejně jako v minulém příspěvku použijeme třídu Contact:

class Contact < ActiveRecord::Base
#to je vsechno
end

A její tabulku contacts:

CREATE TABLE contacts (
id integer NOT NULL PRIMARY KEY,
name varchar(255),
address varchar(255)
);

Atribut id je default primární klíč (ve třídě jsme tohle nikde neurčili, je to default chování Active Record). Pak můžeme kontakt najít jednoduše dle id:

c = Contact.find(1) #najde kontakt s id 1

Metodu find třídy Contact jsme nikde nenapsali, je zděděna z ActiveRecord::Base. Zajímavé jsou však další “automatické findery” dle atributů tabulky, aniž bychom je kdekoli definovali:

c = Contact.find_by_name('Jirka') #metodu find_by_name jsem nikde nepsal
c = Contact.find_by_address('Valhalla') #tuhle jsem taky nikde nepsal

Fungují dokonce kombinace atributů. Je jedno v jakém pořadí, všimněte si spojky “and” mezi atributy:

c = Contact.find_by_name_and_address('Jirka', 'Valhalla') #nejdriv jmeno, pak adresa
c = Contact.find_by_address_and_name('Valhalla', 'Jirka') #naopak

Tohle je vývojářův sen. Žádné rozhraní, žádné implementace, při změně atributů v tabulce nemusíme findery měnit či připisovat nové. Změníme pouze schéma tabulky a nové findery a jejich kombinace máme okamžitě k dispozici. Samozřejmě bez redeploy aplikace či restartu serveru.

Vypadá to jako magie, ale Active Record využívá Ruby metodu method_missing, která se zavolá vždy, když metoda v objektu neexistuje.  V tomto případě si method_missing sáhne do schématu tabulku a za běhu přidá novou metodu dle kombinace atributů.

Protože máme logiku i data pěkně pohromadě, pak se nemusíme rozpomínat či studovat, jaké rozhraní používat pro hledání, jaké pro ukládání, co pro změnu či mazání, apod. Prostě potřebuju pracovat s kontaktem, tak použiju třídu Contact či některou z jejich instancí.

Příklad:

c1 = Contact.new(:name=>'Hradil')
c1.save #ulozime novy kontakt
c1.name = 'Jirka Hradil' #zmenime jmeno
c1.save #ulozime zmeny, provede se UPDATE, protoze zaznam uz byl persistovan a ma id
c2 = Contact.find_by_name('Jirka Hradil') #najdeme si kontakt
c2.delete #smazeme kontakt, provede se DELETE

Zkuste si to a uvidíte, jak neradi se budete vracet k Hibernate :).

Publikoval Jiří Hradil • 08.08.2010 v 17:08 • pod kategorií Ruby on RailsŽádné komentáře

Active Record je nejlepší ORM

Při vývoji webových aplikací pomocí Ruby on Rails se okamžitě setkáme s potřebou ukládat objekty do databáze. Ruby on Rails používají Active Record, což je první ORM, které je opravdu radost používat. Ve srovnání třeba s Toplinkem nebo Hibernate mě nijak nebrzí a neuvěřitelně urychluje vývoj.

Za pozornost stojí:

Konvence před konfigurací (Convention over Configuration)

A to narozdíl třeba od Springu bez keců. Žádné XML, žádné mapování databázových atributů do pofidérních XML objektů či anotací, nic. Stačí Ruby business objekt podědit od třídy ActiveRecord::Base Mapování se bere rovnou z databázové tabulky a v Ruby objektu o něm není ani zmínka.

Příklad:

Třída

class Contact < ActiveRecord::Base
#to je vsechno
end

…si všechny atributy včetně datových typů načte dle databázové tabulky contacts:

CREATE TABLE contacts (
id integer NOT NULL,
name varchar(255),
address varchar(255)
);

Protože se používá CoC, nemusíme mít nikde žádný soubor, který definuje, že třída Contact se má podívat do contacts, spojení je definováno automaticky (Contact se automaticky převede na malá písmena a dá se do množného čísla).

Protože je použita reflexe dle struktury tabulky, ve třídě Contact máme opět automaticky k dispozici gettery a settery na všechny atributy z tabulky, takže můžeme klidně hned použít:

c=Contact.new
c.name='Jirka Hradil'
c.address='Valhalla'

nebo ještě rychlejší:

c = Contact.new(:name=>'Jirka Hradil', :address=>'Valhalla')

Tohle je, vážení kolegové, jednoduchost v ryzí formě. Napadá někoho, jak to udělat ještě líp?

Data a logika pohromadě

Klasický Java patternista s tímhle bude mít asi problém. Dobré je přece mít servisní vrstvu, pod ní DAO vrstvu, logika je v servisní vrstvě striktně oddělená od DAO, vše pro jistotu přes rozhraní, pak implementovat… Proč? Pokud byste někdy potřebovali změnit implementaci, tak “jen” implementujete rozhraní. Já osobně jsem si ale vždy vystačil pouze s jedinou implementací a tyto patterny mě jen zdržovaly. ActiveRecord::Base nám do třídy přidá rovnou instanční metody pro uložení. Takže jakmile máme vytvořen náš kontakt, ukládáme:

c.save #to je vsechno

Tímto se vygeneruje klasický INSERT INTO… a záznam je persistován. Žádný service locator, žádné vytváření session, nic. Pokud potřebujeme ukládat třeba více objektů najednou nebo dát před ukládání nějakou logiku, vytvoříme si vlastní metodu a v ní si uděláme, co je třeba.

YAML místo XML či properties souborů

YAML je formát pro serializaci či definici dat a v Rails se používá pro konfigurační soubory. Někde přece jen musí být napojení Rails aplikace do databáze definováno a tímto souborem je database.yml:

...
development:
adapter: postgresql
encoding: unicode
database: book_development
pool: 5
username: uzivatel
password: heslo
...

To je v kostce veškeré napojení Active Record na konkrétní databázi a všechna potřebná konfigurace. Opět - jde to jednodušeji?

V některém z dalších článků se zmíním o tom, jak je to s transakcemi, asociacemi a s lazy vs eager.

Publikoval Jiří Hradil • 03.08.2010 v 22:08 • pod kategorií Ruby on RailsŽádné komentáře