Laden...

[Artikel] LINQ to SQL

Erstellt von Noodles vor 16 Jahren Letzter Beitrag vor 16 Jahren 31.806 Views
N
Noodles Themenstarter:in
4.644 Beiträge seit 2004
vor 16 Jahren
[Artikel] LINQ to SQL

LINQ to SQL

Voraussetzung: Spracherweiterungen in C# 3.0

Dies wird ein kleiner Überblick zu LINQ to SQL. Eins vorweg LINQ hat nicht zwingend etwas mit Datenbanken zu tun, LINQ to SQL schon.
LINQ to SQL ist ein O/R Mapper, welcher mit dem .NET Framework 3.5 mitkommt. Dieser erlaubt es, relationale Daten als .NET Klassen zu modellieren. Die Daten können sowohl gelesen, als auch manipuliert werden. LINQ to SQL unterstützt Transaktionen, Views, Gespeicherte Prozeduren und benutzerdefinierte Funktionen.

Visual Studio 2008 enthält einen Designer, der beim Modellieren der Klassen hilft. Man kann dies auch manuell tun, entweder durch Attribute oder eine XML-Datei, welche mit dem SDK Tool SqlMetal.exe generiert werden können. Der Designer erstellt standardmäßig die Klassen mit Attributen. Ich werde es hier beim automatischen Erstellen durch den Designer belassen.

Um den Designer verwenden zu können, fügt man dem Projekt ein neues Item ( „LINQ to SQL Classes“ ) hinzu. Anschließend kann man aus dem ServerExplorer per Drag & Drop die gewünschten Tabellen oder Views hinzufügen. Desweiteren gibt es einen Bereich für Methoden, auf den Gespeicherte Prozeduren oder Funktionen hinzugefügt werden können. Der Designer erkennt die Relationen und fügt dafür die benötigten Eigenschaften in die Klassen ein. Außerdem erkennt er bei Tabellennamen die Mehrzahl und macht beim Generieren der Klasse daraus eine Einzahl. Aus Customers wird die Klasse Customer. Dies funktioniert nur mit englischen Bezeichnern.

Im Designer kann pro Eigenschaft per „Delay Loaded“ eingestellt werden, dass diese erst bei Zugriff aus der Datenbank geladen werden soll.
Die wichtigste Klasse ist die DataContext Klasse, über diese werden die Objekte geladen und gespeichert. Der DataContext übersetzt die Anfragen der der Objekte in SQL Queries und das Ergebnis der Queries zurück in Objekte. Über die Log Eigenschaft des DataContext kann man genau sehen, wann welche Query abgesetzt wird.

Eine Lambda Expression wird entweder als Delegate oder als ExpressionTree übergeben. Der Compiler entscheidet dies anhand der Interfaces IEnumerable und IQueryable. Bei LINQ to SQL ist der Typ IQueryable, daraufhin wird die Lambda Expression als ExpressionTree übergeben. Diese kann der Provider dann auswerten und die entsprechende SQL Query daraus generieren.

Beispiel für eine Abfrage aller Kunden aus London:

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;
var customers = from c in ctx.Customers 
                where c.City == "London"
                select c;

foreach( var customer in customers )
    Console.WriteLine( customer.ContactName );

Alle Kunden aus London mit den zugehörigen Bestellungen:

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;
                
var customers = from c in ctx.Customers 
                where c.City == "London"
                select c;

foreach ( var customer in customers )
{
    Console.WriteLine( customer.ContactName );
    foreach( var order in customer.Orders )
        Console.WriteLine("  {0}", order.OrderID);
 }

Hier fällt auf, dass bei jedem Zugriff auf Orders eine Query abgesetzt wird, um diese aus der Datenbank zu laden. Dieses Verhalten kann geändert werden. Zum einen kann man einstellen, dass gar keine Relationen mit geladen werden. Dazu setzt man die Eigenschaft DeferredLoadingEnabled des DataContext auf false. Zum anderen kann eingestellt werden, dass die Relationen gleich mit geladen werden, so dass nur eine Query zur Datenbank abgesetzt werden muss.

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;

DataLoadOptions options = new DataLoadOptions();
options.LoadWith<Customer>( c => c.Orders );
ctx.LoadOptions = options;
            
var customers = from c in ctx.Customers 
                where c.City == "London"
                select c;

foreach ( var customer in customers )
{
    Console.WriteLine( customer.ContactName );
    foreach ( var order in customer.Orders )
        Console.WriteLine( "  {0}", order.OrderID );
}

