štvrtok, 9. decembra 2010

NHibernate a automaticky udržiavané časové známky (timestamps)


Keď padne rozhodnutie používať vo vašom projekte NHibernate, môžete si v mnohých ohľadoch uľahčiť život (teda hneď potom, čo si omotáte hlavu okolo tejto zázračnej technológie). V tomto blogu vám ukážem ako dohovoriť NHibernate, aby automaticky udržiavalo timestamps na vami-zvolených poliach (teda reálne budete potrebovať mať nanájvyš jedno pole automaticky timestampované).

Ako príklad si uvedieme nasledovné: Užívateľ vašej (webovej) aplikácie uverejní komentár. Tento komentár bude vo vašej aplikácii obsahovať samotný text komentára a časové pole DateUpdated, v ktorom budete chcieť udržiavať dátum a čas zverejnenia/poslednej úpravy komentára. Zakaždým keď užívateľ komentár zverejní, alebo upraví, a obsah komentára je vpísaný/aktualizovaný v databáze, chceme aby sa zaznamenal aj aktuány časový údaj. Toto by za vás mohlo robiť NHibernate, stačí si ho tak nastaviť. Dobre sledujte nasledujúcich pár riadkov kódu, alebo si tie riadky skrátka len skopírujte a použite.

Najprv vás zasypem kódom a potom trošku pokecáme o tom ako to funguje. Budeme potrebovať triedu implementujúcu dve NHibernate interface-y: IPreUpdateEventListener, IPreInsertEventListener. Táto trieda bude potrebovať dva namespace-y, ktoré si môžeme pridať medzi ostatné základné namespace-y ktoré dostaneme pri vytvorení novej čistej triedy:

using NHibernate.Event;
using NHibernate.Persister.Entity;

Trieda samotná bude vyzerať takto:

1 public class TimestampOnInsertUpdate : IPreUpdateEventListener, IPreInsertEventListener
2 {
3  public bool OnPreUpdate(PreUpdateEvent @event)
4  {
5   return !AutoUpdateTimestamps(@event.Entity, @event.Persister, @event.State, true);
6  }
7 
8  public bool OnPreInsert(PreInsertEvent @event)
9  {
10   return !AutoUpdateTimestamps(@event.Entity, @event.Persister, @event.State, false);
11  }
12 
13  private bool AutoUpdateTimestamps(object entity, IEntityPersister persister, object[] state, bool isUpdate)
14  {
15   var dateTimeProperties = entity.GetType().GetProperties().Where(p => p.PropertyType.Equals(typeof(DateTime)));
16   var autoTimestampedProperties = dateTimeProperties.Where(p => p.GetCustomAttributes(false).OfType().Any());
17   if (!autoTimestampedProperties.Any())
18   {
19    return true;
20   }
21   
22   IEnumerable propertiesToTimestamp;
23   if (isUpdate)
24   {
25    propertiesToTimestamp = autoTimestampedProperties.Where(p => !p.GetCustomAttributes(false).OfType().First().InsertOnly);
26   }
27   else
28   {
29    propertiesToTimestamp = autoTimestampedProperties;
30   }
31 
32   if (!autoTimestampedProperties.Any())
18   {
19    return true;
20   }
34 
35   var timestamp = DateTime.Now;
36   try
37   {
38    foreach (var propertyToTimestamp in propertiesToTimestamp)
39    {
40     Set(persister, state, propertyToTimestamp.Name, timestamp);
41     propertyToTimestamp.SetValue(entity, timestamp, null);
42    }
43   }
44   catch (InvalidOperationException)
45   {
46    return false;
47   }
48   return true;
49  }
50 
51  private void Set(IEntityPersister persister, object[] state, string propertyName, object value)
52  {
53   var index = Array.IndexOf(persister.PropertyNames, propertyName);
54   state[index] = value;
55  }
56 }

Ďalej si budeme potrebovať nadefinovať svoju vlastnú atribútu (custom attribute) AutoUpdatedTimestampAttribute, ktorej definícia bude veľmi jednoduchá:

[AttributeUsage(AttributeTargets.Property, AllowMultiple=false)]
public sealed class AutoUpdatedTimestampAttribute : System.Attribute {

 public bool InsertOnly { get; set; }

