Kategorie: XML


#.NET: #LINQ to SQL und XML-basierte Mappings

30. Juli 2009 - 14:34 Uhr

Was von Anfang an bei LINQ to SQL gestört hat, ist die scheinbare Notwendigkeit, auf Mappings per Attribut zurückzugreifen. Stellen Sie sich ein klassisches Szenario vor: Es sollen mit den selben LINQ-Statements je nach Situation unterschiedliche Tabellen abgefragt werden (ich habe das zum Beispiel bei einem Kunden, der mit Navision arbeitet). Das geht mit Attributen schlicht nicht. Oder Sie möchten einfach keine Attribute nutzen, die ja eine fixe Verdrahtung mit LINQ to SQL bedeuten würden, was oftmals nicht wünschenswert ist.

Zum Glück gibt es die XmlMappingSource-Klasse. Die erlaubt es, die Mappings in XML-Dateien abzulegen und somit unabhängig von der Klasse zu halten, was in Bezug auf Wartbarkeit und Entkoppelung von Schichten ohnehin deutlich sinnvoller ist.

Die XmlMappingSource-Klasse verfügt über vier statische Methoden:

  • FromReader(): Nimmt eine XmlReader-Instanz entgegen
  • FromStream(): Erwartet einen Stream
  • FromUrl(): Erwartet die Angabe eines URI
  • FromXml(): Akzeptiert eine als XML interpretierbare Zeichenkette

Etwas umständlich ist, das nicht mehrere XML-Dateien zugewiesen werden können, aber da kann man sich ggf. mit kleineren Workarounds (Laden mehrerer Dateien, Überführen in ein Gesamt-XML) behelfen.

Um zu verdeutlichen, wie mit dem XML-basierten Mapping gearbeitet kann, soll im folgendenen Beispiel eine einfache Personen-Klasse verwendet werden:

using System;

namespace XML_LINQ_Sample.Model
{
   /// <summary>
   /// Repräsentiert eine Person
   /// </summary>
   public class Person
   {
      /// <summary>
      /// Initialisierung der Klasse
      /// </summary>
      public Person()
      {
         LastChanged = DateTime.Now;
      }

      /// <summary>
      /// Id der Person, Identity in der Tabelle
      /// </summary>
      public int Id { get; set; }

      /// <summary>
      /// Vorname
      /// </summary>
      public string FirstName { get; set; }

      /// <summary>
      /// Nachname
      /// </summary>
      public string LastName { get; set; }

      /// <summary>
      /// E-Mail-Adresse
      /// </summary>
      public string Email { get; set; }

      /// <summary>
      /// Datum der letzten Änderung
      /// </summary>
      public DateTime LastChanged { get; set; }
   }
}

Das Mapping für diese Klasse kann nun wie folgt aussehen:

<Database Name="Samples"
          xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007">

   <!-- Person -->
   <Table Name="Persons" Member="XML_LINQ_Sample.Model.Person">

      <!-- Typ-Definition -->
      <Type Name="XML_LINQ_Sample.Model.Person">

         <!-- ID -->
         <Column Name="Id" Member="Id" IsPrimaryKey="true" IsDbGenerated="true" />

         <!-- Vorname -->
         <Column Name="FirstName" Member="FirstName" />

         <!-- Nachname -->
         <Column Name="LastName" Member="LastName" />

         <!-- Email -->
         <Column Name="EMail" Member="Email" />

         <!-- Letzte Änderung -->
         <Column Name="LastChanged" Member="LastChanged" />
      </Type>

   </Table>

</Database>

Wichtig für dieses Mapping ist die Angabe des korrekten Namensraumes http://schemas.microsoft.com/linqtosql/mapping/2007, da der Inhalt des XML-Dokuments sonst schlicht nicht ausgewertet werden könnte. Die einzelnen Typen werden innerhalb eines Table-Elements in einem Type-Element definiert. Eine Spalte definiert das Column-Element, wobei die beiden Attribute Name und Member Pflichtangaben sind. Zusätzlich werden beim ID-Element die beiden Attribute IsPrimaryKey und IsDbGenerated verwendet. Deren Aufgabe ist es, anzugeben, ob die Spalte den Primärschlüssel der Tabelle repräsentiert (IsPrimaryKey) und ob der jeweilige Wert vom Datenbanksystem automatisch generiert werden soll (IsDbGenerated).

Tipp: Das XML-Dokument sollten Sie als Ressource ihrem Projekt hinzufügen, da dies die Handhabung deutlich erleichtert. In diesem Fall wird es unter dem Namen PersonMapping als Ressource gespeichert.

Innerhalb unseres Programms können Sie nun eine Methode definieren, die diese Ressource lädt und einem DataContext zuweist:

/// <summary>
/// Erzeugt einen DataContext
/// </summary>
private static DataContext GetDataContext()
{
   // ConnectionString-Einstellungen abrufen
   ConnectionStringSettings connStr = ConfigurationManager.ConnectionStrings["Sample"];

   // DbProviderFactory abrufen
   DbProviderFactory factory = DbProviderFactories.GetFactory(connStr.ProviderName);

   // Connection erzeugen lassen
   DbConnection connection = factory.CreateConnection();
   connection.ConnectionString = connStr.ConnectionString;

   // MappingSource definieren
   XmlMappingSource source = XmlMappingSource.FromXml(Resources.PersonMapping);

   // DataContext zurückgeben
   return new DataContext(connection, source);
}

