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

Java vs Ruby on Rails - cesta tam a zase zpátky

Historie

Než se dostanu k jádru věci, dovolte mi malou cestu časem.

Někdy kolem roku 1999 jsem se jako první jazyk začal učit PHP. Zadáním bylo vytvoření interního web systému pro hodnocení zaměstnanců. Protože jsem se v jazycích nevyznal, vzal jsem první knihu, která mi přišla pod ruku - “ PHP - Hypertextový preprocesor” od Jirku Koska. Už tehdy mě překvapila neuvěřitelná rychlost vývoje. Za týden jsem zpatlal první verzi, napojenou na MySQL databázi. Žádné šablony, žádné navrhové vzory, žádné transakce, žádná 3-vrstvá architektura. Logika pěkně zmatlaná s HTML, pár stránek. Autorizace spočívala ve vsunutí kouzelného atributu do HTML requestu s hodnotou 1, což bylo dle mé naivní představy dostatečné.

Jirka Kosek mi pak stačil pro další 3 roky, abych se uživil jako PHP programátor. Postupně jsem se naučil, že je dobré oddělit logiku od aplikační vrstvy, že existuje něco jako “normální forma”, “sql inject”, “databázové transakce”, apod. Měl jsem však pocit, že musí existovat něco “jednoduššího”, více výkonného, standardního. Tak jsem se dostal k Javě, která byla v té době (kolem roku 2002) opravdu moc sexy. Pomocí Netbeans bylo docela rychlé napsat jednoduchou aplikaci ve Swingu přes GUI editor. Co na tom, že metody v kódu byly zamknuté, nedalo se do nich zasáhnout ručně, třída byla pořád delší a Netbeans byly opravdu pomalé. Psal jsem v Javě, takže jsem byl na špičce, to bylo jasné ;).

Od Swingu jsem se chtěl dostat k Java web aplikacím. A tady jsem poprvé narazil. Napsat Java web aplikaci nebylo vůbec jednoduché. Najednou jsem toho měl umět opravdu dost. JSP, JSTL, EJB, JDBC. Samé stupidní zkratky a co bylo nejhorší - pořad přibývaly. Jen jsem se začal zhruba orientovat, co je vlastně JSP, tak přišlo JSTL se sdělením “jsi idiot, že píšeš v JSP, použij mě”. Jen co jsem zkusil JDBC, dočetl jsem se “JDBC je pro lamy, EJB, to je budoucnost”. Ale protože jsem byl čínský pionýr, který si sám dává překážky, aby je mohl zdolávat, do všech těch buzz jsem se zakousl. Když něco dělám, tak to dělám pořádně a tak jsem to vzal a dal včetně certifikace. Alespoň jsem se naučil základy Javy. To mi ovšem na psaní web aplikace nestačilo. Musel jsem projít všemi těmi Sun srandami. Než jsem byl schopen web aplikaci napsat, musel jsem studovat několik měsíců. To mi nevadilo, protože na konci přece čekala nirvána - něco se naučím a pak už budu web aplikace sekat jako Baťa cvičky.

Jenže Java je nevěrná mrcha. Něco se naučíte a hned přijdou další frameworky, které vše řeší lépe a radostněji. Takhle jsem se postupně dostal k ORM (Hibernate, Toplink), dalším  prezentačním frameworkům (JSF, Stripes, Wicket, Spring MVC), must-have nástrojům (Ant, Maven, Spring, JUnit) a dalšímu, občas dobrému balastu (princip nepřetržité integrace, statická analýza kódu, normy psaní zdrojových kódů…). Tohoto období nelituji, určitě mě neustálé studium posunulo dál. Javu jsem také několik let školil a všechny své kolegy (a později zaměstnance či klienty) dostal, jemně nasměroval či dokopal k Sun certifikacím.

Poslední rok jsem však začal pochybovat ohledně celé Java platformy. Nikoli z pohledu nefunkčnosti, spíše z pohledu efektivity vývoje. V Kyberii používáme striktně agilní vývoj, což je komplikované označení pro “nevím-co-bude-zítra-vývoj”. Agilní vývoj (a jeho implementace XP a Scrum) stále považuji za nejefektivnější možný způsob vývoje software. A tady mě Java brzdila.

Největší překážky v Javě při vývoji webových aplikací:

Složitost jazyka

