Ak potrebujete ulozit typ DateTime s presnostou na milisekundy (napr ukladam transakcie a tam potrebujem co najvyssiu presnost), moze byt neprijemne zistit, ze NHibernate v kombinacii s MSSQL serverom (a tipujem, ze sa to tyka aj dalsich db serverov) vam milisekundovu cast jednoducho odsekne.
Preco sa to deje? Nativny .NET DateTime typ uklada cas - konkretne milisekundy s presnostou na 7 miest. MSSQL nativny DateTime iba 3 (DateTime2 uz na 7, vid t-sql msdn doc: http://msdn.microsoft.com/en-us/library/ms187819.aspx). NH standardne pouziva ADO.NET pre komunikaciu s MSSQL, a ten nam nic presnejsie ako mu databaza ponuka nevrati.
Kvoli tymto rozdielom NH odsekava milisekundovu cast a nechava tuto funkcnost na uzivatela.
Mozne riesenia:
1) ako prve by nas napadlo ukladat ticks ako Int64 - to zafunguje, avsak niekedy chceme mat cas ulozeny ako DateTime (napriklad pri rieseni problemov, ak mame logy (napr transakcii), jazyk SQL a databaza nam umozni komfortne a rychlo vyhladat problemove zaznamy).
2) slo by to za pouzitia Interceptorov, toto riesenie sa mi zda nevhodne, smrdi. Overridnut OnSave, OnFlushDirty...
public override bool OnFlushDirty(object entity,
object id,
object[] currentState,
object[] previousState,
string[] propertyNames,
IType[] types)
{
if(entity is DomainObjectICareAbout)
for ( int i=0; i < propertyNames.Length; i++ ) {
if ( currentState[i] is DateTime ) {
DateTime dt = (DateTime)currentState[i];
dt = dt.AddMilliseconds(-1 * dt.Millisecond);
return true;
}
}
}
return false;
}
3) session.Refresh(entity) by malo zafungovat akurat je to extra query.
4) vlastny typ - implementovat IUserType, co sa mi zda najvhodnejsie riesenie.
Pekny post som nasiel tu: http://www.mostlyclean.com/post/2007/11/Increasing-DateTime-storage-precision-in-Nhibernate-(and-Castle-ActiveRecord).aspx
Z clanku ukazka:
public class PreciseDateTimeUserType : IUserType
{
#region IUserType Members
......
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
int ordinal = rs.GetOrdinal(names[0]);
if (rs.IsDBNull(ordinal))
{
return new NullPreciseDateTime();
}
else
{
long value = rs.GetInt64(ordinal);
return new PreciseDateTime(value);
}
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
long val = ((PreciseDateTime) value);
((IDbDataParameter) cmd.Parameters[index]).Value = val;
}
.....
#endregion
}
Je tam priklad tak pre NH ako aj CastleRecord.
Enterprise library je kolekcia znovu pouzitelnych a kombinovatelnych kniznic od Microsoftu roznej funkcionality (data access, logging, exception handling, caching, crypto, validation, security, atd) s cielom pouzia (nie len) vo vacsich projektoch. Kazda kniznica riesiaca isty problem sa nazyva blok a tieto bloky mozete nakombinovat az postavite pekny (lego) domcek. Posledna verzia bola vydana v oktobri 2008 a nesie verziu 4.1 (aj ked z vacsej casti obsahuje bugfixy a vylepsenie vykonnosti, preto ak pouzivate starsiu verziu, rozhodne doporucujem upgrade).
http://msdn.microsoft.com/en-us/library/cc467894.aspx
Spring.NET je trosku mensi moloch, obsahujuci okrem ineho aj IoC kontajner (alebo Dependency Inversion, ako sa Vam paci). Je vyvyjany ako open source a mometalna stabilna verzia je 1.2.0 aj ked 1.3.0 je vo verzii RC a coskoro sa dockame final release.
http://www.springframework.net/
Enterprise Library Logging Block
EL obsahuje Logging block zodpovedny za logovanie a snazi sa nam integraciu co najviac ulahcit. Architektura je zalozena na pouziti trace listenerov (podobne ako to pozname z .NET a jeho tracing funkcionality).
Zakladne a zjednodusene kroky pre integrovanie logging bloku:
- nareferencujeme prislusne kniznice (Common a Logging z EL by mali stacit)
- nakonfigurujeme kategorie (xml), do ktorych budeme logovat
- nakonfigurujeme (xml), ktore trace listenery chceme pouzit (tu mame na vyber z viacerych pripravenych, vratane event log, e-mail, db, message queue, subor, WMI alebo vlastny trace listener.
- V kode vytvorime instanciu EventLog triedy, naplnime datami, ktore by sme radi zalogovali a zavolame staticku metodu Write Enterprise library.
Priklad pouzitia
var entry = new LogEntry()
{
// init
};
Microsoft.Practices.EnterpriseLibrary.Logging.Logger.Write(entry);
Zjednoduseny priklad konfiguracie (2 kategorie, druha loguje len do suboru, event log trace listener loguje len spravy s typom error a vyssie).
<loggingConfiguration name="Logging Application Block" tracingEnabled="true"
defaultCategory="" logWarningsWhenNoCategoriesMatch="true">
<listeners>
<add
fileName="trace.log"
formatter="Text Formatter"
listenerDataType="FlatFileTraceListenerData”
traceOutputOptions="None"
filter="All"
name="FlatFile TraceListener"
/>
<add
source="Enterprise Library Logging"
formatter="Text Formatter"
log="Application"
machineName=""
listenerDataType="FormattedEventLogTraceListenerData"
traceOutputOptions="None"
filter=”Error"
name="Formatted EventLog TraceListener"
/>
</listeners>
<formatters> … </formatters>
<categorySources>
<add switchValue=”All” name=”Errors”>
<listeners>
<add name=”Formatted EventLog TraceListener” />
<add name=” FlatFile TraceListener” />
</listeners>
</add>
<add switchValue=”All” name=”User Transactions”>
<listeners>
<add name=” FlatFile TraceListener” />
</listeners>
</add>
</categorySources>
</loggingConfiguration>
Z hore uvedeneho je jasne, ze EL Logging Block nam ponuka out-of-box riesenie, lahko integrovatelne a plne konfigurovatelne. Vo vaccine pripadov budeme chciet vytvorit vlastny trace listener, pretoze zabudovane listeneri nam nebudu stacit.
Budeme potrebovat podedit od triedy CustomTraceListener, odekorovat novu triedu atributom [ConfigurationElementType(typeof(CustomTraceListenerData))] a overridnut metodu TraceData (podla toho, co potrebujeme). Jej telo bude obsahovat nasu logiku pre spracovanie instancie LogEntry (napriklad ulozenie do db, odoslanie sms, a pod), ktora do metody prichadza ako parameter.
Zatial vsetko dobre. Kde to zacina haprovat je vykon. Ak vyvyjate aplikaciu, ktora si musi frcat (typicky priklad obsluhovat xyz requestov za sekundu), Logging blok nebude dobra volba. Nedavno sme stresstestovali jeden nas live webservis, kde sme pouzili prave logging blok a boli sme zial nemilo prekvapeny vysledkami. Co to som sa docital na webe ako je architektura az priliz komplikovana, ale tie cisla boli naozaj prekvapujuce.
Ako sme testovali?
Separatna siet, dedikovane databazy a 2 webservre a load balancer (aby sme sa co najviac priblizili k live enviromentu). webservis robi pomerne komplikovane penazne transakcie a je sucastou distribuovanych transakcii cez http, takze vykon je v nasom pripade ziaduci. Servis obsahuje niekolko funkcii, kazda rozne zatazuje system, od loginu az po samotne transakcie. Cely test bezal 3 hodiny.
Orientacne vysledkyPriemerny pocet obsluzenych req/s (vsetky metody) s Logging blokom: 211
Priemerny pocet obsluzenych req/s (vsetky metody) cez Logging bloku: 272
Kde je uzke miesto?Este doplnim, ze odhalenie uzkeho miesta nebolo az take trivialne, nakolko na aplikacii prebehol pomerne rozsiahly refactoring logovania, ktory predstavil enterprise library a z pociatku nebolo jasne, v com je problem. A tak prisiel cas pre .NET profiler a JetBrains dotTrace, vynikajuci tool, vrele doporucujem.
dotTrace odhalil, ze komunikacia v bode, ked loging bloku posleme objekt na zalogovanie do prislusnych trace listenerov (v nasom pripade to bola len databaza) trva velmi dlho. Cisla, ktore odhalil profiling:
- 60 volani Write statickej metody Loging bloku trvalo 16.254ms (2,32% celkoveho casu)
- 60 volani pripojeneho trace listenera (v ramci volanej metody Write), ktory data zapisal do databazy bolo dokopy 4.748ms (0,68% celkoveho casu)
Niekde sa nam straca viac ako 10.000ms, to je skutocne obrovske cislo.
PoucenieNakolko niektore bloky EL sa vo firme uspesne pouzivaju a samotny logging blok pouziva niekolko release teamov, boli sme malo opatrni a vyuzili predpripravene triedy a funkcnost ako priamu zavislost. Najma ked sa jednalo len o obycajne logovanie. Vymenit EL za nieco ine nebolo az take narocne, nakolko predchadzajuci refactoring vyriesil mnohe problemy a odtienil ostatnu cast kodu od implementacie logovania, ale aj tak sme si mohli usetrit cas odstranenim tejto zavislosti. Takze minimalizovat zavislosti aj od relativne nekomplikovanych casti programu. A teda interfaces a IoC.
RiesenieImplementovali sme vlastny jednoduchy loging blok. Mozno to znie divne, ale zvazme fakty:
- implementacia vznikla uz v ramci odhalenia vykonnostnych problemov loging bloku
- custom trace listner sme mali implementovany
- vykon
- vyberat a testovat ine existujuce frameworky, ktore navyse vo velkej firme ako nasa musi schvalit ista rada architektov, dalsie polienko pod nohy
- mame k dispozicii IoC kontajner
Ked sa tak pozrieme na hore popisany logging block z EL (a ostatny frameworky na tom budu nejako podobne), vacsinu funkcionality resp konfiguracie je napisana prave v .xml subore. Pockat, co nam este umoznuje pripravit nieco podobne? Nainicializovat a vratit objekt daneho typu. Ano spravne, IoC kontajner. V nasom pripade spring.net. V samostatnom subore nakonfigurujeme de facto to iste ako v konfiguracnom subore loging bloku.
V kode potom vytvorime 2 interfaces:
public interface ILoggingBlock
{
void Write(LogEntry data);
}
public interface ITraceListener
{
void TraceData(LogEntry data);
}
LogEntry bude nasa classa, mozeme sa inspirovat existujucimi rieseniami, napriklad takto:
public class LogEntry
{
public LogEntry()
{
this.Severity = TraceEventType.Error;
}
public object Data { get; set; }
public string Title { get; set; }
public string Message { get; set; }
public TraceEventType Severity { get; set; }
public int EventId { get; set; }
public ICollection Categories { get; set; }
}
Pricom do property Data si mozeme poslat nase custom data, ktore potom trace listner ulozi do db.
Ako bude vyzerat samotny logging blok, ktory okrem trace listeners podporuje aj kategorie? V jednoduchosti je krasa:
public class LoggingBlock : ILoggingBlock
{
public LoggingBlock(IDictionary> traceCategories)
{
this.TraceCategories = traceCategories;
}
#region Props
protected IDictionary> TraceCategories { get; set; }
public int TraceListenerFailedEventId { get; set; }
#endregion
#region ILoggingBlock Members
public void Write(LogEntry data)
{
foreach ( string category in data.Categories )
{
if ( !this.TraceCategories.ContainsKey(category) )
{
throw new Exception("Logging configuration does not contain category: " + category);
}
var traceListeners = this.TraceCategories[category];
foreach ( var tl in traceListeners )
{
try
{
tl.TraceData(data);
}
catch ( Exception ex )
{
System.Diagnostics.EventLog.WriteEntry(
"My Logging Block"
, "Warning: trace listener failed ("
+ ex.Message
+ "). Logging to other trace listeners will continue.”
+ Environment.NewLine
+ "Category: " + category
+ Environment.NewLine
+ ex.ToString()
, EventLogEntryType.Error
, TraceListenerFailedEventId
);
}
}
}
}
#endregion
}
A co trace listeners? Staci implementovat interface ITraceListener a v pripade napr windows event logu pouzit .NET building metodu System.Diagnostics.EventLog.WriteEntry(..). Cela praca na niekolko minut. Jedina komplikovanejsia cast ostava nakonfigurovanie IoC kontajnera. Ak ho v teame pouzivate, tak je to tiez brnkacka a vysledok bude prudko totozny z hore uvedenou konfiguraciou z EL. V buducnosti ak budeme chciet vymenit nas loging blok za nieco sofistikovanejsie, implementujeme interface ILoggingBlock a v konfiguracii IoC nastavime prislusne referencie a typy. To nam umozni menit loging bloky za behu, co je velmi pekne najma pocas stress testov adeptov na novu implementaciu funkcnosti. Samozrejme hore uvedena implementacia je trivialna ale je na com stavat. S pomocou IoC kontajnera nie je problem doplnit Filtre pre rozny typy LogEntry (error, warning, information, critical), event id pre jednotlive typy (aby napr nas MOM system vedel rozposlat varovne emaily, kde sa zaloguje vynimka (error) a pod). A to uz smeruje k pravdepodobne nejakemu peknemu existujucemu rieseniu, zalezi co aplikacia potrebuje. Pre nase servisy zatial viac nepotrebujeme a rychlost a jednoduchost je vyborna. Ak bude treba, vieme existujuce riesenie lahko integrovat a otestovat v porovnani s existujucim.
Na tomto mieste by som rad spomenul este jedno poucenie. Cele nastudovanie enterprise library, jej upgrade v ramci celej firmy z 4.0 na 4.1 (bug v buildin event log trace listeneri), jej integrovanie a konfigurovanie, testovanie a profilovanie zabralo pomerne dost casu oproti alternativnemu prezentovanemu rieseniu, resp obdobnej alternativy. 4.0 -> 4.1: dalsia nevyhoda uzavreteho projektu, kde cakame rok na opravenie banalnej chyby.
Vysledky reprezentuje druha hodnota zo stresstestov bez EL. Volania medzi ILoggingBlock a ITraceListener su standardne dlhe ako bezna komunikacia (s pretypovanim) v .NET. Priznam sa, ze neviem a ani sa mi to nechce zistovat, co sposobuje take zle casy v pripade EL.
Uceleny zoznam logovacich frameworkov mozeme najst napr tu:
http://www.dotnetlogging.com/Treba mat ale vzdy napamati rozsiritelnost, vykon, konfigurovatelnost, zdrojaky (opensource), aktivita v komunite. Opravte ma ak sa mylim, ale taky popularny log4net trochu zaspal dobu. Ake su vase skusenosti s logovacimi frameworkami?
ZaverSnazil som sa poukazat len na jedno z mnohych peknych vyuzitii IoC kontajnerov a vykonnostny problem Logging Blocku Enterprise library. Ako dalej zvysit rychlost logovania, ak je napr databaza bottleneck v pripade, ze musime toho logovat velmi vela (napr kvoli zakonom v danej krajine) za pouzitia messaging servisov (TIBCO, MSMQ) snad nabuduce.