 public AutoUpdatedTimestampAttribute()
 {
  InsertOnly = false;
 }
}

Naša modelová trieda bude potom môcť takto jednoducho prehlásiť, že niektoré z jej vlastností (properties) potrebujú mať vždy automaticky aktualizovanú časovú známku (timestamp). Príkladná trieda dole napríklad našepkáva NHibernate, žeby bola rada keby sa jej ktosi ujal a aktualizoval jej DateUpdated vlastnosť (pri každom uložení zmeny alebo vytvorení nového záznamu v databáze). Taktiež sa zmieňuje, že by bolo fajn keby pred vložením záznamu (ale len pre vložením) do databázy bola vlastnosť DateCreated nastavená na aktuálny dátum/čas.

...
public virtual string Comments { get; set; }
public virtual DateTime DateLastUsed { get; set; }

[AutoUpdatedTimestamp]
public virtual DateTime DateUpdated { get; set; }

[AutoUpdatedTimestamp(InsertOnly=true)]
public virtual DateTime DateCreated { get; set; }
...

Ešte nám treba objasniť NHibernate, že kde bolo tam bolo, žila raz jedna trieda TimestampOnInsertUpdate, ktorá sa zaujíma o dianie v NHibernate mechanizme pred insert-om/update-om. Stačí pár riadkov medzi nakonfigurovanie NHibernate a vytvorenie ISessionFactory:

var nhConfig = new NHibernate.Cfg.Configuration().Configure();
var timestamper = new TimestampOnInsertUpdate();
nhConfig.EventListeners.PreUpdateEventListeners = new NHibernate.Event.IPreUpdateEventListener[] { timestamper };
nhConfig.EventListeners.PreInsertEventListeners = new NHibernate.Event.IPreInsertEventListener[] { timestamper };
return nhConfig.BuildSessionFactory();

Mohli by sme ich tu zaregistrovať viac než dosť, keďže priradzujeme pole event listernerov. Mohli by sme si zostaviť celú radu listenerov takto:

nhConfig.EventListeners.PreUpdateEventListeners = new NHibernate.Event.IPreUpdateEventListener[] {
 timestamper, id_generator, some_other_listener, nonsense_generator, ...
};
...

Ale to hádam všetci vieme :-)

Ako to funguje


Naša NHibernate IPreInsertEventListener a IPreUpdateEventListener implementujúca trieda je asi najzaujímavejšia časť z toho čo som tu uviedol. Preto som k jej kódu pripojil aj čísla riadkov, aby sa mi na ňu lepšie komentovalo. (Ak vám čísla riadkov prekážajú pri copy-paste robote, vedzte, že nie je prečo robiť paniku: Visual Studio má schopnosť stĺpcovej editácie a tak si pokojne vložte kód aj s číslami do Visual Studia a potom stlačte a držte Alt a myškou označte celý stĺpec čísel a tabulátorov. Zmažeme a práca hotová.)

Najprv však v skratke popíšem všetko ostatné a prečo sme to nakódovali tak ako sme to nakódovali a nie nejak ináč (v článku uvedenom na spodku blogu je uvedený trošku iný príklad používajúci IAuditable interface (ak sa nemýlim)). Využitím vlastnej atribúty a neskôr reflection mechanizmu získame flexibilitu - naše modelové triedy nemusia implementovať žiaden konkrétny interface, ich DateTime vlastnosti nemusia mať také-či-onaké konkrétne predpísané meno, ale je celkom na nás, ktorú vlastnosť označíme našou atribútou. Ak by sme časom odpojili náš IPreInsertEventListener / IPreUpdateEventListener od NHibernate (rozhodli sa nepoužiť NHibernate na udržiavanie časových známok (rozhodneme sa použiť default values na databázových poliach, resp. databázové trigger-y alebo inú metódu), naše modelové triedy môžu pokojne zostať nezmenené, pretože atribúty ani nemusíme odobrať - ak nie sú nikým využité, nevadí.

Náš listener sa dá jednoducho zapojiť / odpojiť z mechanizmu NHibernate, ako sme videli vyššie. Samozrejme si podobným spôsobom môžeme naprogramovať iné aspekty našej aplikácie a pripojiť ich do mechanizmu. V našej aplikécii sa začne diať mágia :-) Tieto listenery musíme zapojiť do NHibernate konfigurácie ešte pred požiadaním o konštrukciu ISessionFactory.

