Thomas Kramer

IT-COW | All posts tagged 'Codegeneratoren'

"LINQ und Konsorten" Teil 2

By Administrator at August 25, 2010 16:26
Filed Under: C#, Webcast-Reviews

Dieser Beitrag behandelt den zweiten Teil der Webcast-Reihe LINQ und Konsorten von Dariusz Parys aus dem MSDN-Netzwerk von Microsoft. Mein Review zum ersten Teil ist hier verlinkt; weiterhin verweisen möchte ich auf mein eigenes XML-Tutorial auf dieser Seite.

 

Als Anschauungsmaterial testet Herr Parys mit der Beispiel-Datenbank "Northwind" von Microsoft. Bei mir musste ich sie erst nachinstallieren -> Download-Link. Das Verzeichnis c:\SQL Server 2000 Sample Databases wurde bei mir automatisch generiert und ich musste erst für meinen Windows-Benutzer die Zugriffsrechte auf das Verzeichnis hinzufügen (das lag daran das die Datenbank in einem Installer geliefert wird und dieser automatisch einen UAC-Prompt bringt, deswegen wurde das Verzeichnis mit der Admin-Rolle generiert - ein Fehler im Installer).

 

Als erstes muss die Datenbank dem Server-Explorer im Visual Studio bekannt gemacht werden, dazu einfach Rechtsklick auf Datenverbindungen, auf "Verbindung hinzufügen" klicken und entweder direkt Verbindung zu der Datenbankdatei herstellen oder zum SQL Server und darüber die Datenbank auswählen.

 

Danach fügt man dem Projekt ein neues Element hinzu, die LINQ to SQL-Klassen. Anschließend wechselt man in VS auf die .dbml-Datei (Database-Markup-Language) dieser Klassen, das ist eine interne XML-Datei welche für den Designer verwendet wird. Dann kann man einfach per Drag and Drop die Datenbanktabellen hinzufügen, und er erkennt auch automatisch anhand der Foreign-Keys die Relationen zwischen den Tabellen, welche über Pfeile dargestellt werden.

 

Diese XML-Datei kann man sich in einem Texteditor betrachten, der Aufbau ist eigentlich recht einfach. Hier das Beispiel anhand der Northwind-Datenbank:

 


<?xml version="1.0" encoding="utf-8"?><Database Name="NORTHWND" Class="DataClasses1DataContext" xmlns="http://schemas.microsoft.com/linqtosql/dbml/2007">
  <Connection Mode="AppSettings" ConnectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\NORTHWND.MDF;Integrated Security=True;Connect Timeout=30;User Instance=True" SettingsObjectName="WindowsFormsApplication1.Properties.Settings" SettingsPropertyName="NORTHWNDConnectionString" Provider="System.Data.SqlClient" />
  <Table Name="dbo.Customers" Member="Customer">
    <Type Name="Customer">
      <Column Name="CustomerID" Type="System.String" DbType="NChar(5) NOT NULL" IsPrimaryKey="true" CanBeNull="false" />
      <Column Name="CompanyName" Member="Company" Type="System.String" DbType="NVarChar(40) NOT NULL" CanBeNull="false" />
      <Column Name="ContactName" Type="System.String" DbType="NVarChar(30)" CanBeNull="true" />
      <Column Name="ContactTitle" Type="System.String" DbType="NVarChar(30)" CanBeNull="true" />
      <Column Name="Address" Type="System.String" DbType="NVarChar(60)" CanBeNull="true" />
      <Column Name="City" Type="System.String" DbType="NVarChar(15)" CanBeNull="true" />
      <Column Name="Region" Type="System.String" DbType="NVarChar(15)" CanBeNull="true" />
      <Column Name="PostalCode" Type="System.String" DbType="NVarChar(10)" CanBeNull="true" />
      <Column Name="Country" Type="System.String" DbType="NVarChar(15)" CanBeNull="true" />
      <Column Name="Phone" Type="System.String" DbType="NVarChar(24)" CanBeNull="true" />
      <Column Name="Fax" Type="System.String" DbType="NVarChar(24)" CanBeNull="true" />
      <Association Name="Customers_Orders" Member="Orders" ThisKey="CustomerID" OtherKey="CustomerID" Type="Orders" />
    <

/Type>
  </Table>
  <Table Name="dbo.Orders" Member="Orders">
    <Type Name="Orders">
      <Column Name="OrderID" [..]

 

Das Attribut xmlns (=XML-Namespace) weist auf das Schema für die verwendeten Datentypen hin, dieser Bezeichner ist keine Adresse und muss nicht aufgelöst werden können - wenn man die XML-Datei gegen ein Schema validieren wollte müsste man erst über import namespace das Schema tatsächlich inkludieren. Aber für das Beispiel ist das irrelevant, das hatte mich nur früher mal irritiert.

 

