Posts mit dem Label Database werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Database werden angezeigt. Alle Posts anzeigen

Montag, 31. Oktober 2022

Tool Lagerverwaltung (Teil 4) Datenbank und Schnittstelle

Bisher hatte ich nur erwähnt, dass als dritte Schicht die Datenbank abbildet. Klingt etwas übertrieben, macht aber einen deutlichen Performance unterschied gegenüber der Verwendung von Text Dateien in CSV Format.

 

Benötigt

  • Visual Studio 2022 oder anderen Compiler
  • .NET 6.0
  • Codexzier's Application Framework
  • GitHub
  • SQLite
  • MS SQL oder PostgreSQL (nicht zwingend für den weiteren Verlauf)

Einbinden

Das Einbinden einer Datenbank soll so entkoppelt sein, dass die Datenbank Auswechselbar sein soll. Spricht, entweder soll SQLite für den Lokalen Einsatz verwendbar sein oder eine Externe Datenbank auf einem anderen Server.

 

Kein OR-Mapper

Man könnte an der stelle einen OR-Mapper verwenden, der für die Größe des Projektes auch völlig ausreicht. Aber ich will auf diesen Komfort verzichten und schreibe die SQL-Statement aus. Das Thema möchte ich für einen anderen Blogeintrag nutzen, in dem ein Umzug zum OR-Mapper beschrieben wird.

 

Tabellen

In den vorigen Post habe ich von den Einträgen immer von einer Sache gesprochen. Das liegt daran, dass in Zukunft zu speicherndem Inhalt offen ist und durch den Benutzer selbst definiert werden kann durch Vorlagen. 

Nun soll die Haupttabelle nicht 'Sache' oder 'Thing' heißen, und 'Objekt' erst recht nicht. Passend ist 'Artikel' oder 'Item'. Fehlt also noch als Eigenschaft ID für die Eindeutigkeit des Datensatzes und 'Titel' soll in Kurzform beschreiben, was der Eintrag ist. Weitere Eigenschaften kommen später, denn jede weitere Eigenschaft muss sich aus dem Nutzerkontext ergeben.

 public class ArticleItem  
 {  
   public long Id { get; set; }  
   public string Title { get; set; } = "title";  
   public string Description { get; set; } = String.Empty;  
   public bool IsArchived { get; set; } = false;  
   public bool IsTemplate { get; set; }  
   IEnumerable<IArticleSubItem> ArticleSubItems { get; set; } = Array.Empty<IArticleSubItem>();  
 }  


Schnittstellen definieren

Grundlegend werden mindestens drei Methoden benötigt. Speichern, Abrufen und aktualisieren. An der Stelle muss ich noch betonen, dass noch kein komplettes Konzept steht, weshalb die Schnittstelle rudimentär bleibt.

 public interface IDatabaseConnector  
 {  
   void CreateTable<TTable>();  
   void Insert(ArticleItem articleItem);  
   IEnumerable<ArticleItem> GetAll();  
   ArticleItem GetById(long id);  
   IArticleSubItem SubItem_GetById(long id);  
   void Update(ArticleItem articleItem);  
   void Update(IArticleSubItem subItem);  
 } 

 

Nuget Pakete für SQLite

Hier reicht nur die Pakete zu installieren für SQLite. Die Installation MS SQL oder PostgreSql gehe ich nur ansatzweise an, um später zu zeigen, inwieweit ein Wechsel zu einer anderen Datenbank möglich ist. Deshalb werden bei den beiden Datenbank Technologien, dessen Nuget Pakete nicht installiert.

  • SQLite-net-standard 1.5.1
  • PostgreSQL
  • MS SQL

PoC - Proof of Concept

Der Wechsel zu einer anderen Datenbank. Die Funktionalitäten würde man zunächst in der Solution unter dem Projekt Komponenten hinterlegen. Das würde bedeuten, dass die Abhängigkeiten zu den drei Datenbanken in diesem Projekt liegen und später vorrausetzen, dass die Bibliotheken zu den anderen zwei immer gefordert werden, obwohl nur eine der möglichen Datenbank verwendet wird.

 

