Enterprise Modeling Anti-Patterns
February 1st, 2010Link.
Most tervezek egy cégnek egy nagyobb architektúrát, és közben próbálom szem előtt tartani ezt a listát, könnyű beleesni a benne szereplő hibákba.
Link.
Most tervezek egy cégnek egy nagyobb architektúrát, és közben próbálom szem előtt tartani ezt a listát, könnyű beleesni a benne szereplő hibákba.
A következőn töröm a fejem. Az Entity Framework SSDL-jében definiálva vannak az entitás property-k alapvető jellemzői: nullázhatóság, max hossz. Ezeket a GUI-n ki kell kényszeríteni. Nyilván vannak összetettebb validálási szabályok, de most koncentráljunk ezekre az elemiekre.
Utálok minden redundanciát egy rendszerben, ezért azt gondoltam, a szabályokat kiolvasom az EF sémájából, és ebből táplálom meg a validáló részeket, így nem kell törődni az egyszerű validálásokkal, automatikusan működni fognak.
A következő kis kódocska mutatja meg a metaadatok használatát:
o.ForceLoadingSchemas();
var sspaceEntitySets = o.MetadataWorkspace
.GetItems<EntityContainer>(DataSpace.SSpace)
.First().BaseEntitySets.OfType<EntitySet>();
foreach (EntitySet es in sspaceEntitySets)
{
foreach (EdmProperty p in es.ElementType.Properties)
{
ReadOnlyMetadataCollection<Facet> facets = p.TypeUsage.Facets;
Debug.WriteLine("{0} is {1} nullable", p.Name, (bool)facets["Nullable"].Value ? "" : "not");
if (facets.Contains("MaxLength"))
{
Debug.WriteLine("{0} MaxLenght is {1}", p.Name, (int)facets["MaxLength"].Value);
}
Debug.WriteLine("{0} is {1} nullable", p.Name, (bool)facets["Nullable"].Value ? "" : "not");
}
}
A ForceLoadingSchemas az ObjectContext partial classában van:
public void ForceLoadingSchemas()
{
CreateQuery<BusinessEntity>("AdventureWorks2008Entities3.BusinessEntities").ToTraceString();
}
Csinált már valaki ilyet? Van benne valami csapda, amit most nem látok?
Error: Incorrect syntax near valami.
Akkor jön elő, ha az SQLCLR assemblyt és benne a függvényeket akarja az VS deployolni. Több oka lehet, most az volt, hogy egy .NET oldalon double-t visszaadó függvény véletlenül így lett deklarálva:
[SqlFunction(..., TableDefinition = "Datum datetime, Szazalek double")]
Mi a hiba benne? SQL Serverben nincs double, csak real és float. Ráadásul a C# float az az SQL real és a C# double az SQL Server float (kb.). :)
Az előbbi helyesen:
[SqlFunction(..., TableDefinition = "Datum datetime, Szazalek float")]
Miért kellett SQLCLR függvényt írni? A futó aggregálások (én legalábbis nem tudok jobbat kurzor nélkül) o(n2)-es algoritmusok, ezt CLR-ben könnyen meg lehet írni o(n)-re. Pl:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public class UserDefinedFunctions
{
internal class Result
{
public long RowId;
public double CumDollarGain;
public double TopDollarGain;
public double DollarDrawDown;
}
//Input table: create table #trades(RowId long, DollarGain money, other columns possible)
[SqlFunction(FillRowMethodName = "FillRow", DataAccess = DataAccessKind.Read,
TableDefinition = "RowId bigint, CumDollarGain float, TopDollarGain float, DollarDrawDown float")]
public static IEnumerable Cumul()
{
using (var conn = new SqlConnection("Context connection=true"))
{
using (var cmd = new SqlCommand("select RowID, DollarGain from #trades", conn))
{
var res = new List<Result>();
conn.Open();
double cumulPrice = 0, topPrice = 0, drawDawn = 0;
using (SqlDataReader r = cmd.ExecuteReader())
{
int idCol = r.GetOrdinal("RowID");
int gainCol = r.GetOrdinal("DollarGain");
while (r.Read())
{
var price = r.GetDouble(gainCol);
cumulPrice += price;
topPrice = Math.Max(price, topPrice);
drawDawn += price;
drawDawn = Math.Min(drawDawn, 0);
res.Add(new Result
{
RowId = r.GetInt64(idCol),
CumDollarGain = cumulPrice,
TopDollarGain = topPrice,
DollarDrawDown = drawDawn
});
}
return res;
}
}
}
}
public static void FillRow(object obj,
out long id,
out double cumDollarGain,
out double topDollarGain,
out double dollarDrawDown)
{
var r = (Result)obj;
id = r.RowId;
cumDollarGain = r.CumDollarGain;
topDollarGain = r.TopDollarGain;
dollarDrawDown = r.DollarDrawDown;
}
};
Sajnos nem lehet átpasszolni a megnyitott SqlDataReadert a két metódus között, ezért kénytelen az ember letárolni az eredményhalmazt. Persze pár ezer sornál ez nem gond.
Imádom, nagyon ügyes dolog. Csak azért nem hajtotta ki jobban a procikat, mert már nem győzték a diszkek.
Érdekes problémaként még az jött elő, hogy több mint 100 szálat indított a Parallel, mert úgy érezte ez jó lesz, de az Sql Server connection poolja alapban max. 100 kapcsolatot engedélyez, így egyes szálak bedugultak. Lejjebb lehet venni a szálak számát, vagy feljebb a poolt. Nekem 80 szál elég volt, úgyse győzte már az SQL Server az adatokat.
Hogyan építsünk EF-re többrétegű appot?
1. Anti-Patterns To Avoid In N-Tier Applications
2. N-Tier Application Patterns
3. Building N-Tier Apps with EF4
Jó cikkek.
Na, ez aztán a furcsa. Pár napja írtam, mennyire lassan indul az EF. Nem tudom mitől, talán attól, hogy felraktam a Microsoft ADO.NET Entity Framework Feature Community Technology Preview 2-t, ami lecserélte a bugos .NET 4 Beta2-es EF assemblyket, amelyek nem használták ki az előfordított viewkat? Nem tudom, nem nyomoztam utána, mert most jó. :)
Korábban annyira felbosszantott, hogy elkezdtem nézni az NHibernate-et, de ahhoz meg a LINQ támogatás jár még gyerekcipőben, nekem meg tele van azzal a programom. Így az kiesett.
Marad az EF, így már nagyon szeretem.
Talán 7. éve, már nem is emlékszem az elejére. Köszönet az MS-nek a megtiszteltetésért.
Boldog Új Évet.
Ez az Entity Framework egy nehezen betörhető ló. A run-time teljesítménye jó, de az indulása, az katasztrófa.
Pár hasznos link a témában.
Alapelvek 1, 2.
Hol találhatunk fogást rajta?.
Viewk előre generálása:
Régi módon.
Eszköz hozzá: EdmGen.
Új módon:
How to use a T4 template for View Generation
Nagy modellek esetén hogyan ne őrüljünk meg (meg fogunk) 1, 2, gyakorlati példa.
Nem küldök push jellegű üdvözlő emaileket, úgyis mindenkinek tele lesz a postaládája, inkább így, pull módon kívánok mindenkinek boldog, békés Karácsonyt. Gyerekeseknek meg betegségmenteset, a Karácsony sok gyereknél betegségeket hoz elő, mi eddig megúsztuk. :)
A feladvány, hogy van sok szűrési feltétel, ezekre szűrni kell, ha van érvényes értékük, vagy kihagyni a szűrésből, ha nincs.
A feladatra SQL Server esetén bool algebrás megoldást írnék:
select * from tabla where (@p1 is null) or (@p1 = col1) and (@p2 is null) or (@p2 = col2) ... option(recompile)
Az option hint azért kell, mert így a nullos paraméterek teljesen kiesnek a tervből, csak a valódi szűrésekre készül terv. Ez szerintem óriási dolog, érdemes észben tartani.
Ugyanezt a logikát linqval is el lehet játszani, ref típusokkal kb. így:
var x = from z in Valami where (p1 == null) || (p1 = z.col1) && (p2 == null) || (p2 = z.col2)
Egy másik megoldásban azt használjuk ki, hogy késleltetett módon értékelődnek ki a kifejezések, így lehet őket láncolni. Itt látható ez a megoldás, és még más megközelítések is.
A harmadik megoldásban írhatunk egy saját szűrő operátort is erre a célra. Az előbbi címről:
public static IQueryable<TSource> WhereIf<TSource>(
this IQueryable<TSource> source, bool condition,
Expression<Func<TSource, bool>> predicate)
{
if (condition)
return source.Where(predicate);
else
return source;
}
Nehéz megindokolni, melyik megoldás a jobb.
120 oldalas kis olvasmány, alig várom már, hogy legyen egy kis időm elolvasni.
Az tetszik a 4.0-ban, hogy végre megint hozzányúltak az alapokhoz, mint pl. a threadinghez.
Imádom a WinDbg-t, mondtam már? Az egyik kis programom módszeresen eszegette a memóriát. Ciklusban végez feldolgozást, rettentő sok adattal, és ezek egy része szépen bennragadt a memóriában. .NET-ben nincs memory leak a klasszikus értelemben, de van oly módon, hogy a rootokból marad referencia egyes objektumokra, így azok élve maradnak, szándékaink ellenére. Néha azonban nem triviális kinyomozni, melyik root miatt ragadt be valami a memóriába.
A VSTS profiler segítségével odáig el lehet jutni, hogy ki ragad be a memóriába. Az Object LifeTime nézetben az Instances alive at end oszlopra rendeztetve láthatjuk, kik maradnak élve a program végén. Én beraktam egy force-olt GC-zést a program végére, így aki ezek után még benn maradt, azok egy része szándékaim ellenére tette ezt, ezért azt leaknek tekintem, és meg kell szüntetni.
Jöhet a WinDbg. File, Open Executable, F5. A program végén a GC után raktam még egy Console.ReadLine()-t. Amikor ide eljut, a debuggerben CTRL-Breakkel megállítom a program futását (várakozását). Betöltöm az sos-t:
.load C:\Windows\Microsoft.NET\Framework64\v4.0.21006\sos.dll
Kilistáztatom a GC heapen levő ojjektumokat:
!DumpHeap -stat
000007ff005c2278 2766 221280 System.Data.Metadata.Edm.TypeUsage 000007fef1b90d00 370 222136 System.Byte[] 000007fef1b89be0 13379 1330896 System.String 000007fef1b8c8a8 1125 1447552 System.Int32[] 000007fef1b3bef0 9298 3224392 System.Object[] 000007ff00274988 245954 29514480 ATS.Bar
Akinek nem szabadna már a memóriában lenni, az az ATS.Bar objektumok, 245954 darab, 29514480 bájt méretben. Nézzük megy a példányokat belőle:
!DumpHeap -type ATS.Bar
0000000004952c60 000007ff00274988 120
0000000004952cd8 000007ff00274988 120
0000000004952d50 000007ff00274988 120
0000000004952dc8 000007ff00274988 120
0000000004952e40 000007ff00274988 120
0000000004952eb8 000007ff00274988 120
total 0 objects
Statistics:
MT Count TotalSize Class Name
000007ff00fb38a0 1 24 System.Collections.Generic.GenericEqualityComparer`1[[ATS.BarCollectionDescriptor, Common]]
000007ff00271050 1 24 ATS.BarDAL
000007ff00270e68 1 48 ATS.BarFactory
000007ff00fb31b8 1 88 System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]], mscorlib]]
000007ff00fb27a8 1 88 System.Collections.Generic.Dictionary`2[[ATS.BarCollectionDescriptor, Common],[ATS.BarFromTickFactory, Common]]
000007ff00275378 1 88 System.Collections.Generic.Dictionary`2[[ATS.Symbol, Common],[ATS.BarCollectionByInterval, Common]]
000007ff00fb3ee0 1 96 System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]], mscorlib]][]
000007ff00fb3b10 1 96 System.Collections.Generic.Dictionary`2+Entry[[ATS.BarCollectionDescriptor, Common],[ATS.BarFromTickFactory, Common]][]
000007ff00fb0498 1 96 System.Collections.Generic.Dictionary`2+Entry[[ATS.Symbol, Common],[ATS.BarCollectionByInterval, Common]][]
000007ff00fb2d30 3 120 System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]]
000007ff00790990 3 120 ATS.BarCollectionDescriptor
000007ff00fb1568 7 168 System.Collections.ObjectModel.ObservableCollection`1+SimpleMonitor[[ATS.Bar, Common]]
000007ff00886530 3 240 ATS.BarFromTickFactory
000007ff00fb1648 7 280 System.Collections.Generic.List`1[[ATS.Bar, Common]]
000007ff00273788 7 952 ATS.BarCollection
000007ff00274988 245954 29514480 ATS.Bar
Total 245993 objects
A kis 120 bájtos izék a problémásak (a végén az összefoglaló azért tartalmaz több típust is, mert substring szűrést csinál a -type). Az első oszlop az egyedi objektumok címe, a második a típus metódusleíró táblája.
És most jön a lényeg. Ki miatt érhető el a rootokból mondjuk az utolsó Bar példány?
!GCRoot 0000000004952eb8
DOMAIN(00000000003CF530):HANDLE(Pinned):1217d8:Root: 0000000012657048(System.Object[])-> 0000000002bf7f28(System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]], mscorlib]])-> 0000000002bf94b8(System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]], mscorlib]][])-> 0000000002f06ca0(System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]])-> 0000000002f06cc8(System.Object[])-> 0000000002f05860(ATS.BarFromTickFactory)-> 0000000004952eb8(ATS.Bar)
Ebben az látszik, az első bejegyzésből, amit még a CLR startup hozzott létre hogyan lehet eljutni a beragadt objektumunkhoz. A CLR generikus neveket C#-ra visszafordítva az látszik, hogy egy Dictionary
Ez alapján már rekonstruálható a probléma forrása. A probléma elemi oka, hogy túlzásba vittem a statikusok használatát, és nem figyeltem a takarításukra.
static readonly Dictionary<BarCollectionDescriptor, BarFromTickFactory> FactoriesByDesc =
new Dictionary<BarCollectionDescriptor, BarFromTickFactory>();
Ebből rendesen kiszedtem a tárolt BarFromTickFactory példányt, ha már nem volt rá szükség. De elfeledkeztem róla, hogy volt egy másik kollekció is:
public static readonly Dictionary<int, List<BarFromTickFactory>> FactoryListBySymbolId =
new Dictionary<int, List<BarFromTickFactory>>();
Ebből viszont nem szedtem ki a hivatkozás az adott BarFromTickFactory-ra, így az szépen beragadt a memóriába.
Tanulságok:
1. Szeretjük a WinDbt-t.
2. Kerüljük a statikusokat. Ha a BarFromTickFactory példányokat egy mások osztály példányai tárolnák, akkor ha azok kifutnak a szkópból, automatikusan a GC martalékai lesznek. A sok statikus sok odafigyelést igényel, kár erőltetni őket.
Két hete vettem a fáradtságot, és felraktam az eddigi Windows 2008 helyére a Windows 7-et. (Kicsit nehezen szántam rá magam, mert minden OS váltás után vagy egy hét mire minden újra a helyére kerül.)
Hamarosan észrevettem, hogy nagyon belassul a gép. A Resource Monitor CPU fülén a Processes fejlécen látható egy érték, % Maximum Frequency. Ez azt jelzi, mennyire vette vissza a Windows (vagy a BIOS?, nem tudom) a proci frekvenciáját. A belassulások idején ez kb. 20%-on állt, és nem akar feljebb menni. Bosszantó, leszabályozza a Windows a proci sebességemet. Mivel nem elégedtem meg egy egyszerű k. Windows beszólással (másképp mehetnék HUP tagnak), utánanéztem a dolognak. Azt írják, ha a gép hűtésével van gond, akkor történhet ilyen. Volt, pl., akinek leállt a proci ventilátora.
Tesztként kikapcsoltam a BIOS-ban a SpeedStepet, ettől mindig 100%-on volt a sebesség, de egész nap süvített a gép, és lesütötte a t.met is.
Végül bekapcsoltam a kompresszort, és alaposan kifújattam a port a gépből. Azóta hideg a gép, mégis, ha kell, felhúzza a Windows 100%-ra a procisebességet (a BIOS-ban újra engedélyeztem a SpeedStepet).
Terjesszétek a jó hírt, ha belassul a gép, tessék alaposan kiporolni.
Az előző bejegyzéshez kapcsolódik még az alábbi. Sok felesleges memóriaallokállást és aztán GC-zést okoztam az alábbi kóddal:
...
OnBarArrived(new BarArrivedEventArgs(bar));
...
private void OnBarArrived(BarArrivedEventArgs e)
{
if (BarArrived != null)
{
BarArrived(this, e);
}
}
Ha a BarArrived event null, azaz nem iratkozott fel senki az eventre, akkor feleslegesen hozok létre egy BarArrivedEventArgs-ot. A javított verzió így néz ki:
private void OnBarArrived(Bar bar)
{
if (BarArrived != null)
{
BarArrived(this, new BarArrivedEventArgs(bar));
}
}
A bar objektum már úgyis kész van, az eventargot viszont csak akkor hozom létre, hogy tényleg szükség van rá.
Két hete elkezdtem intenzíven használni a Visual Studio 2010-et, mivel szükségem volt a MemoryMappedFiles-ra a 4.0-s fwből. Ezt shared memoryként használom, amivel a laptopon kb. 1GByte/sec-kel tudok adatokat másolni két processz között, így kiváló cache-t tudtam építeni a segítségével. De most nem erről lesz szó.
A profilert is jelentősen továbbfejlesztették, van benne pl. rendes memória allokálás követés (lehet, hogy a régi is tudta ezt, akkor pardon).
Nézzük az alábbi képet:
A SecondInterval.Day property-t közel 6 milliószor hívtam meg, minden híváskor előállítva egy új ojjektumot, 140MByte-nyi szemetet hagyva magam után.
A naív implementáció így nézett ki:
public static SecondInterval Day
{
get { return new SecondInterval(86400); }
}
Maga a típus egy egyszerű Whole Value pattern implementáció, egy sima immutable objektum, ráadásul class, így referenciális típus.
Nem memóriapazarló módon így néz ki a property:
private static readonly SecondInterval aDay = new SecondInterval(86400);
public static SecondInterval Day
{
get { return aDay ; }
}
A readonly fontos, hogy nehogy valaki kiüsse a referenciámat, és berakjon egy sunyi másik időt reprezentáló ojjektumot a napi helyére. Az ojjektum immutable, ez fontos, másként semmit nem érne a readonly, a gyomrát lehetne piszkálgatni.
Amin még el lehetne gondolkodni, hogy struktúrává, value type-pá átalakítani a típust, csak akkor meg minden helyen ahol használom, másolni kellene az értékét. Mivel ez most belül egy 32 bites int, és 64 bit alatt futtatom, ahol a referenciák 64 bitesek, a másolással még mindig jobban járok. Úgyhogy lehet, hogy struct lesz, de előbb megnézem classként hogy muzsuikál.
A galamblelkűek, a diszkrimináltak. A forum.index.hu lehalt, az index.hu is akadozik.
Óriási a témakör, két infómorzsa:
AntiXSS, nem házibarkács ellenőrzés (tudomásom szerint MS is ezt használja belül).
Ezeket érdemes megnézni, érdekesek.
Ha nem szeretnénk, hogy egy rövid, de teljesítményzabáló kódunkat megszakítsa a GC: Latency Modes és Low-Latency GC in .NET 3.5.
Ha interopolunk, és várunk visszahívást nem managed oldalról, de félünk, hogy a GC felszabadítja az ojjektumunkat visszahívás előtt: GC.KeepAlive.
Ha nagyon sok memóriát zabáló appot írunk, és nem szeretnénk belefutni OutOfMemoryException-be, inkább lassítanánk a feldolgozáson: MemoryFailPoint.
1, 2, 3, 4. Nem csak a szokásos, közismert dolgok, hanem mélyebbek is.
Pl. tudod-e, mekkora memóriát kér el induláskor a GC az OS-től? Hogy 3-féle GC is van (2-t ismer a legtöbb ember, már, aki egyáltalán hallott róla, hogy többféle GC is létezik)? Vagy miért száll el általában egy x86-os .net app 1.5G körüli memóriafelhasználásnál? Stb.
Az SQL Serveren szokásos orderby newid() szerveroldali megoldást csak linq to SQL esetén tudjuk kihasználni, szerveroldali függvénnyel. Az EF-ben az ilyesmi nem megy:
... orderby Guid.NewID() ...
Ez az EF verzió még nem tudja lefordítani szerveroldali kifejezéssé az orderby kifejezését.
Viszont a random rendezést át lehet nyomni kliensoldalra, ha a lekérdezést “materializáluk” (AsEnumerable) előbb:
Random rnd = new Random(); (from s in ATSEntities.Instance.Symbol select s).AsEnumerable().OrderBy(o => rnd.Next()));
Nem guidot használtam, hanem Randomot, az kisebb költségű, és az én célomra nem baj, ha csak pszeudo-random a sorrend.