Wie man sieht werden die ganzen Tabellen untereinander mit dem Element Table Name aufgelistet und innerhalb dessen die Spalten definiert sowie jeweils deren Pendant in der Datenbanktabelle. Type wie System.String entspricht dem programmseitig verwendeten Datentyp und DBType dem der Datenbank, über das Element Association Name werden Verknüpfungen zu weiteren Tabellen hergestellt wobei hier Type auf den internen programmseitigen Type Name der Tabellen in der XML-Datei verweist. Die Attribute ThisKey und OtherKey geben die verbindenden Schlüsselfelder an...

 

Jedenfalls, arbeiten wird man mit den DataContext-Klassen welche ebenfalls automatisch generiert werden. Dessen Generat erinnert mich an meine Arbeit mit dem Codegenerator XSD, auch dieser fügt eine Signatur oben in den Quelltext ein. Der von LINQ sieht so aus

 

//------------------------------------------------------------------------------
// <auto-generated>
//     Dieser Code wurde von einem Tool generiert.
//     Laufzeitversion:4.0.30319.1
//
//     Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn
//     der Code erneut generiert wird.
// </auto-generated>
//------------------------------------------------------------------------------

 

In den Attributen über den Klassen und Methoden wird die Verknüpfung mit der Datenbank hergestellt, die Attribute sehen folgendermaßen aus:

 

[global::System.Data.Linq.Mapping.DatabaseAttribute(Name="NORTHWND")]

 

für die Datenbank,

 