-> Option 1

Alles so lassen und später die Pakete rausnehmen, die man nicht braucht.

 

-> Option 2

Schnittstelle und Datenbanken in eigene Projekte verschieben. Über das Projekt ‚Componente' wird je nach Einstellung entschieden, welche Datenbank und Bibliotheken geladen werden soll.

 

Qual der Wahl

Bei einem Hobby Projekt reicht in der Regel die erste Option aus, solange man das für sich nur nutzen möchte. Die Anwendung hat keine großen Performance Ansprüche und wenn Probleme auftreten, kann man diese selbst schnell lösen.

Warum Option 2? Nicht wegen der Performance, sondern die Möglichkeit statt einer Lokalen Datenbank, auf eine zentrale Datenbank zu wechseln. Aber wenn diese Option nicht genutzt wird, sollten zumindest keine Probleme entstehen mit Referenzen, die nicht genutzt werden.

 

Erweiterung der Solution

Ein Projekt muss angelegt werden für die Datenobjekte und Schnittstelle, die ich hier 'SharedBasis'. Und für jede zu verwendeter Datenbank bekommt ein eigenes Projekt und erhält die Referenz zu dem Projekt 'SharedBasis'.



Was kommt in 'SharedBasis'

Hauptsächlich die Grunddaten Objekte und Schnittstellen, mehr sollte da nicht hineinkommen. Im Folgenden belasse ich mich auf die Schnittstelle für das Abrufen und Aktualisieren der Daten. Das Datenobjekt Artikel bekommt nur wesentliche Informationen, erst über dem ArticleSubItem wird der Umfang des Artikels ausgebaut. Darauf gehe ich in einen anderen Blogeintrag weiter darauf ein.

 public interface IArticleSubItem  
 {  
   long Id { get; set; }  
   long ArticleId { get; set; }  
 }  

Für den Ausbau der zu definierenden 'SubItems', ist für den aktuellen Stand noch nicht erforderlich. Letzten Endes gehe ich nur auf Grundlagen ein, die aber soweit reichen müssen, damit sich für mich die weiteren Details erschließen.

 

Ein Projekt pro Datenbank

Die Verbindung zu den Datenbanken sind eigentlich bei allen sehr ähnlich und doch werden diese getrennt behandelt in jeweils einem Projekt. In jedes Datenbank Projekt muss einmal die Reference 'WarehouseManagement.SharedBasis' hinzugefügt werden. Für ein wenig Ordnung habe ich die Projekte in einen Solution Ordner 'Data' angelegt.


Interface einsetzen

Wie bereits im Projekt 'WarehouseManagement.DatabaseSQLite' zu sehen ist, habe ich die Klasse DatabaseConnector.cs angelegt, bzw. wurden die vorhandenen Klassen mit den Namen 'Class.cs' umbenannt und das Interface 'IDatabaseConnector' implementiert.

 public class DatabaseConnector : IDatabaseConnector  
 {  
   #region interface methods  
   public void CreateTable<TTable>()  
   {  
     this.Execute(db => db.CreateTable<TTable>());  
   }  
   public IEnumerable<ArticleItem> GetAll()  
   {  
     throw new NotImplementedException();  
   }  
   public ArticleItem GetById(long id)  
   {  
     throw new NotImplementedException();  
   }  
   public void Insert(ArticleItem articleItem)  
   {  
     throw new NotImplementedException();  
   }  
   public IArticleSubItem SubItem_GetById(long id)  
   {  
     throw new NotImplementedException();  
   }  
   public void Update(ArticleItem articleItem)  
   {  
     throw new NotImplementedException();  
   }  
   public void Update(IArticleSubItem subItem)  
   {  
     throw new NotImplementedException();  
   }  
   #endregion  
 }   

Attribute einsetzen