Java není jednoduchý jazyk. Ze začátku to vypadá, že ano, ale pár tříd aplikaci nedělá. Po “hello world” následuje polymorfismus, modifikátory přístupu, kontejnery, knihovny ze základního API, chuťovky jako reflexe či vlákna… Jistě, nemusíme znát všechno, ale většinu ze standardního API bychom znát měli. Obecně trvá vyškolení Java začátečníka na programátora, který projde standardní certifikací SJCP, asi 20 týdnů (zdroj: moje zkušenosti). Po této době ovšem tento programátor umí jen základy jazyka a není schopen psát produkční aplikaci (nezná ORM, JSP, apod…). Celý proces vyškolení nováčka do produkčního programátora trvá asi rok. Zkuste si to přepočítat na peníze. To byl IMHO také důvod, proč byli a jsou Java experti přeplacení. Samotný vývoj software je pak pro klienty zbytečně drahý a pokud budeme předpokládat, že Java je trend, pak trh trpí nedostatkem kvalitního software.

Širokost platformy

Sun nasměruje budoucí vývoj platformy přes JCP, vznikne nějaké JSR a pak se začne implementovat. Z různých implementací si vývojový tým musí vybrat. A právě možnost výběru vývoj komplikuje. Začínáme pochybovat, zda jsme si vybrali správně, porovnáváme, zkoušíme. U nás jsme třeba začali s JPA přes Toplink, ale později jsme migrovali na Hibernate, protože měl prostě větší komunitní základnu. Byla to jistě správná volba, ale stálo nás to nemalé úsilí a spoustu času.

Špagetování

Pokud už vyberete správné frameworky (u nás to byla kombinace Hibernate JPA - Wicket - Maven2 - Spring - Hudson CI - JUnit-Lucene-Hibernate Search), tak celou aplikaci musíte provázat spoustou špaget, aby vám držela pohromadě. Na špagetování se ukázal vhodný Spring, který je pro default použití jednoduchý, ale při bližším koketování neuvěřitelně složitý. Křivka učení je hlavně ze začátku velmi nestrmá.

Kompilace a redeploy

Tohle byla největší překážka. Při každé změně třeba i v prezentační vrstvě jsme museli aplikaci redeployovat. Servlet kontejner (Tomcat nebo pro vývoj Jetty) namísto nabušeného aplikačního serveru (Glassfish, apod.) proces dost urychlil, ale stejně jsme museli čekat typicky desítky vteřin, než se po reloadu stránky změna projevila (počet tříd v naší aplikaci šel do několika stovek či tisíc). Zkoušeli jsme i hot-redeploy, Java Rebel, apod. Ale stejně jsme museli čekat. Se slzami v očích jsem vzpomínal na PHP, které sice bylo vedle Javy jako ošlivý nepříbuzný, ale zato pekelně rychlý.

Nepřetržitá integrace a testování

V Javě není na testování ani nepřetržitou integraci žádný standard či implementace, která by proces napříč různě použitými frameworky zjednodušila a urychlila. Všechno jsme museli vybudovat ručně - Maven, Hudson CI, scripty pro naplnění testovací databáze, vyhodnocení, pokrytí testy, posílání mailem, apod. Což jsme zvládli v řádu týdnů, ale samozřejmě jsem se musel ptát - copak to nejde jednodušeji?

Ruby on Rails - zpátky na koleje

A ono to jde. Po několika týdnech experimentování, setkání s Jiřím Fabiánem z JetMinds a vytvoření pilotního projektu jsme na konci roku 2009 z Javy migrovali na Ruby on Rails. Když jsem viděl, jak rychle jsme dokázali vytvořit nový projekt a udržovat klienty spokojené tím, že nemusí na výsledky čekat hodiny či dny, ale vteřiny či minuty, bylo rozhodnuto. Efektivita vývoje je dle našich interních měření asi 10x větší. Vrátili jsme se na začátek. Jen místo PHP teď jedeme v Ruby :).

Ale o tom zase v příštím článku.

Publikoval Jiří Hradil • 26.07.2010 v 16:07 • pod kategorií Ruby on Rails Tags: , Žádné komentáře

Hibernate Search – fulltext nad Hibernate ORM

