none
Sql Compact Spaltenposition verändern mit SetOrdinal - aber wie? RRS feed

  • Frage

  • Hallo,

    vorneweg - ich bin Newbe, also bitte möglichst ausführliche Antworten und nichts voraussetzen...

    Ich habe folgendes Problem:

    Ich bekomme von einer anderen Anwendung ca. 50 Textfiles, die dort als Input/Output zur Parameterübergabe benutzt werden. Ich will nun ein Programm (in C#) erstellen, die diese Files zeilenweise einliest, in eine Datenbank speichert und nach dem Bearbeiten wieder in die Textfiles zurückschreibt. Das habe ich soweit auch alles schon realisiert, komme aber bei der Frage des Verschiebens der Spalten beim besten Willen nicht weiter.

    Zur Verdeutlichung:

    Textfile1: A=a1, D=d1, G=g1

    Textfile2: A=a2, B=b2, G=g2

    Datenbank:

    A D G B

    a1 d1 g1 null

    a2 null g2 d2

    Damit bekomme ich dann Probleme mit dem Einlesen in die externe Anwendung, da die Reihenfolge der Parameter nicht mehr stimmt. Leider weiss ich aber weder vor dem Erstellen der Datenbank noch nach dem Einlesen in welcher Reihenfolge ich die Parameter übergeben muß. Mein Ansatz war einen Zähler bei jedem ADD mitlaufen zu lassen, den ich dann mit GetOrdinal vergleiche und falls nötig mit SetOrdinal an die richtige Position verschiebe. Das funktioniert im DataSet / DataTable auch ganz wunderbar, nur interessiert das die Datenbank halt überhaupt nicht. Die betroffene Stelle sieht so aus (ich weiß, das da noch ein Insert, ADD oder ähnliches für den Builder fehlt, hab aber schon alles mögliche ausprobiert und komme halt einfach nicht weiter):

     

    SqlCeCommand command = conn.CreateCommand();
    command.CommandText = "SELECT * FROM Units"
    ;
    SqlCeDataReader reader = command.ExecuteReader();
     int
     ordZaehler = reader.GetOrdinal(alter);
     reader.Close();
    SqlCeDataAdapter adp = new
     SqlCeDataAdapter(command);
     DataSet ds = new
     DataSet();
     adp.Fill(ds);
     int
     zae;
     zae=ds.Tables[0].Columns.IndexOf(alter);
     ds.Tables[0].Columns[zae].SetOrdinal(zaehler);
     //DataTable dat = new DataTable();
    
     //adp.Fill(dat);
    
     //int zae;
    
     //zae = dat.Columns.IndexOf(alter);
    
     //dat.Columns[zae].SetOrdinal(zaehler);
    
     //adp.Update(dat);
    
    SqlCeCommandBuilder cb = new
     SqlCeCommandBuilder(adp);
    adp.Update(ds);
    

     

    Freitag, 2. Juli 2010 09:39

Antworten

  • Hallo Andi,

    zunächst das Einlesen, tausche dafür die Methode EinlesenDatei aus:

     

        private DataTable EinlesenDatei(string dateiname)
        {
          var spalten = new List<string>();
          var daten = new List<string>();
          // ANSI-Encoding?
          using (var reader = new StreamReader(dateiname, Encoding.Default)) 
          {
            string zeile;
            while ((zeile = reader.ReadLine()) != null)
            {
              // evtl. weitere Überprüfungen (Null, Leer usw.) einbauen
              string[] zeilenwerte = zeile.Split(':');
              if (zeilenwerte.Length == 2)
              {
                spalten.Add(zeilenwerte[0]);
                daten.Add(zeilenwerte[1]);
              }
            }
          }
    
          DataTable table = new DataTable(dateiname);
          // Spalten anlegen
          foreach (var spalte in spalten)
          {
            table.Columns.Add(spalte, typeof(string));
          }
          // Daten (eine Zeile)
          table.Rows.Add(daten.ToArray());
          return table;
        }
    

     

    Komplizierter ist es mit dem Sortieren und nicht etwa wegen SetOrdinal ... ;-)

    Denn hier hängt es auch von den Eingabedaten ab. Denn es ist von Bedeutung,
    in welcher Reihenfolge die Dateien mit welchen Spalten verarbeitet werden.
    Ich habe zwar eine einfache Suchheuristik eingebaut, die auch nach der Vorgänger-
    bzw. Nachfolgerspalte sucht und versucht die Spalte einzusortieren.
    Das klappt naturgemäß aber nur, wenn eine der Spalten auch vorkommt.
    Taucht zunächst keine der Spalte auf, weil sie erst in einer späteren Datei
    verwendet wird, so geht das schief (in den Beispielen nicht der Fall).

    Einfacher wäre hier im allgemeinen, die Sortierung gleich zu hinterlegen -
    soviele Spaltennamen sollten wohl kaum vorkommen und ändern sollte es
    sich auch nicht alle paar Minuten.

    Tausche dazu die Methode SpaltenErmitteln aus:

     

     /// <summary>Ermittelt die Spaltendefinition anhand des DataSets</summary>
        private List<ColumnDefinition> SpaltenErmitteln(DataSet dataSet)
        {
          List<ColumnDefinition> columnList = new List<ColumnDefinition>();
          foreach (DataTable table in dataSet.Tables)
          {
            for (int columnIndex = 0; columnIndex < table.Columns.Count; columnIndex++)
            {
              var column = table.Columns[columnIndex];
              var definition = new ColumnDefinition(
                column.ColumnName,
                (SqlDbType)column.ExtendedProperties[MohrenkopfEVA.DataTypeProperty]);
              int index = columnList.IndexOf(definition);
              if (index == -1)
              {
                // Versucht die Position anhand der bereits gefundenen Daten zu ermitteln
                if (columnIndex > 0)
                {
                  // Nur der ColumnName interessiert (einfacher über IEquatable<string>)
                  var previousDefinition = new ColumnDefinition(table.Columns[columnIndex - 1].ColumnName, SqlDbType.NVarChar);
                  int previousColumnIndex = columnList.IndexOf(previousDefinition);
                  if (previousColumnIndex != -1)
                  {
                    columnList.Insert(previousColumnIndex + 1, definition);
                    continue;
                  }
                }
    
                if (columnIndex < table.Columns.Count - 1)
                {
                  var nextDefinition = new ColumnDefinition(table.Columns[columnIndex + 1].ColumnName, SqlDbType.NVarChar);
                  int nextColumnIndex = columnList.IndexOf(nextDefinition);
                  if (nextColumnIndex != -1)
                  {
                    columnList.Insert(nextColumnIndex, definition);
                    continue;
                  }
                }
    
                // Letztes Mittel Anfügen
                columnList.Add(definition);
              }
              else
              {
                // Abweichender Typ => String
                if (definition.DataType != columnList[index].DataType)
                  columnList[index].DataType = SqlDbType.NVarChar;
              }
            }
          }
          return columnList;
        }
    Ich habe die Name Spalte im übrigen drin gelassen. Die würde ich nicht gesondert
    behandeln sondern als zuätzliche Information mitnehmen. Denn gerade wenn Du
    die Zeilen auseinanderhalten wieder trennen willst, ist das nützlich.

     

    Wenn hier immer nur ein Satz enhalten ist (wovon ich nicht ausgegangen bin),
    könnte man sich das Ganze im übrigen vereinfachen und die vielen DataTables
    mit einer Zeile zusammenfassen. Da ist jetzt mehr Overhead als Nutzdaten enthalten.

    Selbst ein vollständiger Verzicht auf die DataTable und eine Lösung mit List<T> wäre denkbar
    Du solltest Dir Auflistungen und Datenstrukturen , denn das gehört zum Grund-Handwerkszeug
    eines Entwicklers, so wie das Getriebe zum Motor ;-)

    Aber beschäftige Dich noch ein wenig damit, ob Du selbst dazu die Lösung findest.
    Gehe dazu das Programm schrittweise (F10/F11) durch und schau Dir die Variablen an.
    Denn so eine Verarbeitung ist nun zunächst nicht besonders und die Konzepte
    (ich hab auf Tricks und Abkürzungen verzichtet) lassen sich immer wieder verwenden -
    so denn einmal verstanden, deswegen der Hinweis auf EVA.

    Gruß Elmar

    Sonntag, 4. Juli 2010 17:11
    Beantworter