Man kann den Namen der Datenbank Technologie in der Eigenschaft weglegen, erfordert jedoch, dass eine Instance vom 'DatabaseConnector' gestartet wird. Oder erweitert den Klassennamen zu 'DatabaseConnector' mit den Technologienamen. Beides ist nicht schön und deshalb passt die Verwendung von selbst erstellten Attributen, um dort den Technologienamen abzulegen.

 public class DatabaseConnectorNameAttribute : Attribute  
 {  
   public DatabaseConnectorNameAttribute(string name)  
   {  
     this.Name = name;  
   }  
   public string Name { get; }  
 }  

So bleibt der Grundname und dennoch können die Klassen von ihrer Technologie unterschieden werden.

 [DatabaseConnectorName("SQLite")]  
 public class DatabaseConnector : IDatabaseConnector  
 {  
 ...  
 [DatabaseConnectorName("PostgreSQL")]  
 public class DatabaseConnector : IDatabaseConnector  
 {  
 ...  
 [DatabaseConnectorName("MS SQL")]  
 public class DatabaseConnector : IDatabaseConnector  
 {  
 ...  

Test anlegen

Für das Erfassen der Datenbank Verbindungen (DatabaseConnector) wird im Projekt 'WarehouseManagement.Components' keine Reference der Datenbank Projekte hinzugefügt. Stattdessen soll das zur Laufzeit passieren und hierfür soll eine Klasse die Aufgabe übernehmen. Hier muss jedoch die 'WarehouseManagement.SharedBasis' Reference hinzugefügt werden, um später die Schnittstelle aus den Datenbank Projekten zu erkennen.

Der erste Unit Test soll die kompilierten DLLs aus den Datenbank Projekten einlesen. Beim ersten Mal und nach Änderungen der Datenprojekten, muss darauf geachtet werden, dass diese einen Aktuellen Bild hinterlegen. Oder einfach die gesamte Solution Inhalt neu kompilieren.

 [TestClass]  
 public class DatabaseConnectionTest  
 {  
   [TestMethod]  
   public void ReadDlls()  
   {  
     // arrange  
     // act  
     var result = DatabaseConnection.GetDatabaseConnectors();  
     // assert  
     Assert.IsNotNull(result);  
     Assert.AreEqual(3, result.Count());  
   }  
 }  

DLL Dateien Namentlich filtern

Da alle Projektinhalte in einem Ordner landen, werden beim Abrufen der DLL-Dateinamen auch die mitgenommen, die keine Verbindung haben zu einer Datenbank.

 

 

Deshalb sollte hier der einfach nach den Projektnamen 'WarehouseManagement.Database' gefiltert werden.

Nun läuft der Test durch, ohne dass die DLLs gefunden wurden. Das liegt daran, dass für das Unit Test Projekt die DLLs nicht als Referenz genannt sind. Deshalb müssen diese noch rüber kopiert werden aus den Bin Order des jeweiligen Datenbank Projektes. Damit man über den Dateiexplorer nicht immer händisch rüber kopieren muss, kann dies über den Test Projekt Eigenschaften angeordnet werden mit den Build -> Output -> Post-build event und Xcopy

xcopy $(SolutionDir)WarehouseManagement.DatabaseSQLite\bin\Debug\net6.0\WarehouseManagement.DatabaseSQLite.dll $(SolutionDir)WarehouseManagement.Components.Test\bin\Debug\net6.0 /Y  
 xcopy $(SolutionDir)WarehouseManagement.DatabaseMsSQL\bin\Debug\net6.0\WarehouseManagement.DatabaseMsSQL.dll $(SolutionDir)WarehouseManagement.Components.Test\bin\Debug\net6.0 /Y  
 xcopy $(SolutionDir)WarehouseManagement.DatabasePostgreSQL\bin\Debug\net6.0\WarehouseManagement.DatabasePostgreSQL.dll $(SolutionDir)WarehouseManagement.Components.Test\bin\Debug\net6.0 /Y  

Methode zu Ende ausschreiben

Nun sollte die Methode zum Einlesen der Datenbank Verbindungsstücke aus den DLLs gelesen werden können. Die Folgende Ausführung ist sehr kompakt geschrieben, welches durch Linq, Lamda-Ausdrücken und Reflection realisiert wird. Hier empfehle ich, die von euch gewohnte Ausschreibung des Codes zu verwenden. Dann fällt ein späterer Einblick in die einzelnen Codestellen leichter.

 public class DatabaseConnection  
 {  
   public static IEnumerable<string> GetDatabaseConnectors()  
   {  
     return Directory  
       .GetFiles(Environment.CurrentDirectory)  
       .Where(file => file.Contains("WarehouseManagement.Database"))  
       .Select(Assembly.LoadFrom)  
       .SelectMany(assembly => assembly.GetExportedTypes())  
       .Where(type => typeof(IDatabaseConnector).IsAssignableFrom(type))  
       .Select(type => type.Name);  
   }  
 }  

Ansatz fertig

Und lasst euch in euerer Programmierung nicht reinreden, wenn eure Lösung funktioniert. Ein besser gibt's nicht. Sondern nur andere Lösungsformen.

Außerhalb des Themas 'Tool Lagerverwaltung' beschreibe ich den weiteren Ausbau der Datenbank Verbindungen in einen für sich geschlossenen Blogpost. In diesen ist der Ansatz gezeigt, wie sowas aufgebaut sein könnte.


Codestand: Database and interface

 

Übersicht

Teil 1 - Wiki anlegen in GitHub

Teil 2 - Neue Solution

Teil 3 - Noch ein Wiki anlegen

Teil 4 - Datenbank und Schnittstelle

Teil 5 - Konzept ausschreiben

Teil 6 - Umzug nach .NET MAUI

 

Links

 

Donnerstag, 26. April 2018

SQLite oder CSV auf Raspberry Pi 2/3 mit Win 10 IoT


Irgendwann kommt der Punkt, da möchte man seine Daten auch speichern. Bei dem Einsatz von vielen Daten kann auf die Klassische Art in einer CSV im IsolatedStorage gespeichert werden. Ist einfach zu lesen führt aber zu redundante Dateninhalte. Mit SQLite lassen sich relationale Dateninhalte zusammenstellen. Aber man muss zusätzliche Referenzen hinzufügen und sich mit SQL auseinandersetzen (allerdings nur ein wenig). Beide Varianten funktionieren auf PC, Tablet, Windows Phone und natürlich auf Raspberry Pi 2 und 3.

Was nehme ich?
Vorweg sollte man sich fragen, was wird mein Ziel. Das hängt immer von der eigenen Anwendung ab. Soll in der Stunde ein Durchschnittswert errechnet werden der dann angezeigt werden soll, dann reicht sicherlich ein Array.
Möchte ich Benutzereinstellungen Speichern? Dann könnte der folgende Programmschnipsel reichen der den IsolatedStorage verwendet.

ApplicationData.Current.LocalSettings.Values["MyKey"] = MyValue;
if (ApplicationData.Current.LocalSettings.Values.ContainsKey("MyKey"))
   
MyValue = ApplicationData.Current.LocalSettings.Values["MyKey"];

Aus <https://stackoverflow.com/questions/42750736/uwp-how-to-use-isolated-storage>

Für diesen Bleiben wir zunächst bei einfachen Datensätzen, die einfach hintereinander gespeichert und in einer Tabelle abgebildet werden können. Der Teil für Relationale Datenhaltung wird auf den nächsten Post eingegangen.

Datenabruf
Der Vorteil einer Datenbank Abfrage ergibt sich dann beim Abrufen der Daten. Wurden z.B. so viele Daten gespeichert, dass es in Summe ca. 20 Megabyte groß ist, dann könnte sich das Öffnen der CSV Datei etwas lang werden. Und wenn man nur zu einem Bestimmten Zeitraum oder einen Bereich haben möchte, dann ist das gesamte einlesen einer Datei und das Parsen der Inhalte relative Zeitintensiv.

Performance Vergleich?
Hierzu muss man sagen, dass der IsolatedStorage nicht gedacht ist, schnell einzelne Daten zu speichern. In der Demo Anwendung sind zwei provisorische Klasse mit IsolatedStorage angelegt. Diese sind nur soweit geschrieben, das damit Lesen, Speichern und zurück setzen ermöglicht. (siehe am Ende Link zum Github Repository). Letzten Endes sollte anhand der Ergebnisse zeigen, was für den einen oder anderen die bessere Entscheidung sein könnte und unabhängig von weiteren Kriterien betrachtet werden sollte.

Test Vorgang
Als erstes wird festgelegt, wie viele Daten geschrieben werden. Vorbelegt als Default Anzahl sind zehn Datensätze zu erstellen. Die Werte sind in allen Datensätzen dieselben, außer der letzte wird abwechselnd zu gewiesen (hier Wohnzimmer und Flur).


Jeder Vorgang wird zehnmal ausgeführt und gemessen. Daraus ergibt sich dann die Durschnittszeit.

  • Bestehende Daten löschen
  • Zehn Mal die Anzahl Daten schreiben. Bei jeden neu lauf, werden die Daten wieder gelöscht.
  • Die Zehn Daten, zehnmal lesen. Daten werden immer neu geladen.
  • Datenabrufen und auf "Wohnzimmer" filtern. Auch hier werden die Daten immer neu geladen

 Nach dem Durchlauf werden die Ergebnisse der einzelnen Durchläufe sowie der Durchschnittswert angezeigt.

Beispiel Anwendung
Die Beispiel UWP Anwendung ist nur Zweckmäßig für den Test aufgebaut. Oben Links kann über die Textbox ein Wert ab 1 eingetragen werden und setzt damit die Anzahl Daten, die geschrieben werden sollen. Mit dem Button "Run" wird der Test Ausgeführt. Sobald dieser durchlaufen ist, steht unter dem Button "Finish".
Rechts sind zwei Textboxen die wiederum zwei Beispiele zu SQLite und IsolateStorage abbilden, die im Programmcode im Einzelnen betrachtet werden können. (Die Ergebnisse im Bild können von PC zu PC abweichen)


Ergebnisse
Ziel Systeme sind natürlich der Raspberry Pi 2 und 3. Warum das Schreiben mit SQLite auf dem Raspberry Pi3 langsamer ist, konnte ich mit dem Testaufbau noch nicht ermittelt. Zudem kommt, dass nach dem Aufräumen der Methoden Inhalte, sich die Zeiten verschlechtert haben. Warum dies ist, werde ich auf einen späteren Post eingehen.

Ergebnisse in dem der Programmcode einfach runter geschrieben wurde


Ergebnisse nach dem Aufräumen


Fazit
Der Aufbau der Methoden kann sicherlich besser gelöst werden. Die Methoden Inhalte in 'Func<T>' auszulagern ließ die Ergebnisse deutlich mehr schwanken. Die Gemessene Zeit stieg um das Sieben- bis Zehnfache an beim Lesen mit 'Where' Abfragen. Der Test mit Schreiben in die SQLite Datenbank ist dahingegen weniger abweichend, dafür aber beim IsolatedStorage ist die Durschnittszeit auf ca. das zwanzigfache angestiegen.
Beide haben Vor- und Nachteile. Mit diesen Beispiel Test wäre IsolatedStorage gut fürs schnelle Speichern und für das schnelle Lesen die SQLite Datenbank.
Die Auslegung der Tests ist sehr spärlich und behandelt noch nicht das Verwenden von Relationalen Daten. Das kommt dann mit den nächsten Posts.

Offene Punkte:
  • Speichern von Relationalen Daten
  • Abruf von Relationalen Daten
  • Probleme mit Code Optimierung




Referenzen

Ameisen Simulation und andere Dinge

Wer lange sich schon mit C# beschäftig, hat sicherlich schon mal was von AntMe gelesen oder gehört. Diese Idee hatte ich aufgegriffen und mi...