Pokud chceme do naší Java aplikace integrovat fulltextové vyhledávání, dříve či později skončíme u Apache Lucene, frameworku, který je pro vyhledávání v Javě v podstatě standardem a synonymem. Pokud se rozhodneme používat čistý Lucene, musíme mj. řešit:

  • mapování doménových objektů do Lucene (v Lucene jsou všechny atributy obyčejné řetězce)
  • mapování výsledků vyhledávání zpátky do doménových objektů
  • transakce
  • otevírání a zavírání indexu
  • zamykání indexu, v 1 okamžiku může do indexu zapisovat pouze 1 proces
  • škálování fulltextu přes více serverů

Jestliže se však držíme zavedených řešení a používáme Hibernate (ať už Hibernate Core nebo Hibernate jako JPA providera), můžeme využít rozšíření Hibernate Search.
Hibernate Search je projekt, který Hibernate Core doplňuje o fulltextové prohledávání persistovaných objektů a funguje jako fasáda nad Lucene.

Hibernate Search nabízí:

  • API nad fulltextem podobné JPA
  • odstínění od low-level objektů Lucene-nestaráme se vůbec o otevření/uzavření indexu, o zamykání indexu 1 procesem, atd.
  • fulltextové vyhledávání nad persistovanými objekty pomocí Lucene tříd a objektů
  • automatický převod objektů do indexu a zpět (pokud nevyhovuje/nepostačuje, lze napsat převodníky)
  • automatickou synchronizaci indexu s doménovými objekty
  • reindexaci již persistovaných objektů
  • výsledky vyhledávání jsou kompletní objekty, Hibernate Search vrátí na dotaz seznam vyhovujících ID a Hibernate pak načte z db objekty s těmito ID
  • transakční chování – zápis do fulltext indexu se provádí až nakonec po commitu změn do db
  • škálování jako master/slave pomocí JMS
  • jednoduchost, pokud známe Lucene a Hibernate, můžeme jej začít používat okamžitě.

Ukázková aplikace

Pro praktickou ukázku integrujeme fulltextové vyhledávání do jednoduché webové aplikace pro správu firem (klasická create-read-update aplikace).

Použitý stack

  • Hibernate Entity Manager 3.4.0.GA - persistence
  • Hibernate Search 3.1.0.GA – fulltext
  • Apache Maven 2.0.9 – project management
  • Apache Wicket 1.3.5 – prezentační vrstva
  • Spring 2.5.5 – lepidlo
  • PostgreSQL 8.2-relační databáze

Poznámka k následujícím zdrojovým kódům: uvádím pouze soubory, které se bezprostředně týkají Hibernate Search, kompletní zdrojáky aplikace jsou na konci příspěvku.

Závislosti

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

 <modelVersion>4.0.0</modelVersion>
 <groupId>org.hradil</groupId>
 <artifactId>HibernateSearchSimpleApplication</artifactId>
<packaging>war</packaging>
 <version>1.0</version>
 <name>Hibernate Search Simple Application</name>

 <dependencies>

 <!--  wicket -->
 <dependency>
 <groupId>org.apache.wicket</groupId>
 <artifactId>wicket</artifactId>
 <version>1.3.5</version>
 </dependency>

 <!-- wicket+spring -->
 <dependency>
 <groupId>org.apache.wicket</groupId>
 <artifactId>wicket-spring-annot</artifactId>
 <version>1.3.5</version>
 </dependency>

 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-entitymanager</artifactId>
 <version>3.4.0.GA</version>
 </dependency>

 <dependency>
 <artifactId>hibernate-annotations</artifactId>
 <groupId>org.hibernate</groupId>
 <version>3.4.0.GA</version>
 </dependency>

 <!-- Hibernate Search -->
 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-search</artifactId>
 <version>3.1.0.GA</version>
 </dependency>

 <dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring</artifactId>
 <version>2.5.5</version>
 <exclusions>
 <exclusion>
 <groupId>commons-logging</groupId>
 <artifactId>commons-logging</artifactId>
 </exclusion>
 </exclusions>
 </dependency>

 <!-- LOGGING DEPENDENCIES - LOG4J -->
 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-api</artifactId>
 <version>1.5.2</version>
 </dependency>

 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-log4j12</artifactId>
 <version>1.5.2</version>
 </dependency>

 <!-- work around for jetty commons logging issue -->
 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>jcl-over-slf4j</artifactId>
 <version>1.5.2</version>
 </dependency>

 <dependency>
 <groupId>c3p0</groupId>
 <artifactId>c3p0</artifactId>
 <version>0.9.1.2</version>
 </dependency>

 <dependency>
 <groupId>postgresql</groupId>
 <artifactId>postgresql</artifactId>
 <version>8.2-507.jdbc3</version>
 <scope>compile</scope>
 </dependency>

 <!--  JUNIT DEPENDENCY FOR TESTING -->
 <dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>3.8.2</version>
 <scope>test</scope>
 </dependency>

 <!--  JETTY DEPENDENCIES FOR TESTING  -->

 <dependency>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>jetty</artifactId>
 <version>6.1.4</version>
 <scope>provided</scope>
 </dependency>

 </dependencies>

 <build>
 <resources>
 <resource>
 <directory>src/main/resources</directory>
 </resource>
 <resource>
 <directory>src/main/java</directory>
 <includes>
 <include>**</include>
 </includes>
 <excludes>
 <exclude>**/*.java</exclude>
 </excludes>
 </resource>
 </resources>
 <testResources>
 <testResource>
 <directory>src/test/java</directory>
 <includes>
 <include>**</include>
 </includes>
 <excludes>
 <exclude>**/*.java</exclude>
 </excludes>
 </testResource>
 </testResources>
