úterý, června 24, 2008

Jak udělat espresso

Protože mně Nikdo předal štafetu, tady je moje oblíbené video: Jak správně na espresso. Mimochodem, mají tam nádhernou mašinku. Jednou bych chtěl takovou doma:-)



Kafe které tam používají (Ethiopia Sidamo) se dá sehnat i u nás, např. já ho kupuju v Mama Coffee kde ho mají v super kvalitě a za rozumnou cenu.

pátek, června 20, 2008

Když Já není Já

Nebojte, nebudu tu rozebírat schizofrenní poruchy vyvolané dlouhodobým zíráním do monitoru. Jde o klíčové slovo this v C# a o jeden zajímavý případ, kdy this není this, nebo alespoň není to this, které jsme si mysleli že bude.

Pro začátek si představte, že máte dva objekty, mezi kterými je obousměrná vazba 1-N. Například User a Task, tedy uživatel a jeho úkoly, kde úkol může být přidělen nejvýše jednomu uživateli:


public class User
{
IList _tasks;
public IList Tasks
{
get { return _tasks; }
set { _tasks = value; }
}
}

public class Task
{
User _holder;
public User Holder
{
get { return _holder; }
set { _holder = value; }
}

... etc ...
}


Protože nepracujeme ve státním sektoru, Tasků je hodně a je nutné, aby si naše aplikace pamatovala úkoly uživatelů i zítra. Použijeme tedy nějakou databázi; tím myslím existující databázový systém, který si stáhneme z internetu (MS SQL Express, MySQL apod.). Nezačneme tedy implementací vlastního RDMS, ani nebudeme simulovat databázi pomocí mnoha XML souborů uložených kdesi na disku (asi se divíte proč se s tím tak rozepisuju, ale nevěřili by jste, kolik lidí nemá rádo databáze, co si sami nenapsali).

A protože jsme vychytralí, použijeme nějaký O/RM nástroj pro mapování objektů do databáze. A jelikož nejsme bohatí, bude to NHibernate, který je zdarma, a podporuje víc databází než LINQ to SQL (navíc jsme chudí takže stále pracujeme ve Visual Studiu 2005 kde není podpora lambda výrazů).

Když budeme tedy chtít přiřadit Task nějakému uživateli, náš kód bude vypadat nějak takhle:


...
// odněkud vytáhneme volný Task
Task task = TaskRepository.GetNextUnassignedTask();
// najdeme uživatele, který nemá co na práci
User user = UserRepository.GetUserReadingBlogsButNotMine();
// a dáme mu úkol, ať se snaží hošánek (nebo frajerka)
task.Holder = user;
user.Tasks.Add(task); // zachováme obousměrnost vazby, aby se další kód mohl bez problémů odkazovat na user.Tasks
...


Ha. Funguje to. Něco tu ale smrdí. Ano, jsou to ty dva řádky, které nastavují vazbu mezi Userem a Taskem. Nestačil by jeden? Jak známe kolegu X., určitě někde druhý řádek zapomene (nám by se to samozřejmě nestalo), a pak bude od tohoto bodu dále v paměti strašit inkonzistence dat, až do ukončení databázové ISession. Pak může snadno nastat chyba úplně na jiném místě a než na to přijdeme, ujmou se nás psychoterapeuti. Zkusíme tedy, ach my bláhoví, upravit property Holder:


public class Task
{
User _holder;
public User Holder
{
get { return _holder; }
set {
if(_holder!=value)
{
if(_holder!=null)
_holder.Tasks.Remove(this);
_holder = value;
if(_holder!=null)
_holder.Tasks.Add(this);
}
}
}

... etc ...
}


Ponechme teď stranou fakt, že pokud to samé uděláme v User.Tasks.Add(), nastane nekonečná smyčka kterou ukončí až milosrdné přetečení zásobníku nebo jiná populární výjimka (ve skutečnosti budeme chtít z objektu User publikovat pouze read-only wrapper kolekce Tasks, protože to bude inverzní konec asociace, který není ukládán do databáze). Přejděme mlčky i problém inicializace objektu Task při jeho nahrávání z databáze, kdy kolekce _holder.Task ještě nebude inicializovaná a přidáním tasku do ní si připravíme pozdější muka při nahrávání kolekce z databáze (to se dá řešit, viz např. třída PersistentGenericBag<T> a její metoda DelayedAddAll v NHibernate).

Leč je tu ještě daleko zákeřnější past: this není this které potřebujeme. Jak je to možné? Protože používáme lazy loading našich objektů. Je velmi pravděpodobné, že místo objektu task dostaneme proxy objektu task. Náš milý task proto nebude instance typu Task, ale typu s krásným názvem CProxyTypeTest_BidirectionalTestTaskDomain_NHibernate_ProxyINHibernateProxy1, nebo podobně pojmenovaného. Tento typ je dynamicky vygenerovaný potomek našeho typu Task a slouží jako proxy, která nahraje task z databáze teprve když je to potřeba. Když je proxy třída odvozená z naší třídy, v čem je tedy problém? V tom, že objekt task NENÍ proxy objekt; v programátorštině mezi nimi není referenční identita. Proxy objekt totiž neukládá data do sebe, ale obsahuje referenci na náš objekt a přeposílá mu veškerá volání metod a vlastností (a zpátky předává návratovou hodnotu). Aby to nebylo tak jednoduché, mezi proxy a naším taskem stojí ještě další objekt, tentokrát typu CastleLazyInitializer z NHibernate. Ale to už zabíhám do detailů.