[global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.Customers")]

 

für Datenbank-Tabellen und

 

[global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_CustomerID", DbType="NChar(5) NOT NULL", CanBeNull=false, IsPrimaryKey=true)]

 

für Datenbank-Spalten. Wenn man sich das genauer ansieht bemerkt man das es nur einen Standard-Konstruktor gibt, die Werte einfach über Methoden zugewiesen/ausgelesen werden und die DataContext-Klasse alle Tabellen|Spalten-Klassen inkludiert - also keine Unterklassen verwendet werden.

 

Die Abfragen dagegen geschehen jedoch in der für LINQ typischen Syntax. Als Beispiel hatte er ein neues Projekt hinzufügt (Konsolenanwendung) mit einer Referenz auf die LINQ-Klassen sowie den verwendeten DataProvider (System.Data.Linq). Die Abfrage sollte alle Bestellungen aller Kunden ausgeben:

 

using ( NorthwindDataContext ctx = new NorthwindDataContext())
{ ctx.Log=Console.Out; //gibt sql-befehle aus

  var query = from c in ctx.Customers
                   where c.Country == "Germany"
                   select c;

  foreach (var c in query)
  {
     Console.WriteLine(C.Company);

     foreach (var o in c.Orders)
     {
        Console.WriteLine ("Bestellung: {0}", o.OrderID);
     }
  }
  Console.ReadKey();
}

 

Anonyme Typen sind typisch für LINQ-Abfragen... mittels des Befehls ctx.Log=Console.Out werden die generierten SQL-Befehle mit ausgegeben, das war mir noch nicht bekannt. Anhand dessen konnte man auch erkennen das parameterisiertes TSQL erzeugt wird. Console.Readkey sorgt übrigens dafür, das das Konsolenfenster nicht sofort wieder geschlossen wird.

 

Update 27.08.2010: fortgefahren war Herr Parys damit über der var query = ... - Zuweisung die folgenden Zeilen zu ergänzen:

 

DataLoadOptions options = new DataLoadOptions();
options.LoadWith<Customers>(c => c.Orders);
ctx.Log = Console.Out;
ctx.LoadOptions = options;

 

Der LoadWith-Parameter sorgt dafür das nicht nur die Customers-Tabelle, sondern auch die davon abhängige Tabelle Orders direkt mitgeladen wird. Das hatte er dadurch belegt das nicht mehr ein SQL-Befehl pro Schleifen-Iteration ausgegeben wurde, sondern nur einmal zu Beginn.

 

An der Stelle wollte ich wissen ob man noch eine weitere Tabelle direkt laden kann, habe dazu aber hier folgende Aussage gefunden: First, the LoadWith setting only works for one level of hierarchy. Demnach wohl nicht möglich... hier auch ein Link zur LoadWith-Methode in der MSDN: Link (die Northwind-Datenbank scheint beliebt bei Microsoft zu sein).

 

Danach hatte er dann das Beispiel so verändert (Achtung, falsch!):

 

using ( NorthwindDataContext ctx = new NorthwindDataContext())
{
    DataLoadOptions options = new DataLoadOptions();
    options.LoadWith<Customer>(c => c.Orders);
    cts.Log = Console.Out;
    ctx.LoadOptions = options;

    var query = from c in ctx.Customers
                     from o in c.Orders
                     from od in o.OrderDetails
                     where c.Country == "Germany"
                     select new
                            {
                               Firma = c.Company,
                               Gesamtbestellwert = o.OrderDetails.Sum(
                                     pos => pos.Quantity * pos.UnitPrice )
                            }

 

Das ist deswegen falsch weil die Summen-Funktion eine Aggregatsfunktion ist und auf ein GroupBy angewandt werden muß - will man einfach nur die Summe dieser beiden Spalten haben gibt man stattdessen einfach

                           

[..]
                            select new
                            {
                                Firma = c.Company,
                                Bestellwert = od.UnitPrice * od.Quantity
                            };

 

ein. Ich hatte mich schon gewundert, sind die Werte doch völlig falsch gewesen, was ihm natürlich auch aufgefallen ist. Will man den Gesamtbestellwert pro Kunde ausgeben - wie er es wohl ursprünglich im Sinn hatte - muss man es so machen:

               

var query = from c in ctx.Customer
            from o in c.Orders                 
            from od in o.Order_Details                 
            where c.Country == "Germany"
            orderby c.Company ascending                 
            group od by new {c.Company} into x
            select new {
                        Menge = x.Key,
                        Preis = x.Sum(test => test.Quantity * test.UnitPrice)
                        };

 

Ehrlich gesagt finde ich die GroupBy-Syntax von LINQ etwas gewöhnungsbedürftig, ich musste mich erst darauf einstellen. Will man nach mehreren Spalten gruppieren gibt man statt group od by c.company into x bspw. group od by new {c.Company, c.ContactName} into x ein. Das Ergebnis fließt in die neue Ergebnismenge x ein.

 

Sobald ein group eingefügt wurde kann im select new {}-Statement (erzeugt einen anonymen Typ) nicht mehr auf Datenspalten der Tabellen-Aliase c, o oder od zugegriffen werden, zumindest nicht direkt - stattdessen müsste man in dem Fall test.Products.CategoryID bspw. eingeben, also ausgehend von der Gruppierungsergebnismenge test (bzw. x) mit nachfolgendem Punkt auf die weiteren Tabellen zugreifen.

 

Lässt man das group weg, kann man auf diese Tabellen direkt über die hier vergebenen Buchstaben c, o oder od zugreifen. Mit x.Key lassen sich die Gruppierungsfelder abfragen.

 

Was bei dieser Abfrage auch auffällt ist, das für LINQ kein Left Outer Join definiert werden muss, wie sonst bei SQL üblich. Das erkennt LINQ über die Foreign Keys offenbar automatisch. An der Stelle fällt mir übrigens eine Besonderheit von Oracle ein, dort konnte man in SQL-Abfragen mit Verknüpfungen dieser Art T1.Spalte(+)=T2.Spalte oder T1.Spalte=T2.Spalte(+) auch ein Outer Join erreichen, aber das war nicht standardisiert und funktionierte nur mit Oracle.

 

Bezüglich SQL Server habe ich vorhin das OpenSource-Tool Anjlab SQL Profiler entdeckt - bei der Express-Version von SQL Server ist das Profiling-Pendant von Microsoft leider nicht dabei. Mit dem Tool kann man theorethisch die SQL-Befehle mitloggen, in meinen Tests vorhin wurden allerdings nur kryptische Daten ausgegeben... möglicherweise werden auch nur DML-Befehle dabei geloggt, also Befehle die Daten verändern.

 

Ansonsten bin ich gerade noch auf die Software LINQPad gestoßen: Link. Update 28.08.2010: ich hoffe das nicht LINQPad den Bluescreen unter Vista bei mir gerade erzeugt hat, aber ich denke mal nicht. Die Software gefällt mir nämlich ansonsten ganz gut, sie bringt auch die Beispiel-LINQ-Abfragen des Buches C# 3.0 in a Nutshell mitsamt der dazugehörigen SQL Server-Datenbank Nutshell.mdf mit - ideal zum Testen von LINQ-Abfragen, ohne immer den Programm-Kontext beachten zu müssen.

 

Die Datenbankdatei direkt im Server-Explorer einzubinden erscheint mir nicht so sinnvoll; so kann es vorkommen das das Programm keine Änderungen an der Datenbank durchführen kann weil die Datei noch im Server-Explorer von Visual Studio geöffnet ist. Daher besser die Datenbankdatei im SQL Server Management Studio registrieren und im Server-Explorer nur eine Verbindung zum Datenbankmodul herstellen.

 

Anschließend hatte er gezeigt wie man Daten hinzufügen kann, das ist prinzipiell auch ganz einfach:

               

Customer cm = new Customer()
                {
                    Company = "Demo Shop",
                    CustomerID = "PARYS",
                    ContactName = "Dariusz Parys",
                    Country = "Germany"
                };

                ctx.Customer.InsertOnSubmit(cm);
                ctx.SubmitChanges();

 

Hier wird ein Kunde Dariusz Parys hinzufügt; erst wird der Customer-Tabelle Bescheid gesagt das sie bei einem Submit das Objekt cm einfügen soll (Methode InsertOnSubmit()) und dann wird der Submit (Methode SubmitChanges()) ausgeführt.

 

Um einen Datensatz zu ändern geht man folgendermaßen vor, zuerst selektiert man das Objekt:

               

Customer cus = (from c in ctx.Customer
                where c.CustomerID == "PARYS"
                select c).First();

 

Die from-Abfrage von LINQ liefert ansich ein IQuaryable-Interface zurück, daher muss dieser Typ erst mit .First() in das Customer-Tabellenobjekt umgewandelt werden. Anschließend führt man die Änderungen an dem Datensatz aus und speichert die Veränderungen:

 

cus.ContactName = "Dariusz Parys";
ctx.SubmitChanges();

 

Demnach ganz einfach. Dazu hatte er übrigens gezeigt wie man Änderungen an einem bestimmten Datensatz verhindern kann. Die DataContext-Klasse von LINQ - die automatisch generiert wurde - deklariert für die Tabellen partial Methods, also Erweiterungsmethoden. Partielle Klassen|Methoden stellen eine Möglichkeit dar, den Quellcode einer Klasse oder Methode auf mehrere Dateien aufzusplitten.

 

Die Klassendefinition der Tabelle Customer z. B. sieht so aus:

 

public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
{
     [..]
     #region Definitionen der Erweiterungsmethoden
     partial void OnContactChanging(string value);
     partial void OnContactNameChanging(string value);
     #endregion
     [..]

     [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ContactName", DbType="NVarChar(30)")]
     public string ContactName
     {
         get
         {
             return this._ContactName;
         }
         set
         {
             if ((this._ContactName != value))
             {
                 this.OnContactNameChanging(value);
                 this.SendPropertyChanging();
                 this._ContactName = value;
                 this.SendPropertyChanged("ContactName");
                 this.OnContactNameChanged();
             }
         }
     }

 

Wie man sieht wird das Ereignis OnContactNameChanging in der Property ContactName aufgerufen, wobei die Methode für das Ereignis noch nicht implementiert ist - nur die Methodensignatur ist bereits vorhanden. Daher kann man in einer weiteren Quellcodedatei die vorhandene Klasse Customer erweitern (derselbe Namespace natürlich nötig):

 

public partial class Customer
{
     partial void OnContactNameChanging(string value)
     {
         if (string.Compare(value, "Dariusz Parys", true) == 0)
         {
             throw new ApplicationException("Dariusz Parys ist keine Option!!!");
         }
     }
}

 

Nun ist es nicht mehr möglich die Spalte ContactName eines Datensatzes in der Customer-Tabelle auf den Wert "Dariusz Parys" zu ändern - bei einem Versuch wird eine Exception geworfen.

 

Mehr zu partiellen Klassen und Methoden in der MSDN: Link. Damit bin ich nun auch mit diesem Webcast durch - insgesamt wieder lehrreich, aber diesmal doch mit einigen Fehlern versetzt. Man sieht deutlich das der Webcast live aufgenommen wurde, es war etwas mühsam zu folgen.

 

LINQ insgesamt ist natürlich - wie SQL auch - den domänenspezifischen Sprachen zuzuordnen.

 

Ich kann die Vermutung (oben) von mir nun bestätigen, vom Anjlab SQL Profiler werden tatsächlich nur DML-SQL-Befehle geloggt.

 

Fortsetzung: Link.

 

Weitere Links


  • Blog von Dariusz Parys: Link
  • LINQ-Samples in der MSDN: Link
  • Mehrfach-Gruppierung in C# (MSDN): Link
  • Aggregatoperatoren von LINQ to Entities: Link
  • LINQ to Objects im Buch Visual C# 2008: Link
  • Spracherweiterungen in C# für LINQ: Link

 

Tag-Wolke

Monats-Liste