<plugins>
<plugin>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>maven-jetty-plugin</artifactId>

 <configuration>
 <scanIntervalSeconds>5</scanIntervalSeconds>
 <contextPath>/</contextPath>
 </configuration>

 </plugin>
<plugin>
 <artifactId>maven-compiler-plugin</artifactId>
 <version>2.0.2</version>
 <configuration>
 <source>1.6</source>
 <target>1.6</target>
 </configuration>
 </plugin>

 </plugins>
 </build>

</project>

Konfigurace

applicationContext-jpa.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:tx="http://www.springframework.org/schema/tx"
 xsi:schemaLocation="
 http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"
 >

 <description>
 Konfigurace kontextu pro JPA, vcetne datovych zdroju a transakcnich manazeru.
 </description>

 <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
 <description>
 Datovy zdroj pro persistentni vrstvu. Obsahuje udaje o pripojeni k databazi.
 </description>
<property name="driverClass" value="org.postgresql.Driver"/>
<property name="jdbcUrl" value="jdbc:postgresql://localhost/HibernateSearchSimple"/>
<property name="user" value="postgres"/>
<property name="password" value=""/>
 </bean>

 <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
 <description>
 Tovarna pro manazer entit. Je pouzita trida LocalContainerEntityManagerFactoryBean,
 ktera je doporucena pro produkcni nasazeni JPA.
 Viz. http://static.springframework.org/spring/docs/2.5.x/reference/orm.html
 </description>
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
 <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
<property name="generateDdl" value="false"/>
 </bean>
 </property>

 <!-- nastaveni JPA a Hibernate Search -->
<property name="jpaProperties">

 <value>

 # konfigurace JPA
 hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
 hibernate.hbm2ddl.auto=validate

 # debugging / logging
 hibernate.show_sql=true
 hibernate.format_sql=true
 hibernate.use_sql_comments=true

 # konfigurace Hibernate Search
 # kde bude ulozen Lucene index
 hibernate.search.default.indexBase=/tmp/index

 </value>

 </property>
 </bean>

 <tx:annotation-driven transaction-manager="transactionManager"/>

 <!-- transakcni manazer -->
 <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
 </bean>

</beans>

Entita

Company.java

package org.hradil.search.entity;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;

/**
 * Entita reprezentuje firmu.
 * Firmu lze ve fulltextu vyhledat podle id, name a regNo.
 *
 * @author jirka@hradil.org
 */
@Entity
@Table(name = "company")
@Indexed //tridu budeme chtit indexovat ve fulltextu
public class Company implements Serializable {

 private static final long serialVersionUID = 1216348069826762176L;

 @Id
 @Column(name = "id")
 @SequenceGenerator(name = "company_id_seq", sequenceName = "company_id_seq")
 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "company_id_seq")
 @DocumentId //primarni klic objektu ve fulltextu, zaroven podle nej muzeme vyhledavat
 private int id;

 @Column
 @Field //ve fulltextu chceme hledat firmu podle nazvu
 private String name;

 @Column
 @Field //ve fulltextu chceme hledat firmu podle IC
 private String regNo;

 /**
 * Vytvori novou firmu.
 */
 public Company() {
 }

 /**
 * Vytvori novou firmu a predvyplni atributy.
 * @param name nazev firmy
 * @param regNo IC
 */
 public Company(final String name, final String regNo) {
 this.name = name;
 this.regNo = regNo;
 }

 /**
 * Vrati id firmy
 * @return id
 */
 public int getId() {
 return id;
 }

 /**
 * Nastavi id firmy
 * @param id id
 */
 public void setId(int id) {
 this.id = id;
 }

 /**
 * Vrati nazev firmy.
 * @return nazev
 */
 public String getName() {
 return name;
 }

 /**
 * Nastavi nazev firmy.
 * @param name nazev
 */
 public void setName(String name) {
 this.name = name;
 }

 /**
 * Vrati IC firmy.
 * @return IC
 */
 public String getRegNo() {
 return regNo;
 }

 /**
 * Nastavi IC firmy.
 * @param regNo IC
 */
 public void setRegNo(String regNo) {
 this.regNo = regNo;
 }
}