Alle Antworten

  • Hallo,

    vergiß das Verschieben! SQL ist vom Prinzip darauf ausgelegt,
    dass Spalten über ihre Namen adressiert werden.

    Der einfachste Weg: Lege für die DataColumn die Spaltennamen
    über die ColumnName Eigenschaft am besten gleich beim Erstellen fest.

    Passt das aus irgendwelchen Gründen nicht zusammen,
    kannst Du beim DataAdapter über das DataTableColumnMapping
    eine andere Zuordnung erstellen.
    Das wird auch vom CommandBuilder berücksichtigt.

    Gruß Elmar

    Freitag, 2. Juli 2010 09:58
    Beantworter
  • Hallo,

    vielen Dank für die schnelle Antwort - allerdings hab ich ja die Spaltennamen beim Erstellen gleich generiert, nur wie kann ich der Datenbank mitteilen, das die neue Spalte String1 halt nicht an Position 33 sondern an Position 23 gehört? Es geht dabei nicht um die logischen Abfragen sondern halt nur ganz einfach darum: Füge diese Spalte an Position x in die Datenbank ein.

    "Mithilfe von DataTableMapping können Sie in einer DataTable andere Spaltennamen als in der Datenbank verwenden"

    hilft mir so leider auch nicht weiter....

     

    Oder geht das überhaupt nicht in Sql Compact - und wenn nicht, mit welcher User-freundlichen und freien Datendank (Access-Runtime?) bekomme ich das hin?

     

    Gruß

     

    Mohrenkopf-Fan

    Freitag, 2. Juli 2010 11:00
  • Hallo,

    um eine Spalte an der von Dir gewünschten Position abzurufen, verwende eine SELECT
    Liste, anstatt des "*" (was heisst, die Reihenfolge interessiert mich nicht),
    also SELECT Spalte1, /* 2- 21 ausgelassen */, Spalte22, Spalte33, Spalte24 /*, ... */

    Hintergrund:
    Im relationalen Modell gibt es prinzipiell keine "Spaltenpositionen" sondern nur Spalten.
    Denn dort hat das physikalische Modell (hier: wo liegt die Spalte) keine Auswirkung auf das
    logische Modell (hier: welche Spalten sind in der Tabelle enthalten).
    Und "*" ist nur für die Bequemlichkeit eingeführt worden, wo man alle Spalten
    in beliebiger Reihenfolge haben möchte.
    Und nein: Für Schreibfaule ist es nicht gemacht worden ;-)

    Willst Du die physikalische Reihenfolge ändern mußt Du die Tabelle neu erstellen.
    nach dem Muster (verkürzt):

    CREATE TABLE NeueTabelle (Spalten in neuer Reihenfolge)

    INSERT INTO NeueTabelle (Neuereihenfolge)
    SELECT altereihenfolge FROM Tabelle;

    DROP TABLE Tabelle

    sp_rename 'NeueTabelle' 'Tabelle'

    (nur ist das meist den Aufwand nicht wert).

    Gruß Elmar

    Freitag, 2. Juli 2010 11:27
    Beantworter
  • Funktioniert aber auch nicht mit ADD - ich kann zwar schön meiner (theoretischen 23.) Spalte Werte hinzufügen, geschrieben wird aber in Spalte 33 - sehr eigensinnig dieses Ding....

    - oder muß ich der Spalte sagen: Du bist jetzt int SpaltenZaehLer =23; und wie sortier ich das dann und wofür gibt es denn überhaupt SetOrdinal?

     

    Freitag, 2. Juli 2010 11:54
  • Hallo,

    ich verstehe nicht so ganz, warum Du unbedingt an der Datenbank rumdrehen willst
    (und Dein Codeschnipsel gibt nun wirklich nichts her).

    Wenn Du an später wieder in eine Textdatei schreiben willst,
    wo die Spalte (mit dem Namen Spalte33) an die 23. Position
    rutschen muß, so merke Dir die Positionen für die Textdatei.
    Dafür kannst Du eine kleine Auflistung verwenden, wie sie
    auch die DataColumnMappings darstellen. Und das separat
    in eine Datei speichern.

    Im übrigen kann man eine DataTable ansonsten hin und her
    verbiegen, was die Spaltenreihenfolge angeht. Denn sie stellt
    eine (Speicher)Datenbank für sich dar und der ist es egal
    wo die Spalte steht (auch da gilt wie eingangs betont:
    der Name zählt - genauer die erzeugte DataColumn-Instanz).

    Gruß Elmar

     

    Freitag, 2. Juli 2010 13:43
    Beantworter
  • Hallo,

     

    naja ich bin halt, etwas naiv vielleicht, davon ausgegangen, dass ich alle Änderungen im DataTable durchführe und wenn alles passt nur noch sage so jetzt speichern.

    Der Programmaufbau des Einleseteils ist eigentlich sehr einfach: Textfile öffnen, Zeile einlesen, nach diversen Stringoperationen weil da halt ziemlich viel Müll drinsteht, falls Spalte nicht vorhanden mit ADD Spalte hinzufügen, mit Insert füllen, sonst nur Insert, nächste Zeile, nächstes File, Ende. Wie gesagt, das ist alles problemlos.

    try
    {
      if (Decimal.TryParse(subinput3, out muell))
      {
        alterSql = "ALTER TABLE Units ADD COLUMN " + alter + " dec(10,2)";
        ++zaehler;
      }
      else
      {
        alterSql = "ALTER TABLE Units ADD COLUMN " + alter + " nvarchar(100)";
         ++zaehler;
      }
      SqlCeCommand alt = new SqlCeCommand(alterSql, conn);
      alt.ExecuteNonQuery();
    }
    catch
    {
      //to do                      
    }
    updat = ("UPDATE Units SET " + alter + "='" + subinput3 + "' WHERE name='" + insert + "'");
    SqlCeCommand up1 = new SqlCeCommand(updat, conn);
    up1.ExecuteNonQuery();
    
    
    

    Freitag, 2. Juli 2010 15:31
  • Hallo,

    ich würde dort ein anderes Vorgehen wählen.
    Zunächst alles in eine DataTable mit Spalten mit typeof(String) einlesen.
    Danach eine Analyse Spaltenweise ob die erwarteten Datentypen zutreffen können.

    Daraus kann man danach ein CREATE TABLE in einem Rutsch erzeugen.
    Und die Daten danach über ein parametrisiertes SqlCommand übertragen.

    Denn ein häufiges Ändern von Datentypen bei bereits gefüllter Tabelle ist
    vergleichsweise aufwändig zu selbst einem mehrfachen Durchlesen einer Textdatei.

    Optional: Ein mit korrekten Datentypen erzeugte DataTable erstellen
    und die Daten dorthin übertragen. Dann könnte man auch einen
    SqlCommandBuilder verwenden.
    Lohnt aber eher weniger, wenn eh Du es geschickt anstellst.

    Gruß Elmar

    Freitag, 2. Juli 2010 16:06
    Beantworter
  • Hallo,

     

    diesen Ansatz hab ich auch schon verfolgt, das Problem als solches bleibt jedoch bestehen, zumindest konnte ich es nicht lösen.

    Wenn ich alle Daten auf einmal einlese, weiß ich nicht wo welche Spalte hingehört.

    Also bin ich den gleichen Weg wie oben gegangen, der ja allem Anschein nach eine Sackgasse ist, und lese zeilenweise ein, CreateTable leer, Add und Insert, bzw. nur Insert um dann mal wieder am SetOrdinal hängen zu bleiben, da das Update vom Builder halt nur Insert, ADD oder Delete versteht, aber kein SetOrdinal.

    Mittlerweile experimentiere ich mit nem kompletten Delete der DB vor jedem ADD sowie manuellem Hinzufügen der @P Parameter, aber das halte ich weder für besonders elegant noch performant...

     

    Gruß

     

    Mohrenkopf-Fan

     

    Der Code ist noch sehr chaotisch - Experimentierphase halt:

          SqlCeDataAdapter adp = new SqlCeDataAdapter(command);
          DataSet ds = new DataSet();
          adp.FillSchema(ds, SchemaType.Source);
          adp.Fill(ds);
          DataTable table = ds.Tables[0];
      SqlCeCommandBuilder cb = new SqlCeCommandBuilder(adp);
      int c = 0;
      //while (table.Columns.Count >= c)
      //{
      SqlCeCommand Ergebnis = new SqlCeCommand("SELECT * " + table);
      SqlCeCommand del = new SqlCeCommand("DELETE Units", conn);
      del.ExecuteNonQuery();
      Ergebnis = cb.GetInsertCommand();
      Ergebnis.Parameters.Clear();
      int p = 1;
      int z = 0;
      int a = 0;
      int r = 0;
      while ((table.Rows.Count) > a)
      {
        while ((table.Columns.Count) > c)
        {
    
          Ergebnis.Parameters.AddWithValue("@p" + (p), table.Rows[r][c]);
    
          p++;
    
          c++;
    
        }
        a++;
        r = a;
        c = 0;
      }
    
      Ergebnis.ExecuteNonQuery();
      adp.Update(ds);
      conn.Close();
    

    Freitag, 2. Juli 2010 16:37
  • Hallo,

    solange Du meinst, die Spalten müssen in einer Datenbank in irgendeiner Reihenfolge
    stehen, wird das nicht klappen. In einer relationale Datenbank spielen Positionen
    per Definitonem (nach E. F. Codd )  keine Rolle.
    Und deswegen gibt es bei ALTER TABLE auch keine Positionsangabe,
    sondern neue Spalten landen immer am Ende.

    Dein Code kann mit CommandBuilder auch deswegen nicht funktionieren,
    weil dort nicht über Indizes sondern über die SourceColumn  gearbeitet wird -
    dazu kommen noch SourceVersion und SourceColumnNullMapping -
    (wo wir am Ende wieder bei dem DataColumMapping von oben sind ;-)

    Gruß Elmar

    Freitag, 2. Juli 2010 17:12
    Beantworter
  • Hallo,

    ist dieser Bug nur auf Sql Compact beschränkt oder sind davon auch andere betroffen (MySql etc.)?

    Zum Glück bin ich ja nur Hobby-Programmierer, aber auch so ist es natürlich extrem Schade einen Haufen Zeit für soetwas zu investieren!

    Sicherlich gibt es bestimmt mehr als eine Möglichkeit diesen Fehler zu umschiffen (oder dieses Feature ;-) - Du kannst zwar im DataTable alles ändern, aber Schreiben kannst Du es halt nicht), aber das kanns doch wirklich nicht sein und so schwer kann es doch auch nicht sein einen Befehl zu implementieren, der den geänderten DataTable dann auch schreibt (ein ganz hemmungsloses overwrite z.B.).

     

    Danke trotzdem für Deine Zeit und Hilfe

     

    Gruß

    Mohrenkopf-Fan

    Freitag, 2. Juli 2010 22:52
  • Hallo,

    es ist kein Bug, war nie einer und wird nie einer sein!
    Die Regeln gelten für alle relationalen Datenbanken,
    so sie solche sind und keine Karteikästen (selbst bei MySq).

    Der Fehler liegt in Deinem Ansatz.
    Und ich könnte Dir auch Wege zeigen, wie man es lösen könnte,
    aber mit den Informationen, die Du bisher geliefert hast,
    ist mehr als die bereits gelieferten Andeutungen nicht möglich.

    Dazu müsste man schon wissen (nach EVA ):
    Wie sind die Eingabedaten strukturiert
    Wie sollen sie bearbeitet werden
    Welche Ausgabe soll geliefert werden.

    Gruß Elmar

     

    Samstag, 3. Juli 2010 07:07
    Beantworter
  • Hallo,

     

    aber Du hast doch gesagt, dass Positionen in einer relationalen Datenbank keine Rolle spielen (und das sehe ich genauso) - also ist Sql Compact unter dieser Voraussetzung entweder keine relationale Datenbank, da die Spaltenpositionen ja explizit und unveränderbar vorgegeben sind, oder es ist halt ein Bug.

    Ich gehe aber davon aus, dass wohl jede DB von irgendeiner der 333 Regeln abweicht, aber genug der Theorie.

    Das Problem besteht darin, dass die Input-Daten halt nicht sauber strukturiert sind, d.h. wenn Parameter nicht benutzt werden, sind sie auch nicht vorhanden.

    Durch die externe Anwendung können sich aber nicht nur diese Parameter ändern, sondern auch deren Anzahl, sowie die Anzahl der Files, was zur Folge hat, dass sich nicht nur die Anzahl der Datensätze jedesmal ändert, sondern auch die Anzahl und Zuordnung der Spalten.

    Um bei meinem Eingangsbeispiel zu bleiben:

    Textfile1: A=a1, D=d1, G=g1

    nach der Ausführung:

    Textfile1: A=a1, C=c1, G=g1, H=h1

    wobei die Werte entweder als String oder als Decimal eingelesen werden sollen.

    Die einzige Konstante dabei ist der Name der Textdatei, die immer in der ersten Zeile steht und sich deshalb als Schlüssel geradezu aufdrängt.

    Bearbeitet werden sollen aber alle möglichen Parameter (also auch die, die in der ursprünglichen Textdatei gar nicht vorhanden waren),  mit div. inhaltlichen Einschränkungen (JA/NEIN, Beschränkung des Wertebereichs etc.) aber das ist Detail.

    Die Ausgabe soll dann wieder in ein Textfile erfolgen, wobei aber strikt auf die Reihenfolge der übergebenen Parameter geachtet werden muß, was ja kein Problem wäre, wenn ich einfach jede Zeile unter Auslassung der null-Werte, schreiben könnte.

    Das Ganze hab ich mir modular vorgestellt:

    Modul I - Daten einlesen

    Modul II - Daten bearbeiten

    Modul III - Daten auslesen

    Modul IV - WinForm um die einzelnen Module zusammenzuführen

     

    Gruß

     

    Mohrenkopf-Fan

    Samstag, 3. Juli 2010 08:27
  • Hallo,

    Zunächst: Du solltest genauer lesen! Was Du dort zusammeninterpretierst, habe ich so nicht geschrieben.

    Damit wir uns nicht theoretischen Diskussionen verlieren, mal ein klein wenig Praxis.
    Deine Beschreibung ist nicht gerade hinreichend, aber mit etwas Phantasie (vor allem
    auch auf Deine Seite ;-))  solltest Du anhand des folgenden Beispiels sehen,
    wie man es machen könnte.

    Grundsätzlicher Ablauf ist entsprechend dem, was ich gestern vorgeschlagen hatte:

    • Einlesen aller Textdateien, wobei jede Textdatei in eine DataTable kommt.
    • Verarbeitung, die sich auf das Feststellen des Datentyps beschränkt und um Deine Anforderung ergänzt werden sollte.
    • Ausgabe (Datenbank), wobei die Tabelle erstellt und alle DataTables in eine Tabelle ("Mohrenkopf" übertragen werden.
    • Den Teil mit der Ausgabe als erneute Textdatei (oder Anzeige in einem Grid) habe ich ausgelassen.

    Der Code hat mehr Löcher als ein Schweizer Käse, aber Deiner Beschreibung war nicht mehr zu entlocken
    (und mehr als eine dreiviertel Stunde hatte ich mir eh nicht genehmigt - wegen Fußball u.a.m. ;-)
    Getestet ist das ganze für den Pseudo-Input wie im Kopf als Kommentar eingefügt ist.

    Da Dein Aufbau mit höchster Wahrscheinlichkeit anders (komplexer) aufgebaut ist,
    wirst  Du dort anfangen bzw. Deinen bestehenden Code dort einbauen müssen.

    using System;
    using System.Collections.Generic;
    
    using System.IO;
    using System.Text;
    using System.Globalization;
    using System.Data;
    using System.Data.SqlServerCe;
    
    namespace ElmarBoye.Samples.Code
    {
    
    /* 1. Eingabe: Textfile1.txt
    Textfile1
    A;D;G
    12,45;d1;1,0
    */
    
    /* 2. Eingabe: Textfile2.txt
    Textfile2
    A;C;G;H
    345,67;c1;g1;h1
    */
    
      /// <summary>Steuert die Verarbeitung.</summary>
      /// <example>
      /// var eva = new MohrenkopfEVA(@"F:\TEMP\Mohrenkopf", Properties.Settings.Default.NorthwindConnectionString);
      // eva.Ausführen();
      /// </example>
      public class MohrenkopfEVA
      {
        // Extended Property als SqlDbType
        internal const string DataTypeProperty = "DataType";
        
        private string _verzeichnis;  // Verzeichnis mit den Textdateien
        private string _connectionString; // Verbindung zur Datenbank
    
        public MohrenkopfEVA(string verzeichnis, string connectionString)
        {
          this._verzeichnis = verzeichnis;
          this._connectionString = connectionString;
        }
    
        /// <summary>
        /// Steuerung der Verarbeitung.
        /// </summary>
        public void Ausführen()
        {
          // Einlesen aller Textdateien in getrennte Tabellen
          var dataSet = new MohrenkopfEingabe(this._verzeichnis).Verarbeite();
    
          // Verarbeitet das DataSet
          // Hier nur: Überprüfen der Spalten bezüglich des Datentyps
          new MohrenkopfVerarbeitung().Verarbeite(dataSet);
    
          new MohrenkopfDatenbank(this._connectionString).Verarbeite(dataSet);
    
          // Weiteres wie eine erneute Ausgabe...
        }
      }
    
      #region Class MohrenkopfEingabe
      /// <summary>
      /// Liest alle Textdateien in ein DataSet.
      /// </summary>
      /// <remarks>
      /// Mangels Informationen nur angedeutet.
      /// </remarks>
      internal class MohrenkopfEingabe
      {
        string _verzeichnis;
    
        internal MohrenkopfEingabe(string verzeichnis)
        {
          if (!Directory.Exists(verzeichnis))
            throw new ArgumentException("verzeichnis");
    
          this._verzeichnis = verzeichnis;
        }
    
        #region Einlesen
        internal DataSet Verarbeite()
        {
          DataSet dataSet = new DataSet("Textdaten");
          foreach (var dateiname in Directory.GetFiles(this._verzeichnis, "*.txt"))
          {
            var table = EinlesenDatei(dateiname);
            dataSet.Tables.Add(table);
          }
          return dataSet;
        }
    
        private DataTable EinlesenDatei(string dateiname)
        {
          DataTable table = null;
          using (var reader = new StreamReader(dateiname))
          {
            var tabelleName = reader.ReadLine();
            table = new DataTable(tabelleName);
    
            // Hier kommen irgendwie die Spalten hin...
            var zeile = reader.ReadLine();
            {
              // kann natürlich auch ganz anders aussehen, steht nirgendwo
              string[] spalten = zeile.Split(';');
              foreach (var spalte in spalten)
                table.Columns.Add(spalte, typeof(string));
            }
    
            // die Daten...
            while ((zeile = reader.ReadLine()) != null)
            {
              // extrahieren hier via Split
              string[] daten = zeile.Split(';');
              table.Rows.Add(daten);
            }
          }
          return table;
        }
        #endregion Einlesen
      }
      #endregion Class MohrenkopfVerarbeitung
    
      #region Class MohrenkopfVerarbeitung
      /// <summary>
      /// Verarbeitet die eingelesenen Dateien als DataSet
      /// </summary>
      /// <remarks>
      /// Derzeit nur die Ermitteln des Datentyps, weiteres nach Bedarf
      /// </remarks>
      internal class MohrenkopfVerarbeitung
      {
    
        internal void Verarbeite(DataSet dataSet)
        {
          foreach (DataTable table in dataSet.Tables)
          {
            // Ermittelt den Datentyp
            DatentypErmitteln(table);
          }
        }
    
        /// <summary>
        /// Legt über eine ExtendedProperty den Datentyp fest, hier: string oder decimal
        /// </summary>
        /// <remarks>
        /// Weitere Analyse ist möglich wie
        /// Ermitteln Anzahl Nachkommastellen bei Decimal
        /// Ermitteln Maximale Länge der Spalte wenn String
        /// Andere Datentypen
        /// Die Informationen können über ExtendedProperties abgelegt werden.
        /// </remarks>
        private void DatentypErmitteln(DataTable table)
        {
          // Anfangs optimistisch: Alles ist Decimal
          foreach (DataColumn column in table.Columns)
          {
            column.ExtendedProperties.Add(MohrenkopfEVA.DataTypeProperty, SqlDbType.Decimal);
          }
    
          foreach (DataRow row in table.Rows)
          {
            // Bricht Prüfung ab, wenn keine Decimal mehr zu finden sind
            bool anyDecimalFound = false;
            foreach (DataColumn column in table.Columns)
            {
              // Nur wenn bisher als Decimal zulässig
              if (((SqlDbType)column.ExtendedProperties[MohrenkopfEVA.DataTypeProperty]) == SqlDbType.Decimal)
              {
                decimal value;
                if (!decimal.TryParse((string)row[column], NumberStyles.Number, CultureInfo.CurrentCulture, out value))
                {
                  // Einmal fehlgeschlagen, weiter als Zeichenkette
                  column.ExtendedProperties[MohrenkopfEVA.DataTypeProperty] = SqlDbType.NVarChar;
                }
                else
                {
                  anyDecimalFound = true;
                }
              }
            }
    
            // Sind alles String Datenypen, keine weitere Analyse
            if (!anyDecimalFound)
              break;
          }
        }
      }
      #endregion Class MohrenkopfVerarbeitung
    
      #region Class MohrenkopfDatenbank
      /// <summary>
      /// Übertragung des DataSets in die Datenbank.
      /// </summary>
      /// <remarks>
      /// Legt eine Tabelle für das gesamte DataSet an (Alternativ: Je Tabelle)
      /// Der Datentyp wird anhand einer ExtendedProperty ermittelt, siehe Verarbeitung
      /// </remarks>
      internal class MohrenkopfDatenbank
      {
        // Name der Tabelle
        private const string TabellenName = "Mohrenkopf";
    
        private string _connectionString;
    
        internal MohrenkopfDatenbank(string connectionString)
        {
          this._connectionString = connectionString;
        }
    
        internal void Verarbeite(DataSet dataSet)
        {
          var columns = SpaltenErmitteln(dataSet);
    
          // Erzeugen der Tabelle
          TabelleErstellen(columns);
    
          // Ausgabe in Tabelle
          Ausgabe(dataSet, columns);
        }
    
        /// <summary>Ausgabe aller Tabellen im DataSet anhander Spaltendefinition.</summary>
        private void Ausgabe(DataSet dataSet, List<ColumnDefinition> columns)
        {
          var insertCommand = BefehlErstellen(columns);
          using (var connection = new SqlCeConnection(this._connectionString))
          {
            connection.Open();
            insertCommand.Connection = connection;
    
            foreach (DataTable table in dataSet.Tables)
            {
              // Nicht verwendete Spalten immer Null
              LöscheParameter(insertCommand);
    
              // Ausgabe einer Tabelle
              AusgabeTabelle(table, insertCommand);
            }
          }
        }
    
        /// <summary>Überträgt eine Tabelle in die Datenbank.</summary>
        private void AusgabeTabelle(DataTable table, SqlCeCommand insertCommand)
        {
          var parameters = ParameterListe(table, insertCommand);
    
          foreach (DataRow row in table.Rows)
          {
            // Übertragen der gefunden DataColumns in die Tabelle 
            foreach (var pair in parameters)
            {
              SqlCeParameter parameter = pair.Value;
              // Konvertieren wenn Decimal, sonst String
              string parameterValue = (string)row[pair.Key];
              if (pair.Value.SqlDbType == SqlDbType.Decimal)
                parameter.Value = decimal.Parse(parameterValue, NumberStyles.Number, CultureInfo.CurrentCulture);
              else
                parameter.Value = parameterValue;
            }
    
            insertCommand.ExecuteNonQuery();
            
            // Erfolgreich geschrieben
            row.AcceptChanges();
          }
        }
    
        /// <summary>Erstellt einen INSERT Befehl der alle Spalten umfasst.</summary>
        private SqlCeCommand BefehlErstellen(List<ColumnDefinition> columns)
        {
          var builder = new StringBuilder(128);
    
          builder.AppendFormat("INSERT INTO [{0}]", TabellenName);
          builder.Append(" (");
          // Spaltenliste
          for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
          {
            if (columnIndex > 0) builder.Append(',');
            builder.AppendFormat("[{0}]", columns[columnIndex].ColumnName);
          }
          builder.Append(") VALUES (");
    
          // Parameterliste
          for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
          {
            if (columnIndex > 0) builder.Append(',');
            builder.AppendFormat("@p{0}", columnIndex);
          }
          builder.Append(");");
    
          string commandText = builder.ToString();
          System.Diagnostics.Debug.WriteLine(commandText, "INSERT TABLE");
    
          var command = new SqlCeCommand();
          command.CommandText = commandText;
    
          // Parameter erstellen
          for (var columnIndex = 0; columnIndex < columns.Count; columnIndex++)
          {
            var parameter = columns[columnIndex].GetParameter(columnIndex);
            command.Parameters.Add(parameter);
          }
          return command;
        }
    
        /// <summary>Erstellt die Parameterliste für eine DataTable durch Zuordnung der SourceColumn</summary>
        private IDictionary<DataColumn, SqlCeParameter> ParameterListe(DataTable table, SqlCeCommand insertCommand)
        {
          var parameters = new Dictionary<DataColumn, SqlCeParameter>();
    
          foreach (DataColumn column in table.Columns)
          {
            foreach (SqlCeParameter parameter in insertCommand.Parameters)
            {
              if (column.ColumnName.Equals(parameter.SourceColumn, StringComparison.OrdinalIgnoreCase))
              {
                parameters.Add(column, parameter);
                break;
              }
            }
          }
          return parameters;
        }
    
        /// <summary>Setzt alle Parameterwerte auf DBNull.</summary>
        private void LöscheParameter(SqlCeCommand command)
        {
          foreach (SqlCeParameter parameter in command.Parameters)
            parameter.Value = DBNull.Value;
        }
    
        #region Tabelle erstellen
        /// <summary>Erstellt eine Tabelle anhand des ermittelten Schemas.</summary>
        private void TabelleErstellen(List<ColumnDefinition> columns)
        {
          StringBuilder builder = new StringBuilder(128);
          builder.AppendFormat("CREATE TABLE [{0}]", TabellenName);
          builder.AppendLine(" (");
          builder.AppendLine("\tid int IDENTITY(1, 1) NOT NULL PRIMARY KEY");
          foreach (var definition in columns)
          {
            builder.Append("\t,");
            builder.AppendLine(definition.GetDbColumn());
          }
          builder.AppendLine(");");
    
          string commandText = builder.ToString();
          System.Diagnostics.Debug.WriteLine(commandText, "CREATE TABLE");
    
          using (var connection = new SqlCeConnection(this._connectionString))
          {
            connection.Open();
            using (var command = new SqlCeCommand(commandText, connection))
            {
              // TODO: Alte Tabelle ggf. löschen.
    
              command.ExecuteNonQuery();
            }
          }
        }
        #endregion Tabelle erstellen
    
        /// <summary>Ermittelt die Spaltendefinition anhand des DataSets</summary>
        private List<ColumnDefinition> SpaltenErmitteln(DataSet dataSet)
        {
          List<ColumnDefinition> columnList = new List<ColumnDefinition>();
          foreach (DataTable table in dataSet.Tables)
          {
            foreach (DataColumn column in table.Columns)
            {
    
              var definition = new ColumnDefinition(
                column.ColumnName,
                (SqlDbType)column.ExtendedProperties[MohrenkopfEVA.DataTypeProperty]);
              int index = columnList.IndexOf(definition);
              if (index == -1)
              {
                columnList.Add(definition);
              }
              else
              {
                // Abweichender Typ => String
                if (definition.DataType != columnList[index].DataType)
                  columnList[index].DataType = SqlDbType.NVarChar;
              }
            }
          }
          return columnList;
        }
        #endregion Ausgabe Datenbank
    
        #region ColumnDefinition Class
        /// <summary>Definiert das Schema der Tabelle für die weitere Verarbeitung</summary>
        internal class ColumnDefinition : IEquatable<ColumnDefinition>
        {
          internal ColumnDefinition(string columnName, SqlDbType dataType)
          {
            this.ColumnName = columnName;
            this.DataType = dataType;
          }
    
          internal string ColumnName;
          internal SqlDbType DataType;
    
          internal string GetDbColumn()
          {
            string dataTypeName = null;
            switch (this.DataType)
            {
              case SqlDbType.Decimal:
                dataTypeName = "decimal(18, 2)";
                break;
              default:
                dataTypeName = "nvarchar(255)";
                break;
            }
    
            // Spalte mit Datentyp und NULL erlaubt
            return string.Format("[{0}] {1} NULL", this.ColumnName, dataTypeName);
          }
    
          /// <summary>Erstellt einen Parameter aus der Definition.</summary>
          internal SqlCeParameter GetParameter(int columnIndex)
          {
            var parameter = new SqlCeParameter("@p" + columnIndex.ToString(), this.DataType);
    
            switch (this.DataType)
            {
              case SqlDbType.Decimal:
                parameter.Precision = 18;
                parameter.Scale = 2;
                break;
              case SqlDbType.NVarChar:
                parameter.Size = 255; // nvarchar(255)
                break;
              default:
                throw new NotSupportedException("DataType");
            }
    
            // Wird zur Bestimmung der DataColumn verwendet
            parameter.SourceColumn = this.ColumnName;
            return parameter;
          }
    
          public override string ToString()
          {
            return this.GetDbColumn();
          }
    
          public bool Equals(ColumnDefinition other)
          {
            // Vergleichen nach Namen ohne Berücksichtigung von Groß/Kleinschreibung
            return this.ColumnName.Equals(other.ColumnName, StringComparison.OrdinalIgnoreCase);
          }
        }
        #endregion ColumnDefinition Class
      }
    }
    
    Gruß Elmar

    Samstag, 3. Juli 2010 12:54
    Beantworter
  • Wow - ich bin ehrlich schwer beeindruckt von der Mühe, die Du dir machst um mir zu helfen.

    Fußballbedingt werde ich aber wohl erst morgen an die Umsetzung rangehen können.

    Im Voraus aber schon mal vielen Dank.

     

    Gruß

     

    Andi - der Mohrenkopf-Fan

    Samstag, 3. Juli 2010 13:53
  • Hallo,

     

    ich hab mal Deinen Code einfach 1:1 durchlaufen lassen (ausser split, das hab ich auf : abgeändert) - leider entspricht das Ergebnis überhaupt nicht meinen Erwartungen.

    Zeile 1 jeder urprünglichen Textdatei enthält "name:Name_der_Textdatei", dabei sollte name die erste Spalte sein und Name_der_Textdatei der erste Eintrag.

    In der erstellten DB fehlen diese Einträge komplett.

    Zeile 2 enthält "type:type_of", type sollte also die 2. Spalte ergeben und type_of den 2.Eintrag der ersten Zeile.

    In der erstellten DB habe ich aber folgende Spalten: id, type, type_of1, type_of2, type_of3 (was theoretisch richtig ist, es gibt nur 3 verschiedene), wobei in der Spalte type dann fortlaufend alle ersten Parameter aller Textfiles enthalten sind, die ja eigentlich die Spalten sein sollten und in der Spalte type_of1 dann alle zugehörigen Werte.

    Eine Überprüfung der Spalten ist auch nicht nötig - type ist immer string, length ist immer decimal, egal an welcher Position der Textfiles sie stehen.

    Sorry, da hatte ich mich wohl mißverständlich ausgedrückt.

    Kannst Du mir da nochmal auf die Sprünge helfen?

     

    Gruß

     

    Andi - der Mohrenkopf-Fan

    Sonntag, 4. Juli 2010 10:01
  • Hallo Andi,

    Deine Beschreibung hat nichts konkretes über den Aufbau der Textdateien enthalten,
    und so habe ich dort schlicht und einfach phantasiert.
    Deswegen: Entweder versuchst Du selbst, den Code ensprechend anzupassen
    (etwas solltest Du dafür ja schon haben) oder poste ein, zwei Beispieldateien.

    Gruß Elmar

    Sonntag, 4. Juli 2010 10:30
    Beantworter
  • Hallo,

    ich hab mal ein bißchen mit Deinem Code rumgespielt, komme aber überhaupt nicht damit zurecht - also Variante 2.

    Jede Zeile einer Textdatei besteht aus Spalte:Wert, die DB sollte nach dem Einlesen die Spalten name, groesse, laenge, hoehe, status, gewicht, beleuchtung in genau dieser Reihenfolge enthalten.

     

    Geblaese.txt besteht aus 4 Zeilen:

    name:Geblaese

    grosse:1

    laenge:4,5

    gewicht:12,45

     

    Motor.txt besteht aus 4 Zeilen:

    name:Motor

    groesse:1

    status:aus

    gewicht:8,97

     

    Getriebe.txt besteht aus 5 Zeilen:

    name:Getriebe

    laenge:9,98

    hoehe:3,75

    status:ein

    gewicht:15,99

    beleuchtung:____

     

    wobei ich ja auch schon alles soweit fertig hatte bis auf das Spaltensortieren.....

     

    Gruß

     

    Andi

    Sonntag, 4. Juli 2010 15:05
  • Hallo Andi,

    zunächst das Einlesen, tausche dafür die Methode EinlesenDatei aus:

     

        private DataTable EinlesenDatei(string dateiname)
        {
          var spalten = new List<string>();
          var daten = new List<string>();
          // ANSI-Encoding?
          using (var reader = new StreamReader(dateiname, Encoding.Default)) 
          {
            string zeile;
            while ((zeile = reader.ReadLine()) != null)
            {
              // evtl. weitere Überprüfungen (Null, Leer usw.) einbauen
              string[] zeilenwerte = zeile.Split(':');
              if (zeilenwerte.Length == 2)
              {
                spalten.Add(zeilenwerte[0]);
                daten.Add(zeilenwerte[1]);
              }
            }
          }
    
          DataTable table = new DataTable(dateiname);
          // Spalten anlegen
          foreach (var spalte in spalten)
          {
            table.Columns.Add(spalte, typeof(string));
          }
          // Daten (eine Zeile)
          table.Rows.Add(daten.ToArray());
          return table;
        }
    

     

    Komplizierter ist es mit dem Sortieren und nicht etwa wegen SetOrdinal ... ;-)

    Denn hier hängt es auch von den Eingabedaten ab. Denn es ist von Bedeutung,
    in welcher Reihenfolge die Dateien mit welchen Spalten verarbeitet werden.
    Ich habe zwar eine einfache Suchheuristik eingebaut, die auch nach der Vorgänger-
    bzw. Nachfolgerspalte sucht und versucht die Spalte einzusortieren.
    Das klappt naturgemäß aber nur, wenn eine der Spalten auch vorkommt.
    Taucht zunächst keine der Spalte auf, weil sie erst in einer späteren Datei
    verwendet wird, so geht das schief (in den Beispielen nicht der Fall).

    Einfacher wäre hier im allgemeinen, die Sortierung gleich zu hinterlegen -
    soviele Spaltennamen sollten wohl kaum vorkommen und ändern sollte es
    sich auch nicht alle paar Minuten.

    Tausche dazu die Methode SpaltenErmitteln aus:

     

     /// <summary>Ermittelt die Spaltendefinition anhand des DataSets</summary>
        private List<ColumnDefinition> SpaltenErmitteln(DataSet dataSet)
        {
          List<ColumnDefinition> columnList = new List<ColumnDefinition>();
          foreach (DataTable table in dataSet.Tables)
          {
            for (int columnIndex = 0; columnIndex < table.Columns.Count; columnIndex++)
            {
              var column = table.Columns[columnIndex];
              var definition = new ColumnDefinition(
                column.ColumnName,
                (SqlDbType)column.ExtendedProperties[MohrenkopfEVA.DataTypeProperty]);
              int index = columnList.IndexOf(definition);
              if (index == -1)
              {
                // Versucht die Position anhand der bereits gefundenen Daten zu ermitteln
                if (columnIndex > 0)
                {
                  // Nur der ColumnName interessiert (einfacher über IEquatable<string>)
                  var previousDefinition = new ColumnDefinition(table.Columns[columnIndex - 1].ColumnName, SqlDbType.NVarChar);
                  int previousColumnIndex = columnList.IndexOf(previousDefinition);
                  if (previousColumnIndex != -1)
                  {
                    columnList.Insert(previousColumnIndex + 1, definition);
                    continue;
                  }
                }
    
                if (columnIndex < table.Columns.Count - 1)
                {
                  var nextDefinition = new ColumnDefinition(table.Columns[columnIndex + 1].ColumnName, SqlDbType.NVarChar);
                  int nextColumnIndex = columnList.IndexOf(nextDefinition);
                  if (nextColumnIndex != -1)
                  {
                    columnList.Insert(nextColumnIndex, definition);
                    continue;
                  }
                }
    
                // Letztes Mittel Anfügen
                columnList.Add(definition);
              }
              else
              {
                // Abweichender Typ => String
                if (definition.DataType != columnList[index].DataType)
                  columnList[index].DataType = SqlDbType.NVarChar;
              }
            }
          }
          return columnList;
        }
    Ich habe die Name Spalte im übrigen drin gelassen. Die würde ich nicht gesondert
    behandeln sondern als zuätzliche Information mitnehmen. Denn gerade wenn Du
    die Zeilen auseinanderhalten wieder trennen willst, ist das nützlich.

     

    Wenn hier immer nur ein Satz enhalten ist (wovon ich nicht ausgegangen bin),
    könnte man sich das Ganze im übrigen vereinfachen und die vielen DataTables
    mit einer Zeile zusammenfassen. Da ist jetzt mehr Overhead als Nutzdaten enthalten.

    Selbst ein vollständiger Verzicht auf die DataTable und eine Lösung mit List<T> wäre denkbar
    Du solltest Dir Auflistungen und Datenstrukturen , denn das gehört zum Grund-Handwerkszeug
    eines Entwicklers, so wie das Getriebe zum Motor ;-)

    Aber beschäftige Dich noch ein wenig damit, ob Du selbst dazu die Lösung findest.
    Gehe dazu das Programm schrittweise (F10/F11) durch und schau Dir die Variablen an.
    Denn so eine Verarbeitung ist nun zunächst nicht besonders und die Konzepte
    (ich hab auf Tricks und Abkürzungen verzichtet) lassen sich immer wieder verwenden -
    so denn einmal verstanden, deswegen der Hinweis auf EVA.

    Gruß Elmar

    Sonntag, 4. Juli 2010 17:11
    Beantworter
  • Hallo Elmar,

    das sieht auf den ersten Blick sehr gut aus. Ich werde morgen mal alles durchgehen. Ich weiß jetzt aber auch, wie ich Spaltenpositionen verändern kann und dass mir SetOrdinal auch nicht geholfen hätte, da das eigentliche Problem das richtige Sortieren der Spalten ist.

     

    Nochmals vielen Dank - ich finde es echt Klasse mit welcher Ausdauer Du dich um anderer Leute Probleme kümmerst.

     

    Super - weiter so!

     

    Gruß

     

    Andi

    Sonntag, 4. Juli 2010 20:58
  • Hallo Andi,

    Danke für das Lob :-))
    Ich freue mich, dass ich ein klein wenig zur Lösung beisteuern konnte.

    Gruß Elmar

    Montag, 5. Juli 2010 07:11
    Beantworter