Wenn man nun versucht auf diese Weise auch noch die OrderDetails zu laden, wird man merken, dass dabei wieder eine Query abgesetzt wird. Das liegt daran, dass LINQ to SQL die LoadOption nur für zwei Ebenen zulässt. Um die Komplexität der Ergebnisse zu minimieren.

Desweiteren kann man die LoadOptions Klasse nutzen um die Bestellungen bereits beim Laden zu filtern.

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;

DataLoadOptions options = new DataLoadOptions();
options.AssociateWith<Customer>( c => c.Orders.Where( o => o.OrderDate > new DateTime( 1998, 1, 1 ) ) );
            
ctx.LoadOptions = options;
            
var customers = from c in ctx.Customers 
                where c.City == "London"
                select c;

foreach ( var customer in customers )
{
       Console.WriteLine( customer.ContactName );
       foreach ( var order in customer.Orders )
           Console.WriteLine( "  {0} {1}", order.OrderID, order.OrderDate );
} 

Beispiel für das Aufrufen einer gespeicherten Prozedur:

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;
var result = ctx.Ten_Most_Expensive_Products();
foreach ( var item in result )
    Console.WriteLine( "{0} {1}", item.TenMostExpensiveProducts, item.UnitPrice );

Beispiel für das Anlegen eines neuen Kunden:

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;

Customer customer = new Customer 
{
    CustomerID = "SHARP",
    ContactName = "MyCSharp User",
    CompanyName = "MyCSharp GmbH",
    Country = "Deutschland",
    City = "Berlin"
};
ctx.Customers.Add( customer );
ctx.SubmitChanges();

Löschen eines Kunden:

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;

Customer customer = ctx.Customers.Single( c => c.CustomerID == "SHARP" );
ctx.Customers.Remove( customer );
ctx.SubmitChanges();

Paging von Kunden:

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;

var customers = (from c in ctx.Customers
                 select c).Skip( 10 ).Take( 11 );

foreach (var customer in customers)
    Console.WriteLine(customer.ContactName);

Kunden nach Ort gruppieren und nach Anzahl sortieren:

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;

var customers = from c in ctx.Customers
                group c by c.City into g
                orderby g.Count() descending
                select new
                {
                    City = g.Key,
                    Count = g.Count()
                };

foreach ( var item in customers )
    Console.WriteLine( "{0} {1}", item.City, item.Count );

Alle Lieferanten mit die Kunden, die im gleichen Ort wohnen:

NorthwindDataContext ctx = new NorthwindDataContext();
ctx.Log = Console.Out;

var suppliers = from s in ctx.Suppliers
                join c in ctx.Customers on s.City equals c.City into Customers
                select new
                {
                    Supplier = s.ContactName,
                    Customers,
                    City = s.City
                };

foreach (var item in suppliers)
{
    Console.WriteLine("{0} {1}", item.Supplier, item.City);
    foreach (var customer in item.Customers)
        Console.WriteLine("\t{0}", customer.ContactName );
}

Mit LINQ to XML kommt auch ein performanteres und einfachereres XML API mit. Hier ein Beispiel wie die Kunden aus London in XML geschrieben werden.

NorthwindDataContext ctx = new NorthwindDataContext();
var customers = from c in ctx.Customers
                where c.City == "London"
                select c;

XDocument doc = new XDocument(
        new XElement( "Customers",
             from c in ctx.Customers
             where c.City == "London"
             select new XElement( "Customer",
                    new XAttribute( "id", c.CustomerID ),
                    new XElement( "ContactName", c.ContactName ),
                    new XElement( "Company", c.CompanyName ),
                    new XElement( "City", c.City ) ) ) );
doc.Save( Console.Out );

In Aktionen der generierten Klassen kann man durch partielle Methoden eingreifen. Partielle Methoden sind private Methoden in partiellen Klassen.
Partial Methods

partial class Foo
{
    partial void Bar();
      
    public void Save()
    {
        Bar();
    }
}

Ist eine partielle Methode nur definiert, wird diese nicht vom Compiler berücksichtigt. Es werden also keine Metadaten generiert. Somit wird diese Methode nicht aufgerufen. Dadurch spart man sich den Overhead bei Events, in dem man immer prüfen muss, ob jemand das Event aboniert hat. Wenn man die Klasse Foo kompiliert und anschließend im Reflector betrachtet, findet man die Methode Bar nicht mehr. Erstellt man eine weitere partielle Klasse Foo und implementiert die Methode Bar, wird diese kompiliert und man sieht sie im Reflector. Nun hat man in den generierten Klassen einige partielle Methoden mit denen man in das Verhalten eingreifen kann.

partial class Customer
{
    partial void OnContactNameChanging( string value )
    {
        if (string.IsNullOrEmpty( value ))
        {
            // ...
        }
    }
}