Servisní vrstva - uložení, úprava, vyhledání

CompanyServiceImpl.java

package org.hradil.search.service;

import org.hradil.search.entity.Company;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Implementace sluzby pro firmu.
 *
 * @author jirka@hradil.org
 */
@Transactional(propagation = Propagation.REQUIRED)
@Service
public class CompanyServiceImpl implements CompanyService {

 @PersistenceContext
 private EntityManager em;

 /**
 * {@inheritDoc}
 */
 @Override
 public void addCompany(Company newCompany) {
 em.persist(newCompany);
 }

 /**
 * {@inheritDoc}
 */
 @Override
 @SuppressWarnings("unchecked")
 @Transactional(readOnly = true)
 public List<Company> findCompanyBy(final String fulltextQuery) {

 Assert.notNull(fulltextQuery, "retezec pro vyhledavani nesmi byt null!");

 //vezmeme instanci manazeru fulltextu
 FullTextEntityManager ftEm = Search.getFullTextEntityManager(em);

 //vytvorime parser pro prohledavane atributy firmy, pouzijeme standardni analyzer
 QueryParser parser = new MultiFieldQueryParser(new String[]{"id", "name", "regNo"}, new StandardAnalyzer());

 //vytvorime dotaz do Lucene
 org.apache.lucene.search.Query luceneQuery;

 //pokud po zruseni vsech bilych znaku a hvezdicek zustane jen prazdny retezec, pak vracime vsechny zaznamy
 //napr. dotaz "**** * **  *" bude vyhodnocen tak, ze chceme prohledavat vsechny firmy
 if (StringUtils.trimAllWhitespace(StringUtils.deleteAny(fulltextQuery, "*")).isEmpty()) {
 luceneQuery = new MatchAllDocsQuery();
 } else { //byl zadan retezec, vyhledavame
 try {
 luceneQuery = parser.parse(fulltextQuery); //zparsujeme predany dotaz pomoci parseru Lucene
 } catch (ParseException e) { //neplatny dotaz, prekonverujeme na runtime vyjimku, nemusime zachytavat
 throw new RuntimeException("Neplatny dotaz do fulltextu: " + fulltextQuery, e);
 }
 }

 //vytvorime normalni JPA dotaz, ale pres rozhrani fulltextu
 FullTextQuery query = ftEm.createFullTextQuery(
 luceneQuery,
 Company.class);

 //tridime dle relevance DESC, pote dle id DESC
 SortField[] sortFields = new SortField[2];
 sortFields[0] = SortField.FIELD_SCORE; //relevance, default DESC
 sortFields[1] = new SortField("id", SortField.INT, true); //id, je to intener, DESC=true
 Sort sort = new Sort(sortFields);

 //pridame trideni do dotazu
 query.setSort(sort);

 //a vratime rovnou vyhovujici seznam firem z ORM
 return query.getResultList();

 }

 /**
 * {@inheritDoc}
 */
 @Override
 @SuppressWarnings("unchecked")
 @Transactional(readOnly = true)
 public Company findCompanyBy(int id) {
 return em.find(Company.class, id);
 }

 @Override
 @Transactional(readOnly = false)
 public void updateCompany(Company company) {
 em.merge(company);
 }
}

Zdrojové kódy aplikace

hibernatesearchsimple

Konfiguraci a rozjetí v clusteru si ukážeme v některém dalším příspěvku.

Publikoval Jiří Hradil • 10.05.2009 v 22:05 • pod kategorií hibernatesearch1 komentář

Levná hardwarová infrastruktura