Implementácia našej vlastnej attribúty je jednoduchá, stačí (ako pri každej vlastnej atribúte) vytvoriť uzavretú - sealed - triedu dediacu od System.Attribute a dodať tých pár základných riadkov. Komentár utratím už len na nôtu tohto riadku:
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false)]
Obmedzíme použitie našej atribúty na vlastnosti (čiže aby sme nimi neokrašľovali triedy / metódy atď.) a tiež aby sa mohla uplatniť len jedna atribúta thoto typu na jednu vlatnosť (property). Bol by to chaos, keby môžeme anotovať vlastnosť dvoma protichodnými atribútami:

[AutoUpdatedTimestamp]
[AutoUpdatedTimestamp(InsertOnly=true)]
public virtual DateTime MyProperty;

Náš zázračný listener by nevedel čo s takou vlastnosťou.

Náš listener TimestampOnInsertUpdate na prvých dvanástich riadkoch len implementuje metódy, ktoré musí, sú predpísané v IPreInsertEventListener / IPreUpdateEventListener interface-och. Z oboch metód zavolá magickú metódu AutoUpdateTimestamps a pošle jej to čo treba na celý trik. To sú argumenty entity, persister, state a isUpdate. Entity je náš modelový objekt ktorého stav sa snažíme vopchať do databázy. Persister je objekt zodpovedný za vykonanie samotného úkonu (emituje SQL príkaz(y)), state je aktuálny stav entity, ktorý NHibernate extrahoval z entity a pokúsi sa ho persistovať (uchovať v databáze) pomocou persisteru. Booleanská hodnota isUpdate len informuje o tom, či sa koná insert alebo update (potrebné pre vlastnosti kde atribúta má vlastnosť InsertOnly=true).

Riadok 15 získa z entity všetky vlastnosti typu DateTime. Riadok 16 prezrie tieto vlastnosti a vyberie z nich len tie, ktoré sme označili atribútou AutoUpdatedTimestamp. Ďalej, ak v tomto bode entita nemá žiadne pre-náš-listerner zaujímavé vlastnosti unikáme z metódy s návratovou hodnotou true - všetko OK - vybavené.

Ďalej, od riadku 22 sa venujeme tomu aby sme pri update-ovaní nemenili vlastnosti označené ako InsertOnly. Vyfiltrujeme ich preč, aby sme nemenili to čo nemáme.
Na riadku 32 sa znova pozrieme, či máme čo meniť, ak nie, padáme preč.

Ďalej už je takmer všetko jasné: zaobstaráme si timestamp (r.35) a ideme na vec - každej vhodnej vlastnosti zmeníme hodnotu na aktuálnu časovú známku (1.)priamo na entite r.41, (2.) v stave entity (argument state), ktorý bude persisterom zapísaný do databázy r.40.

Na zmenu stavu entity si zavoláme na pomoc malú metódku r.51, ktorá za pomoci informácií o persisterovi nastaví správnu časť objeku state na patričnú hodnotu.
Krok na riadku 41 je veľmi dôležitý: ako vysvetľuje stránka uvedená na spodku tohto článku, vynechaním tohto kroku by sme si mohli privodiť veľa veľa starostí debugovaním ťažko nájditeľných problémov. Hoci na databázovom servri by sme mali časovú známku nastavenú správne, nebola by v synchróne so stavom našej entity (náš objektový model by nebol zhodný s modelom persistovaným v našej databáze), čo by očividne mohlo napáchať škodu v rámci aplikácie. (Alebo aj nie, ak máme šťastie... radšej ani neskúšajme.)

A to je všetko. Je to v podstate celé jednoduché ako detská hračka, len človek sa samozrejme nenarodil s inštinktívnou znalosťou NHibernate a C#, tak si pomáhame blogmi ako tento. Dúfam, že som niekomu pomohol. Happy coding!

Niektoré časti tejto implementácie boli založené na kóde nájdenom v tomto
článku: Ayende Rahien a jeho článok o IPreInsertEventListener a IPreUpdateEventListener

Žiadne komentáre:

Zverejnenie komentára