Der "magische Teil" ist die Verwendung einer XmlMappingSource. Ansonsten verhält sich der DataContext, wie Sie es gewohnt sind:

/// <summary>
/// Führt irgendwelche Operationen aus
/// </summary>
public void PerformActions()
{

   // DataContext definieren
   using(DataContext dc = GetDataContext())
   {
      // Schema erzeugen lassen, wenn es
      // noch nicht existiert
      if(!dc.DatabaseExists())
      {
         dc.CreateDatabase();
      }

      // Tabelle referenzieren
      Table<Person> personTable = dc.GetTable<Person>();

      // Datensatz finden
      Person person = (
         from Person p in personTable
         where p.LastName.Equals("Samaschke") &&
               p.FirstName.Equals("Karsten")
         select p).FirstOrDefault();

      // Datensatz gefunden?
      if(null != person)
      {
         // Werte ändern
         person.Email = "name@adresse.de";
         person.LastChanged = DateTime.Now;
      }
      else
      {
         // Datensatz anlegen
         person = new Person();
         person.FirstName = "Karsten";
         person.LastName = "Samaschke";
         person.Email = "name@adresse.de";

         // Datensatz einfügen
         personTable.InsertOnSubmit(person);
      }

      // Änderungen speichern
      dc.SubmitChanges();
   }
}

Mit dem Umweg über die XmlMappingSource schlagen Sie letztlich mehrere Fliegen mit einer Klappe:

  • Verwendbarkeit verschiedener Datenbanken oder Datenbanktabellen je nach Szenario
  • Verhinderung des Bindens von Klassen an ein bestimmtes O/R-Mapping-Framework
  • Bessere Wartbarkeit und Pflegbarkeit Ihres DataLayers

Einziger echter Nachteil ist, das Microsoft diesen Ansatz zwar grundsätzlich dokumentiert, Sie aber bis auf die Klassenbeschreibung der XmlMappingSource und die Definition des XML-Schemas für die Mapping-Dateien keinerlei weitere Unterstützung von Seiten des Anbieters erhalten. Da die Elemente und Attribute jedoch den Standard-LINQ-Code-Attributen entsprechen, können Sie die dort getroffenen Aussagen hier 1:1 übertragen.

Kommentieren » | .NET, Tipp, XML

#XML: Arbeit mit Namensräumen beim XmlDocument

28. Juli 2009 - 18:12 Uhr

Viele XML-Dokumente, die Sie im Web herunterladen oder benutzen können, verfügen über einen Namensraum. Den müssen Sie angeben, wenn Sie bei .NET die XmlDocument-Klasse und deren Methoden SelectSingleNode() bzw. SelectNodes() verwenden möchten, um an die für Sie relevanten Informationen mit Hilfe von XPath-Ausdrücken zu gelangen. Die Frage ist nur, wo und wie man das machen kann, denn das Dokument verfügt zwei über eine Eigenschaft NameTable, die kann jedoch scheinbar nicht sinnvoll verwendet werden.

Stattdessen greifen Sie auf die Klasse XmlNamespaceManager zurück. Deren Konstruktor nimmt als Parameter die NameTable des Dokuments entgegen. Die Methode AddNamespace() erlaubt es Ihnen im Anschluß, die für Sie relevanten Namensräume zu definieren. Dabei ist zu beachten, das so ein Namensraum stets aus zwei Komponenten besteht: Einem Präfix (der eine Abkürzung für den Namensraum darstellt) und dem URI, dem eigentlichen Namensraum. Das Präfix ist dabei – im Vergleich zum originalen Dokument – frei wählbar, der URI muss dem Namensraum im zu verarbeitenden Dokument entsprechen. Sie erkennen die Namensräume stets anhand der xmlns- und xmlns:xx-Attribute, wobei ersteres einen Standardnamensraum darstellt, der ebenfalls per XmlNamespaceManager-Instanz bekannt gemacht werden muss.

Folgendes Beispiel verwendet die XmlNamespaceManager-Klasse um eine Abfrage auf bestimmte Knoten in einem fiktiven Dokument umzusetzen:

// Dokument erzeugen
XmlDocument doc = new XmlDocument();

// Inhalt einladen
doc.Load("http://...");

// Namespace-Manager für die Verwaltung der Namensräume
XmlNamespaceManager manager = new XmlNamespaceManager(doc.NameTable);

// Namensräume anfügen, wichtig sind die URIs
manager.AddNamespace("xs", "urn:sample"); //xmlns:xs="urn:sample"
manager.AddNamespace("xt", "urn:sample2"); //xmlns:xt="urn:sample2"

// Standard-Namensraum anfügen
manager.AddNamespace(string.Empty, "urn:default"); //xmlns="urn:default"

// Abfrage ausführen
XmlNodeList selected = doc.SelectNodes("//dummy[@xt:text='foo']/xs:name");

Die einzige wirkliche Schwierigkeit bei der Arbeit mit Namensräumen im .NET-Framework besteht letztlich nur darin, diese zu identifizieren. Nachdem Sie diese Leistung erbracht haben, sollten die weiteren Schritte problemlos umsetzbar sein.

Kommentieren » | .NET, Tipp, XML