Ve svém prvním příspěvku jsem to schytal za slovo "revoluční". Řekl bych, že trochu neprávem. Netvrdím, že jsou Entities něčím revolučním v globálním pohledu, ale něčím revolučním na datové platformě Microsoft (což je ADO, resp. ADO.NET). Spíše bych řekl, že Microsoft uplatnil svůj typický přístup - počkat si, jak se věci vyvinou, poučit se a pak to vzít za trochu jiný konec, čímž vznikne něco snadno použitelného, co obsáhne mnohem širší publikum (což Hibernate při vší úctě není, pro ne-profi vývojáře je to "trochu moc", čímž rozhodně nechci říct, že je sám o sobě špatný).
Zpět k objektovému modelu. Při práci s Entities je možné použít tři různé úrovně:
- Connection/Command/DataReader - tradiční přístup z relačního světa je možné uplatnit i proti virtuální databázi entit. Bohužel není tímto způsobem možné data modifikovat, pouze číst. Také to není vůbec komfortní, protože musíte psát Entity SQL příkazy ručně a výsledky dostanete netypově - musíte si přetypovat do svých typů. Asi to bude spíše využíváno v různých reportovacích a dalších nástrojích, které s daty pracují (a fungují s ADO.NET providery), než ve vašdm kódu
- eSQL dotazy proti kontextu entit s pomocí CreateQuery<T> - tímto způsobem se vám vrátí typový výsledek, se kterým můžete dál pracovat - modifikovat data, mazat je, přidávat přes kontext nová data atd. Dotaz je opět pomocí Entity SQL, tudíž ne moc komfortní. To ale může být někdy výhoda, např. když chceme dotaz dynamicky skládat z předem neznámého počtu fragmentů (filtrů, třídění apod.) - objektový model je na tento typ skládání uzpůsoben.
- LINQ dotazy proti kontextu, který zpřístupňuje jednotlivé entity sety jako typové kolekce - zdaleka nejkomfortnější práce s plnou podporou kontroly syntaxe při kompilaci, IntelliSense atd. Modifikace dat je stejná jako v předchozím případě. Tento způsob bude ve většině scénářů asi nejčastější.
Co všechno entities umí:
- Prakticky kompletní podpora LINQ konstrukcí - Select, Where, OrderBy, GroupBy, Join, ...
- Modifikace dat (vkládání, mazání, editace)
- Pozdní vyhodnocení dotazu - tak aby se minimalizovalo množství dat získávaných z databáze
- Object Identity - každá entita se v rámci kontextu natáhne pouze jednou (nemohou být dva objekty pro tutéž entitu)
- Předkompilaci dotazů pro rychlejší provádění
- Detekce konfliktů (optimistický přístup) - u sloupečků s nastaveným ConcurrencyMode.Fixed se při ukládání kontroluje, zda mi je někdo "pod rukama" nezměnil
- Transakce - běžným způsobem v .NET Frameworku - uzavření kódu do bloku using(TransactionScope)
- Kontrola načítání entit - při navigaci mezi entitami se entity natahují pouze na vyžádání. Chcete-li je "před-natáhnout" abyste minimalizovali počet dotazů do databáze, použijete Include() v dotazu (tzv. eager fetching)
Kód který všechny tyto možnosti ilustruje jsem nakopíroval na konec tohoto článku (využívá databázi AdventureWorksLT). Celé řešení pro Visual Studio 2008 + Entities Beta 3 je navíc v příloze.
Další čtení - http://www.code-magazine.com/article.aspx?quickid=0712042
Dále vyšlo:
P.S. Tohle je druhý příspěvek napsaný pomocí Live Writer. Jak jsem bez něj mohl dříve žít?
// K entitam lze pristupovat pomoci poskytovatelu a jazyka eSQL (jako k beznym relacnim datum) ???
using (EntityConnection conn = new EntityConnection(ConfigurationManager.ConnectionStrings["AdventureWorksLTEntities"].ConnectionString))
{
EntityCommand cmd = new EntityCommand("SELECT p.Name,p.ListPrice FROM AdventureWorksLTEntities.Products AS p WHERE p.ProductCategory.Name='Socks' AND p.ListPrice>@Cena ORDER BY p.ListPrice DESC", conn);
cmd.Parameters.AddWithValue("Cena", 9);
conn.Open();
EntityDataReader rdr = cmd.ExecuteReader(CommandBehavior.SequentialAccess);
while (rdr.Read())
{
Console.WriteLine("{0} ({1})", rdr["Name"].ToString(),rdr["ListPrice"].ToString());
}
conn.Close();
}
// Lepsi je ale ve vetsine pripadu pouzivat objektove sluzby
using (AdventureWorksLTEntities aw = new AdventureWorksLTEntities())
{
// Jednou moznosti je delat standardni eSQL dotazy
var dotazProdukty = aw.CreateQuery<Product>("SELECT VALUE p FROM Products AS p WHERE p.ProductCategory.Name='Socks' AND p.ListPrice>@Cena ORDER BY p.ListPrice DESC");
dotazProdukty.Parameters.Add(new ObjectParameter("Cena",9));
foreach(var p in dotazProdukty)
Console.WriteLine("{0} ({1})",p.Name,p.ListPrice);
// Druhou moznosti je pouzit postupny builder pro dotazy - umoznuje dynamicke skladani
var dotazProdukty2 = aw.CreateQuery<Product>("Products");
dotazProdukty2 = dotazProdukty2.Where("it.ListPrice>@Cena",new ObjectParameter("Cena",9));
dotazProdukty2 = dotazProdukty2.Where("it.ProductCategory.Name='Socks'");
dotazProdukty2 = dotazProdukty2.OrderBy("it.ListPrice DESC");
foreach (var p in dotazProdukty2)
Console.WriteLine("{0} ({1})", p.Name, p.ListPrice);
// Treti, casto nejpohodlnejsi je delat standardni LINQ dotazy a vyuzivat relace mezi daty
// Stejne jako v LINQ - bud pomoci klicovych slov anebo extenznich funkci
var produkty1 = from p in aw.Products where p.DiscontinuedDate == null orderby p.ListPrice select p.Name;
var produkty2 = aw.Products.Where(p => p.DiscontinuedDate == null).OrderBy(p => p.ListPrice).Select(p => p.Name);
//// Mozno vytvaret hierarchie anebo ploche struktury
var plocha1 = from p in aw.Products where p.ProductCategory.Name.Contains("Bikes") select new { p.Name, Category = p.ProductCategory.Name };
var plocha2 = aw.Products.Where(p => p.ProductCategory.Name.Contains("Bikes")).Select(p => new { p.Name, Category = p.ProductCategory.Name });
var hierarchie1 = from c in aw.ProductCategories where c.Name.Contains("Bikes") select new { c.Name, c.Products };
var hierarchie2 = aw.ProductCategories.Where(c => c.Name.Contains("Bikes")).Select(c => new { c.Name, c.Products });
//// Pokud je realace v databazi, je vyhodne ji primo vyuzit, jinak je mozne pouzit join (dost umely priklad :-)
var dvojiceAdres = from a1 in aw.Addresses join a2 in aw.Addresses on a1.City equals a2.City select new { a1, a2 };
// Samozrejme je mozne manipulovat s daty
// Pridani entity
Product hrnek = new Product() { Name = "Bily hrnecek", ProductNumber = "HR-BI-007", SellStartDate = DateTime.Now, SellEndDate = DateTime.Now.AddYears(1), ModifiedDate = DateTime.Now };
aw.AddToProducts(hrnek);
aw.SaveChanges();
// Editace entity a zaroven presun v ramci referencni integrity
Product hrnek2 = aw.Products.First(p => p.ProductNumber == "HR-BI-007");
ProductCategory prislusenstvi = aw.ProductCategories.First(c => c.Name == "Accessories");
hrnek2.SellEndDate += TimeSpan.FromDays(60);
hrnek2.ProductCategory = prislusenstvi;
aw.SaveChanges();
// Vymazani entity z databaze
Product hrnek3 = aw.Products.First(p => p.ProductNumber == "HR-BI-007");
aw.DeleteObject(hrnek3);
aw.SaveChanges();
// Deffered Execution - vyhodnoceni se odklada az na posledni mozny okamzik, aby se minimalizovalo mnozstvi nactenych dat
var kola = from p in aw.Products where p.ProductCategory.Name.Contains("Bikes") select p;
var drahaKola = from p in kola where (p.ListPrice > 3000) && (p.DiscontinuedDate == null) orderby p.Name select new { p.Name, p.ListPrice };
var seznamDrahychKol = drahaKola.ToList(); //az ted se pusti dotaz
// Object Identity - sleduje se identita objektu, jedna entita v databazi = jeden objekt v pameti
var kategorie1 = aw.ProductCategories.First(c => c.Name == "Accessories");
var kategorie2 = aw.ProductCategories.First(c => c.Name == "Accessories");
bool jsouStejne = object.ReferenceEquals(kategorie1, kategorie2);
// Compiled query - pokud se vyuziva opakovane, setri to cas na vyhodnoceni
Func<AdventureWorksLTEntities, string, IQueryable<Product>> productsByCategory =
CompiledQuery.Compile((AdventureWorksLTEntities db, string categoryName) =>
from p in db.Products where p.ProductCategory.Name.Contains(categoryName) select p);
var p1 = productsByCategory(aw, "Shorts");
var p2 = productsByCategory(aw, "Bikes");
// Optimistic concurrency
// Konflikt se detekuje na zaklade vlastnosti ConcurrencyMode.Fixed u vlastnosti Name, u ostatnich neni kontrola!
try
{
ProductCategory c = new ProductCategory() { Name = "Pokus", ModifiedDate = DateTime.Now };
aw.AddToProductCategories(c);
aw.SaveChanges();
// Simulace vyvolani zmen mimo tento datovy kontext
using (AdventureWorksLTEntities aw2 = new AdventureWorksLTEntities())
{
var pokus = aw2.ProductCategories.First(p => p.Name == "Pokus");
pokus.Name="Pokus2";
pokus.ModifiedDate=DateTime.Now;
aw2.SaveChanges();
}
// Tato editace skonci konfliktem
c.Name = "Pokus3";
aw.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
foreach (var se in ex.StateEntries)
{
Console.WriteLine("ID ={0}", ((ProductCategory)se.Entity).ProductCategoryID);
Console.WriteLine("BEFORE REFRESH");
for (int i = 0; i < se.CurrentValues.FieldCount;i++ )
Console.WriteLine(" {0} - current:{1}, original:{2}", se.CurrentValues.GetName(i), se.CurrentValues
.ToString(), se.OriginalValues
.ToString());
aw.Refresh(RefreshMode.ClientWins, se.Entity);
Console.WriteLine("AFTER REFRESH");
for (int i = 0; i < se.CurrentValues.FieldCount; i++)
Console.WriteLine(" {0} - current:{1}, original:{2}", se.CurrentValues.GetName(i), se.CurrentValues
.ToString(), se.OriginalValues
.ToString());
}
aw.SaveChanges(); // druhy pokus
}
finally //uklid
{
ProductCategory pokus = aw.ProductCategories.First(c => c.Name.Contains("Pokus"));
if (pokus != null)
{
aw.DeleteObject(pokus);
aw.SaveChanges();
}
}
// Transakce je mozne explicitne ridit - context ma vlastnost Connection.Transaction
// ale doporucene je pouzit misto toho TransactionScope, napr. takto
using (TransactionScope ts = new TransactionScope())
{
aw.SaveChanges();
// Sem nejake dalsi zmeny v datech mimo tento DataContext
ts.Complete();
}
// Immediate Loading - efektivnejsi nacitani, pokud vim, ze bude treba.
// Zde se nacitaji objednavky i detaily v jednom dotazu.
var objednavky1 = from o in aw.SalesOrderHeaders select new { o.SalesOrderNumber, o.OrderDate, o.SalesOrderDetails };
// ale zde ne, detaily kazde objednavky je nutne nacitat jednotlive
var objednavky2 = from o in aw.SalesOrderHeaders select o;
foreach (var o in objednavky2)
{
Console.WriteLine("{0} ({1})", o.SalesOrderNumber, o.OrderDate);
o.SalesOrderDetails.Load();
foreach (SalesOrderDetail d in o.SalesOrderDetails)
{
d.ProductReference.Load();
Console.WriteLine("{0} (1)", d.Product.Name, d.OrderQty);
}
}
// Pro optimalizaci je mozne pouzit tzv. eager fetching, pomoci Include()
var objednavky3 = from o in aw.SalesOrderHeaders.Include("SalesOrderDetails") select o;
}