Asi nejpřínosnější technologií z rodiny LINQ je LINQ to SQL, o kterém jsem v přehledu všech LINQů napsal:

"LINQ to SQL, dříve nazývaný DLINQ. V podstatě je to objektově-relační mapper, který je napsán tak, aby šlo s daty manipulovat pomocí základních LINQ funkcí. Místo IEnumerable<T> používá IQueryable<T>, což má naprosto zásadní vliv na jeho funkci - proč si povíme někdy příště. Nachází se v knihovně System.Data.Linq."

Nejprve moje subjektivní zkušenost - už dlouho jsem nenarazil na technologii, která by mi přisla tak užitečná a tak snadno bych se ji naučil používat. LINQ to SQL je "LINQ-friendly" objektově-relační mapper mezi .NET frameworkem a databází SQL 2000/2005 (ne, jiné databáze nejsou a nebudou podporovány, což změní až ADO.NET Entities v budoucnu). LINQ to SQL vám umožní zcela se vyhnout používání T-SQL ve vaší datové vrstvě, místo toho můžete vše psát v C#/VB.NET a bude to automaticky přemapováno do příslušných SQL příkazů.

To bude zřejmě mít nějakou výkonností režii. Kupodivu je mnohem nižší než byste čekali. Oproti úvodním CTP, které používaly reflexi, došlo k mnoha vylepšením a výkonnostní režie je relativně malá. Koho by to zajímalo hlouběji, Rico Mariani o tom napsal 5-ti dílný seriál (viz zde a zde), čísla jsou obsažena zejména v části 4. SELECT z databáze je při kompilaci dotazu zcela srovnatelný se surovým DataReaderem a celkový výkon je opravdu více než uspokojivý.

LINQ to SQL lze používat dvěma způsoby. První je "code first", kdy si nejprve vytvoříte definice tříd opatřené příslušnými atributy a z něj si necháte dynamicky vygenerovat databázi. Druhý je "database first", kdy si tabulky, pohledy, uložené procedury a funkce v existující databázi necháte obalit vygenerovanými třídami kódu. Praktičtější mi přijde druhý přístup, zejména díky Visual Studiu 2008, kde je pěkný vizuální návrhář. Do něj si přetáhnete myší existující objekty z databáze (vzniká tak takzvaný mapovací soubor s příponou .dbml), ze kterého studio generuje příslušné třídy. Vygenerovaný kód jsou parciální třídy, do kterých si tudíž můžete doplnit vlastní funkčnost bez obavy, že vám je návhář přepíše. Ke generování lze použít i nástroj SqlMetal.exe.

Dlužím vysvětlení, proč je tak důležité použití IQueryable<T> - je to kvůli tzv. deffered query execution. Kdyby jej nebylo, byl by celý LINQ to SQL nepoužitelně neefektivní. Představte si následující kód:

                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

Kdyby se dotaz vyhodnocoval již na prvním řádku, načetla by se do proměnné kola všechna kola. A to i přesto, že většina z nich nikdy nebude potřeba. Ve skutečnosti si LINQ to SQL pomocí řetězení IQueryable rozhraní vytváří strom jednotlivých vyhodnocovacích konstrukcí, který je přeložen do T-SQL dotazu a vykonán až v poslední možné chvíli (zpravidla při procházení pomocí foreach anebo při konverzi na pole či seznam). T-SQL, které se pouští proti databázi tak vrací pouze minimum potřebných dat, a je tedy (jak lze doufat) optimální. Mimochodem, když kód ladíte a najedete si myší na proměnnou, vidíte vygenerovaný SQL dotaz - pak lze kliknout levým tlačítkem myši a dát Copy Value a zkopírovat si to třeba někam do Notepadu. Když si proměnnou rozbalíte, uvidíte řádek s výsledky a upozorněním, že se dotaz vykoná když se jej pokusíte rozbalit. Celkem pěkné. Mimochodem, Scott Guthrie nabízí ještě lepší vizualizér jako add-in do Visual Studia 2008.

Pokud se chcete dozvědět více, velmi pěkný, aktuální a podrobný článek je zde.