Podívejme se teď znovu na první verzi kódu a přidejme jednu celkem logickou kontrolu výsledku:


...
// odněkud vytáhneme volný Task
Task task = TaskRepository.GetNextUnassignedTask(); // vrací proxy objektu Task
// najdeme uživatele, který nemá co na práci
User user = UserRepository.GetUserReadingBlogsButNotMine();
// a dáme mu úkol, ať se snaží hošánek (nebo frajerka)
task.Holder = user;
user.Tasks.Add(task); // do user.Tasks přidáváme proxy objektu Task
// zkontrolujeme zda je task součást uživatelových úkolů
Assert.IsTrue(user.Tasks.Contains(task)); // OK

...


Protože se NHibernate chová jako správná Identity Map, vrací v rámci jedné session reference konzistentně, tj. pokud se na několika místech budete dotazovat různým způsobem na jeden Task, dostanete vždy odkaz buď na objekt samotný, nebo na stejnou proxy; mezi vrácenými objekty funguje referenční integrita. To znamená, že user.Tasks.Contains(task) bude fungovat a vrátí správně true.

A nyní se podívejme, co se stane když necháme přidání tasku do kolekce na property setteru Holder:


...
// odněkud vytáhneme volný Task
Task task = TaskRepository.GetNextUnassignedTask(); // vrací proxy objektu Task
// najdeme uživatele, který nemá co na práci
User user = UserRepository.GetUserReadingBlogsButNotMine();
// a dáme mu úkol, ať se snaží hošánek (nebo frajerka)
task.Holder = user; // task je proxy, ve skutečnosti zprostředkuje volání property setteru našeho objektu
// zkontrolujeme zda je task součást uživatelových úkolů
Assert.IsTrue(user.Tasks.Contains(task)); // FAIL!!!
...


A zde je jádro pudla: co se ve skutečnosti stane se dá opsat jako task.ProxiedObject.Holder = user;. Uvnitř property setteru Holder proto this ukazuje na náš objekt, ne na proxy. Do kolekce user.Tasks se přidá náš objekt Task, ne proxy v proměnné task. Vypadá to celkem nevinně, protože v tu chvíli už bude náš objekt inicializovaný (přistupujeme na property task.Holder), ale je nutné si uvědomit, že přestává fungovat referenční integrita a tím porušujeme správné fungování Identity Map. Volání metody user.Tasks.Contains(task) na dalším řádku vrátí false, přesto že je náš objekt Task v kolekci a v proměnné task je proxy ukazující na náš objekt. Kolekce user.Tasks prostě obsahuje task.ProxiedObject a ne task.

Zajímavé je podívat se na stack trace v property setteru Task.Holder. Ukazuje, jak se jednoduché volání task.Holder přesměruje přes proxy a interceptor (CastleLazyInitializer):


BidirectionalTest.dll!BidirectionalTest.Task.Holder.set(BidirectionalTest.User value = {BidirectionalTest.User}) Line xxx C#

[Native to Managed Transition]

[Managed to Native Transition]

Castle.DynamicProxy.dll!Castle.DynamicProxy.Invocation.AbstractInvocation.Proceed(object[] args = {Dimensions:[1]}) + 0x76 bytes

NHibernate.dll!NHibernate.Proxy.CastleLazyInitializer.Intercept(Castle.DynamicProxy.IInvocation invocation = {Castle.DynamicProxy.Invocation.SameClassInvocation}, object[] args = {Dimensions:[1]}) + 0xc5 bytes

DynamicAssemblyProxyGen!CProxyTypeTest_BidirectionalTestTaskDomain_NHibernate_ProxyINHibernateProxy1.Holder.set(BidirectionalTest.User value = {BidirectionalTest.User}) + 0xb3 bytes

BidirectionalTest.dll!BidirectionalTest.Bidirectional1NIntegrationTest.Update_ChangesTargetCollection() Line 77 + 0xb bytes C#


A nyní možná trochu překvapivý závěr: Celou tuhle anabázi jsme podstoupili jen proto, že nám nevoněl jeden řádek navíc. Protože se ale synchronizace druhých konců obousměrných asociací (to samé platí i pro many-to-many) ukázala jako plná mnoha problémů, zatím její řešení odsouvám na neurčito. Nemám čas zabývat se problémy, pro které existuje tak jednoduchý workaround. Nejde jen o proxy, ale také o read-only wrappery inverzních konců, rekurzivní smyčky a load time vs. run time, ale také o vlastní typy persistentních kolekcí implementujících IList<T> (místo PersistentGenericBag<T> a spol.), pokud bychom chtěli synchronizovat druhý konec many-to-many asociace při volání Add a Remove (i to je v NHibernate možné, ale je to trochu kostrbaté).

Navíc je tu ještě jedno hledisko, performance. Je třeba si uvědomit, že pokud odebírám prvek z inverzního konce kolekce (nebo přidávám prvek v případě set a idbag), je nutné nejdřív tuto kolekci načíst do paměti, a to většinou zcela zbytečně! Inverzní konce se totiž nijak neukládají. Problém pak ale zůstává, jak chránit další kód ve stejné session proti nekonzistenci dat.

Budu rád pokud zjistím že se pletu a někdo mi napíše jednoduchý způsob jak v NHibernate synchronizaci provést.