Pro jednoho z našich klientů navrhujeme HW infrastrukturu a vzhledem k přemrštěným cenám za značkové servery známých výrobců (IBM, Sun, HP…) padnul návrh na vybudování clusteru pro aplikační kontejnery z “neznačkových” desktopů, které bychom použili jako servery.  Myšlenka má původ v Google (zdroj: David A. Vise, Mark Malseed: Google Story, Pragma 2007, ISBN: 978-80-7349-034-8), kde je nestabilita desktopů vyvážena jejich množstvím. Pokud některý z  desktopů-nodů vypadne,  zafunguje fail-over a load balancer na něj přestane posílat požadavky. Klient v nejhorším případě zaznamená výpadek v řádu vteřin.

Klady řešení:

  • cena, pokud bychom desktop=nod poskládali ze značkových komponent, pak je cena jednoho tohoto nodu v řádech tisíců
  • jednoduchý upgrade - nod by mohl být při výpadku či upgrade nahrazen rychlejším, poskládaným z aktuálních “best of breed” komponent
  • dostupnost - nod lze poskládat z komponent, nakoupených v jakémkoli supermarketu společně s rohlíky
  • nezávislost na dodavateli - sbohem obchodníkům, prodávajícím řešení, kterým nerozumí nebo na kterých se chtějí napakovat ;)

Zápory řešení:

  • možná nekompatibilita komponent - paměť od výrobce ABC  si nerozumí s deskou výrobce XYZ, což nemusíme poznat okamžitě, nod může vykazovat nestabilitu náhodně, lze řešit intenzivním testováním
  • poskytování SLA - infrastrukturu musíme podporovat sami, ideálně mít nakoupeno několik desktopů do zásoby a při výpadku nod rovnou vyměnit za jiný
  • požadavky na prostor - pokud by nod byl umístěn v běžném mini či miditoweru, zabírá více prostoru, než hezké 1U či 2U skříně “značkových” výrobců. Nicméně desktopy lze do skříní montovat taky.

Co je třeba dále zvážit:

  • pozor na SPOF (single point of failure), pokud např. máme relační databázi, která fail-over na jiný server neumí, potencionálně nestabilní desktop tady nelze použít
  • disky použité v nodu, protože na nod bude deploynutý jen .war aplikace a relační databáze je umístěna v jiné vrstvě, stačí nám levné disky, zapojené do prostého RAID 1 (mirror), abychom při výpadku jednoho disku neohrozili stabilitu aplikace

Použité technologie:

Použil jste někdo podobnou infrastrukturu v ostrém provozu?

Publikoval Jiří Hradil • 21.04.2009 v 23:04 • pod kategorií hardware1 komentář

Definitivně: Oracle kupuje Sun

Aktuální informace přímo od zdroje - Oracle a Sun Microsystems vstupují do konečné fáze dohody, podle které Oracle koupí Sun za cca 7.4 miliardy dolarů. Oracle tak získává Javu, Solaris a ostatní technologie (zdroj: Sun Microsystems: Oracle to buy Sun). Vedle nezajímavých řečí o 20-letém partnersví a obrovském přínosu pro komunitu, uživatele, atd. bude velmi zajímavé sledovat skutečný důsledek této akvizice.

Sun byl sice skvělý technologický inovátor, ale svým technologiím nedokázal přinést vhodný obchodní model. Takže se necháme překvapit, co Oracle konkrétně s Javou provede. Co myslíte?

Publikoval Jiří Hradil • 20.04.2009 v 22:04 • pod kategorií NezařazenéŽádné komentáře

Postřehy z agilní praxe

Proces vývoje software odráží několik posledních let jasný trend - nestálost prostředí a připravenost na změny. Pokud shrneme několik agilních metodik (extrémní programování, Scrum, RUP), tak řeší v podstatě naprosto stejné věci-krátké iterace, testy, otevřenost, zeptej se kódu, atd.  Některé z těchto pravidel rozeberu.

Vyvíjíme po malých částech, čili v iteracích=cyklech=sprintech. Jedním z důvodů je nestálost prostředí, ve kterém má být software používán (např. situace na trhu či legislativa), ale hlavně neschopnost a nemožnost vyprodukovat stabilní analýzu systému, podle které se dá programovat. Analytiky software považuji za dinosaury, jejichž doba dávno skončila a při vývoji software jednoduše nejsou potřeba. Základ software z pohledu uživatele má navrhnout klient a proveditelnost návrhu určují koneční vývojáři. Setkal jsem se s tím, že analytik (který nikdy neprogramoval), vygeneroval stovky stránek krásných diagramů a dokumentů, které si nechal nebohým klientem schválit. Klient samozřejmě netušil, co ty obrázky znamenají (když ony byly tak hezky barevné…). Poté se tento stoh dokumentů předhodil vývojářům, kteří obrázkům taky nerozumněli, ale dostali jasný pokyn - “programujte!” Po několikaměsíčním vývoji při prezentaci systému klient zjistil, že vlastně teď potřebuje už něco úplně jiného a software se do produkce vůbec nedostal.