Následující kód ukazuje tyto možnosti:

  • Výběr entit z databáze pomocí LINQ konstrukcí
  • Manipulace s daty - vložení, aktualizace, vymazání, změna relace
  • Volání uložené procedury přes metodu na DataContext objektu
  • Ukázka deferred execution - dotaz se spouští v poslední možné chvíli
  • Demonstrace identity objektů - jeden řádek v databázi => jediná instance objektu
  • Kompilace query, tak aby se eleminovala část režie a následné provádění bylo rychlejší
  • Detekce konfliktu a jeho vyřešení, pokud mi někdo změní data "pod rukama"
  • Vlastní řízení transakcí
  • Immediate Loading - natažení dat, o kterých předem vím, že budou v budoucnu třeba, aby se snížil počet dotazů do databáze

Celé řešení je v příloze, zde pouze nejdůležitější fragment kódu (používá databázi AdventureWorksLT):

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Data.Linq;

using System.Transactions;

 

namespace LINQtoSQL

{

    class Program

    {

        static void Main(string[] args)

        {

            string connString = Properties.Settings.Default.AdventureWorksLT_DataConnectionString;

            using (AdventureWorksLTDataContext aw = new AdventureWorksLTDataContext(connString))

            {

                // Je mozne 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.Products.Add(hrnek);

                aw.SubmitChanges();

                // 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.SubmitChanges();

                // Vymazani entity z databaze

                Product hrnek3 = aw.Products.First(p => p.ProductNumber == "HR-BI-007");

                aw.Products.Remove(hrnek3);

                aw.SubmitChanges();

 

                // Je mozne tez volat funkce a ulozene procedury v databazi pomoci metod na DataContextu

                var zakaznik22 = aw.ufnGetCustomerInformation(22);

 

                // 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<AdventureWorksLTDataContext, string, IQueryable<Product>> productsByCategory =

                    CompiledQuery.Compile((AdventureWorksLTDataContext 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 UpdateCheck u sloupce

                try

                {

                    ProductCategory c = new ProductCategory() { Name = "Pokus", ModifiedDate=DateTime.Now };

                    aw.ProductCategories.Add(c);

                    aw.SubmitChanges();

                    // Simulace vyvolani zmen mimo tento datovy kontext

                    aw.ExecuteCommand("UPDATE SalesLT.ProductCategory SET Name='Pokus2' WHERE Name='Pokus'");

                    // Tato editace skonci konfliktem

                    c.Name = "Pokus3";

                    aw.SubmitChanges(ConflictMode.FailOnFirstConflict);

                }

                catch(ChangeConflictException)

                {

                    foreach (var c in aw.ChangeConflicts)

                    {

                        Console.WriteLine("ID ={0}",((ProductCategory) c.Object).ProductCategoryID);

                        foreach (var mc in c.MemberConflicts)

                            Console.WriteLine("{0}: {1}-{2}-{3}", mc.Member.Name, mc.OriginalValue, mc.DatabaseValue, mc.CurrentValue);

                        c.Resolve(RefreshMode.KeepCurrentValues);

                    }

                    aw.SubmitChanges(); // druhy pokus

                }

                finally //uklid

                {

                    ProductCategory pokus = aw.ProductCategories.First(c => c.Name.Contains("Pokus"));

                    if (pokus != null)

                    {

                        aw.ProductCategories.Remove(pokus);

                        aw.SubmitChanges();

                    }

                }

               

                // Transakce je mozne explicitne ridit - DataContext ma vlastnost Transaction

                // ale doporucene je pouzit misto toho TransactionScope, napr. takto

                using (TransactionScope ts = new TransactionScope())

                {

                    aw.SubmitChanges();

                    // 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 se nacitaji jednotlive

                var objednavky2 = from o in aw.SalesOrderHeaders select o;

                foreach (var o in objednavky2)

                {

                    Console.WriteLine("{0} ({1})", o.SalesOrderNumber, o.OrderDate);

                    foreach (var d in o.SalesOrderDetails)

                        Console.WriteLine("{0} (1)", d.ProductID, d.OrderQty);

                }

            }

            using(AdventureWorksLTDataContext aw=new AdventureWorksLTDataContext())

            {

                // Pro optimalizaci je proto mozne pozadadovat tzv. Immediate Loading (musi byt cerstvy DataContext)

                DataLoadOptions options = new DataLoadOptions();

                options.LoadWith<SalesOrderHeader>(o => o.SalesOrderDetails);

                aw.LoadOptions = options;

                var objednavky3 = from o in aw.SalesOrderHeaders select o;

            }

        }

    }

}