Doporučení:

  • iterace je dlouhá max. 1 měsíc, v počátcích vývoje kratší, klidně i 1 týden
  • vývojáře musíme protlačit ke klientovi, protože jedině zadavatel a tvůrce systému umí pokládat správné otázky a generovat správné odpovědi
  • po každé iteraci následuje prezentace klientovi, který potvrdí aktuální verzi a na jejím základě generuje požadavky další
  • popis systému je obsažen ve zdrojovém kódu a v uživatelských zadáních
  • podrobná dokumentace je zbytečná - vývojáři ji nepotřebují, tvůrce ji musí aktualizovat a koneční uživatelé jsou tak frustrovaní množstvím software, který musí ovládat, že ji číst nebudou. Složitá sice může být problémová doména, kterou software řeší (třeba statistický software), ale nikdy nesmí být složitý software samotný. Pokud např. posadíme statistika k našemu statistickému softwaru, musí ho být schopen do několika minut bez problémů ovládat, protože ovládá problémovou doménu, do které je náš software zasazen a kopíruje ji.

Testujeme - test simuluje používání systému uživatelem. Ať už to schováno pod pojmy test jednotkový (na úrovni metody), integrační (na úrovni větší části systému), zátěžový, opičí, či jiný, smyslem testu je software nastartovat, aby ukázal, co umí. Testy se mají psát průběžně a to z toho důvodu, že na konci je už nikdo psát nebude. Stejně tak s každým testem vytvoříme nekonečného automatického robota, který nám bude hlásit, jestli pořád software dělá, co dělat má. Psaní testů z krátkodobého pohledu zdržuje, ale dlouhodobě nám právě testy drží software stabilní.

Doporučení:

  • kontrolovat pokrytí software testy (např. v Javě přes Cobertura), stanovit si hranici, pod kterou nesmíme jít, ideálně nad 80%)
  • maximálně urychlit psaní a spouštění testů (mock objekty, databáze v paměti)
  • nepřetržitě integrujeme pomocí integračního serveru (Continuum, Hudson, Bamboo…)

Jsme malí - neudržujme velké týmy. Čím více vývojářů, tím více tření, komunikace, rozhodování, administrativy a neefektivity, kterou nakonec (zbytečně) platí klient.

Doporučení:

  • ideální velikost týmu je 3-5 vývojářů, pokud se jich vývoje účastní více, uděláme více týmů a každý z nich má svou problémovou doménu. Tyto týmy však musí být vzájemně synchronizovány, např. pomocí scrum of scrums-porad, kterých se účastní pouze vyhrazený člověk z každého týmu, nikoli všichni členové  dohromady.

Posaďme tým dohromady, čili  “seat the team together”. Tým musí sedět pohromadě. Tečka. Tohle je nejrychlejší způsob řešení komplikací během vývoje a udržení synchronizovaných znalostí o systému. Pokud má kdokoli v týmu otázku, může ji položit a dostane se mu okamžité odpovědi.

Doporučení:

  • respektovat klid pro práci - pokud máme otázku, nekřičíme na celou místnost, ale zeptáme se, zda můžeme vyrušit, v extrémních případech si zavedeme každou hodinu několik minut na otázky a odpovědi
  • červená čepice - jednoduché pravidlo - v týmu je k dispozici symbol “červené čepice” (ať už cedulka, nebo opravdová kšiltovka), kdo ji má na hlavě, ten nesmí být rušen
  • nepracovat z domova přes Internet, bohužel, zatím se ukazuje, že je stále efektivnější být fyzicky pohromadě v 1 místnosti, než komunikovat přes Skype, Jabber, atd. Budu rád, když mi tuto neefektivitu někdo vyvrátí a přidá praktické zkušenosti.
Publikoval Jiří Hradil • 12.04.2009 v 23:04 • pod kategorií Nezařazené3 komentářů