typisierter Db-Zugriff mit Connectoren

    • VB.NET
    • .NET (FX) 4.0

    Es gibt 3 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      typisierter Db-Zugriff mit Connectoren

      Anbei zwei kleine Projekte, sehr ähnlich den Samples zu Daten laden und speichern.
      Hauptunterschied ist, dass hier die Datasets in Datenbanken geschrieben werden. Dabei wird der Connector benutzt, ein Assistent, der zB automatisch anspringt, wenn man eine Datenbank-Datei dem Projekt hinzufügt. Der Connector analysiert dann die Datenbank, und erstellt ein dazu kompatibles typisiertes Dataset, und erstellt für jede Tabelle auch einen typisierten TableAdapter, mit dem die Tabelle persistiert werden kann.
      BilderSerie, wie das vonstatten geht:
      1. zunächst die mdb ins Ausführungsverzeichnis kopieren, und im Projektmappenexplorer die Ansicht "alle Dateien" aktivieren, damit man die mdb auch sieht, obwohl sie noch nicht zum Projekt gehört

      2. Dann natürlich die mdb dem Projekt hinzufügen, was den Connector-Assistenten startet

      3. "Dataset"-Weiter klicksen, und alle Tabellen auswählen

        "Finish" klicksen
      4. Hier sieht man über der mdb nun das neu generierte typisierte Dataset mit 3 untergeordneten Dateien, also es besteht derzeit aus 4 Dateien: .xsd, .Designer.vb, .xsc, .xss

        Es empfiehlt sich, die .xsd ins projektverzeichnis zu verschieben, denn im Debug-Ordner haben eingebundene Projekt-Dateien eigentlich nichts verloren. Dass die .mdb dort verbleiben darf ist eine Ausnahme, geschuldet ihrer besonderen Konfiguration - ich komme noch dazu.
      5. Doppelklick auf die .xsd öffnet den Dataset-Designer:

        Dataset1 hat nur eine Tabelle, aber beachte, dass unten dran der Datatabel1TableAdapter dran hängt, ein typisierter Adapter zwischen DataTable1 und der Datenbank
      6. Im ProjektmappenExplorer die .mdb anwählen und F4 drücken, um zu den Datei-Eigenschaften zu kommen. Dort die Property "copy to output-Directory - do-not-copy" einstellen, weil sonst werden Daten-Änderungen bei jedem Testlauf überschrieben, und man hat den (falschen) Eindruck, nix würde funktionieren.

      7. Und in den Settings der ConnectionString ist falsch:

        Provider=Microsoft.ACE.OLEDB.12.0;Data Source=|DataDirectory|\bin\Debug\MostSimple.mdb;Persist Security Info=True steht da, aber das |DataDirectory| ist bereits \bin\Debug - also der Connectionstring muss auf Provider=Microsoft.ACE.OLEDB.12.0;Data Source=|DataDirectory|MostSimple.mdb;Persist Security Info=True korrigiert werden, sonst gibts komische Fehler, weil er die Datei nicht findet.
        (naja, vlt. einfacher wäre gewesen, die .mdb eben nicht im Debug-Ordner einzubinden, sondern auf Projektebene, und ihre Datei-Eigenschaft halt auf "Copy if newer" zu setzen statt auf "not copy")
      8. Uff! - nun können wir endlich den Form-Designer öffnen, und vom Datenfenster (vs2010: "Menu - Data - Show-Data-Sources" / vs2013: "Menu - View - Other-Windows - Data-Sources") die Tabelle aufs Form ziehen, wodurch das DataGridView generiert wird, sowie eine typDataset-Instanz, eine TableAdapter-Instanz, und eine BindingSource.
        . . .
        Darüber hinaus wird auch ein TableAdapterManager und ein BindingNavigator hingeneriert, aber diese beiden Dinge sind unnützer Schrott, und sofort wieder runterzuwerfen.
      So - nu könnemer auch Code schreiben:

      VB.NET-Quellcode

      1. Private _IdentityCommand As New OleDb.OleDbCommand("SELECT @@IDENTITY")
      2. Private Sub frmMostSimple_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
      3. AddHandler DataTable1TableAdapter.Adapter.RowUpdated, AddressOf RequeryId
      4. Reload()
      5. End Sub
      6. Private Sub RequeryId(ByVal sender As Object, ByVal e As System.Data.OleDb.OleDbRowUpdatedEventArgs)
      7. If e.StatementType = StatementType.Insert Then
      8. _IdentityCommand.Connection = e.Command.Connection
      9. _IdentityCommand.Transaction = e.Command.Transaction
      10. e.Row(e.Row.Table.PrimaryKey(0)) = _IdentityCommand.ExecuteScalar()
      11. End If
      12. End Sub
      13. Private Sub Reload()
      14. DataTable1TableAdapter.Fill(DataSet1.DataTable1)
      15. End Sub
      16. Private Sub Save()
      17. 'Me.Validate: in Bearbeitung stehende Zellwerte - falls valide - als Eingabe übernehmen
      18. If Not Me.Validate Then Media.SystemSounds.Hand.Play() : Return
      19. DataTable1TableAdapter.Update(DataSet1.DataTable1)
      20. Media.SystemSounds.Asterisk.Play()
      21. End Sub
      Zunächst ist in Form_Load per AddHandler (zeile#4) das TableAdapter_RowUpdated-Event auf die RequeryId()-Event-Methode zu leiten, damit bei jedem ausgeführten Insert-Command ein Select-Command nachgeschossen wird, welches den Primkey abfragt, der von der DB vergeben wurde.
      Weil Primkeys konfiguriert man mit AutoIncrement, also die Db legt den PK endgültig fest, nachdem ja bereits im Dataset für neue Datensätze ein provisorischer PK generiert werden musste (Pficht-Feld).
      Sieht man ja auch inne RequeryId()-Methode: der Db-Primkey-Wert wird explizit als Primkey im typDataset eingetragen (zeile#12).
      Also im Normalfall (Datensatz mit Autowert-PK) ist das Inserten immer ein bidirektionaler Vorgang: Das Dataset sendet den Datensatz an die Db, bekommt von der Db den endgültigen Primkey dieses Datensatzes zurück und muss ihn einpflegen.
      Ansonsten ist Laden (Reload()) und Speichern (Save()) bei diesem Simpel-Dataset höchst-simpel: die Kommunikation übernimmt der TableAdapter mit seinen eingebauten generierten Select, Delete, Update, Insert - Commands. Wir selbst haben mit Sql, Db-Commads, -Readern und Kram nichts zu schaffen. :D
      Weitere Select-Queries kann man den TableAdaptern per Kontext-Menu im Dataset-Designer hinzufügen - vorzugsweise um anhand von Parametern eine gefilterte Untermenge der Db-Tabelle abzurufen.
      Dateien
      • WithDataBase.zip

        (742,12 kB, 322 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 9 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Hallo @ErfinderDesRades,

      die Variante scheint mir sehr simpel... ist es bei dieser Variante notwendig, das Access auf dem Zielsystem installiert sein muss? oder wird beim Release die DB im Programm hinterlegt oder wie stelle ich mir das vor?
      Frage2: die DB muss vorher über Access komplett erstellt werden, richtig? also alle Tabellen müssen vorhanden sein, das geht nicht direkt über den Designer, richtig?
      Grüße
      Michael
      "Hier könnte Ihre Werbung stehen..."
      Ein richtiges Datenmodell
      Obiges ist mit nur einer Tabelle ja nicht wirklich ein relationales Datenmodell - es enthält ja nicht eine Relation.
      Daher habe ich dasselbe Klicki-Klicki-Brimborium (s. Bild-Serie aus post#1) auch mit dem 2. Projekt "Phonebook" vollzogen, welches ist ein Addressbuch, wo es immerhin eine 1:n-Relation zwischen Titeln und Personen gibt. Ein Doppelklick im Dataset-Designer auf die Verbindungslinie zeigt die Relation, wie der Connector sie aus den Verhältnissen der Datenbank generiert hat:

      Wichtig! Die Relationen sind unbedingt sofort auf Richtigkeit zu überprüfen, manche Connectoren bauen Mist, etwa meiner hatte UpdateRule und DeleteRule nicht auf Cascade gestellt.
      Also der Normalfall einer Relation (1 PrimkeySpalte mit AutoIncrement) muss konfiguriert sein wie auf dem Bild: Update-, Delete-Rule auf Cascade, AcceptRule.None.

      Auch der Code verkompliziert sich:

      VB.NET-Quellcode

      1. Public Class frmPhonebook
      2. Private _BindingSources As BindingSource()
      3. Private _IdentityCommand As New OleDb.OleDbCommand("SELECT @@IDENTITY")
      4. Private Sub frmPhonebook_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
      5. PersonTableAdapter.Connection = TitleTableAdapter.Connection
      6. _BindingSources = {TitleBindingSource, PersonBindingSource}
      7. AddHandler TitleTableAdapter.Adapter.RowUpdated, AddressOf RequeryId
      8. AddHandler PersonTableAdapter.Adapter.RowUpdated, AddressOf RequeryId
      9. Reload()
      10. End Sub
      11. Private Sub RequeryId(ByVal sender As Object, ByVal e As System.Data.OleDb.OleDbRowUpdatedEventArgs)
      12. If e.StatementType = StatementType.Insert Then
      13. _IdentityCommand.Connection = e.Command.Connection
      14. _IdentityCommand.Transaction = e.Command.Transaction
      15. e.Row(e.Row.Table.PrimaryKey(0)) = _IdentityCommand.ExecuteScalar()
      16. End If
      17. End Sub
      18. Private Sub Reload()
      19. SetDbAccess(True, True)
      20. PhoneDts.Clear()
      21. 'Reihenfolge! - Parent-Table zuerst befüllen
      22. TitleTableAdapter.Fill(PhoneDts.Title)
      23. PersonTableAdapter.Fill(PhoneDts.Person)
      24. SetDbAccess(True, False)
      25. End Sub
      26. Private Sub Save()
      27. 'Me.Validate: versuchen, in Bearbeitung stehende Zellwerte als Eingabe zu übernehmen - sonst Return
      28. If Not Me.Validate Then Media.SystemSounds.Hand.Play() : Return
      29. SetDbAccess(False, True)
      30. SaveDataset(PhoneDts, TitleTableAdapter.Adapter, PersonTableAdapter.Adapter) 'Reihenfolge! - Parent-Adapter zuerst
      31. SetDbAccess(False, False)
      32. Media.SystemSounds.Asterisk.Play()
      33. End Sub
      34. ''' <summary> adapters schreiben dts-Änderungen in die Datenbank. Im ParamArray darf der
      35. ''' Adapter einer ChildTable nicht vor dem Parent-Adapter aufgeführt werden </summary>
      36. Private Shared Sub SaveDataset(ByVal dts As DataSet, ByVal ParamArray adapters As DbDataAdapter())
      37. Const _UnDeleted As DataViewRowState = DataViewRowState.Added Or DataViewRowState.ModifiedCurrent
      38. For Each adp In adapters ' zunächst Zufügungen und Änderungen senden
      39. adp.Update(dts.Tables(adp.TableMappings(0).DataSetTable).Select("", "", _UnDeleted))
      40. Next
      41. ' AcceptRule.Cascade bei den noch verbliebenen Löschungen, denn die Db-Löschweitergabe erübrigt das Senden untergeordneter Löschungen
      42. SetAcceptCascade(dts.Relations, True)
      43. For Each adp In adapters
      44. adp.Update(dts.Tables(adp.TableMappings(0).DataSetTable))
      45. Next
      46. SetAcceptCascade(dts.Relations, False)
      47. End Sub
      48. Private Shared Sub SetAcceptCascade(ByVal relations As DataRelationCollection, ByVal state As Boolean)
      49. For Each rl As DataRelation In relations
      50. If rl.ChildKeyConstraint IsNot Nothing Then
      51. rl.ChildKeyConstraint.AcceptRejectRule = If(state, AcceptRejectRule.Cascade, AcceptRejectRule.None)
      52. End If
      53. Next
      54. End Sub
      55. ''' <summary> Vor-/Nach-Bereitungen des Db-Zugriffs an Connection, DataTables und BindingSources </summary>
      56. Private Sub SetDbAccess(ByVal forLoading As Boolean, ByVal state As Boolean)
      57. For Each bs In _BindingSources
      58. bs.RaiseListChangedEvents = Not state
      59. If Not state Then bs.ResetBindings(False)
      60. Next
      61. If state Then TitleTableAdapter.Connection.Open() Else TitleTableAdapter.Connection.Close()
      62. If Not forLoading Then Return
      63. For Each tb As DataTable In PhoneDts.Tables
      64. If state Then tb.BeginLoadData() Else tb.EndLoadData()
      65. Next
      66. If Not state Then PhoneDts.EnforceConstraints = True
      67. End Sub
      Zunächstmal werden die Connections aller TableAdapter zusammengelegt (zeile#7) - es ist sinnlos und unperformant, wenn jeder TableAdapter seine eigene Connection öffnen und schließen muss.
      Dann werden die in Benutzung stehenden BindingSources in ein Array gepackt zur späteren Verwendung (#8)
      Alle TableAdapter leiten nun ihr .RowUpdated-Event auf die RequeryId()-EventHandler-Methode (#9,10). Der Sinn davon ist ja bereits in post#1 erklärt.
      Sowohl Reload() als auch Save() versetzen alle relevanten Objekte in einen "DbAccess"-Status (und zurück!): (#23, 28, 34, 36)
      Reload() scheint simpel - aber Achtung! Die Reihenfolge ist entscheidend: Übergeordnete Tabellen sind unbedingt vor den ihnen untergeordneten Tabellen zu laden. Sonst Absturz, wenn untergeordnete DataRows auf ParentRows verweisen, die noch nicht geladen sind.

      Updaten mit Löschweitergabe - DeleteRule und AcceptRejectRule der DataRelations
      Beim Einrichten einer Relation in der DB ist sehr empfohlen, Löschweitergabe zu aktivieren. Andernfalls lehnt die Db Löschungen ab, falls dadurch Verweise untergeordneter Datensätze ungültig würden - und derlei Ablehnungen verursachen im Client Exceptions.
      Auch im Client ist Löschweitergabe einzurichten - siehe nochmal Abb#1 dieses Posts.
      Daraus folgt aber, dass beim Senden von Delete-Commands an die Db die untergeordneten einer gelöschten DataRow nicht an die Db zu senden sind - das macht die Db ja selber!
      Die Lösung bedient sich zweier Datenbanking-Grundprinzipien: Transaktion und Crud
      Crud bedeutet Create, Read, Update, Delete, damit korespondieren die SqlCommands Insert, Select, Update, Delete, aber auch die Änderungsverfolgung des Datasets kennt entsprechendes: Nämlich die DataRowstates Added, Unchanged, Modified, Deleted.
      Beim Transaktions-Prinzip entspricht der Commit/Rollback der Datenbank dem Accept-/Reject-Changes im Dataset. Also Änderungen sind vorläufig, und erst Commit/Accept macht sie endgültig (und was Rollback/Reject macht, müsster euch nu denken ;) ).
      Ein DataAdapter sendet nun für jede Datarow das ihrem RowState entsprechende DbCommand, und bei Rowstate.Unchanged sendet er natürlich nicht. Ausserdem, sobald er eine Row geupdated hat, accepted er sie auch - was ja ihren RowState auf Unchanged rücksetzt.
      Und nun endlich kommt die DataRelation.AcceptRule ins Spiel: Steht diese auf .Cascade, so werden untergeordnete Rows mit-accepted, wenn die übergeordnete accepted. Mit diesem Mechanismus können wir nun das Senden untergeordneter Löschungen unterdrücken: Zunächst alle NichtLöschungen updaten, dann AcceptRule.Cascade einstellen, dann alle Löschungen updaten. Dabei immer schön die übergeordneten Tabellen zuerst, sodass wenn die untergeordneten drankommen die "löschweitergegebenen" bereits accepted sind, und vom Update ausgenommen.
      Den Algo dazu habe ich in eine für jede Db-Anwendung wiederverwendbare Shared Sub SaveDataset() verlagert (#42).
      Er ruft in 2 ForEach-Schleifen jeweils die DataAdapter in ihrer hierarchischen Reihenfolge auf: Die erste Schleife updated alle NichtLöschungen (#44-46), die zweite updated die verbliebenen Löschungen (#49-51), und zwar wie gesagt unter AcceptRejectRule.Cascade
      Man kann übrigens das Löschverhalten der DB auch anders konfigurieren (etwa On Delete SetNull), oder noch anders, aber evtl. funktioniert die hiesige Update-Strategie dann nicht mehr.

      Bleibt noch die Methode SetDbAccess() (#64) zu erläutern.
      SetDbAccess() unterscheidet, ob Befüllung oder Abspeichern vorgesehen ist. Beim Befüllen werden zusätzlich auch alle Tabellen mit tb.BeginLoadData() für die folgenden Massen-Operationen optimiert (#72, sowoh An- als auch Aus-schaltung). Ausserdem muss bei Rücknahme der Optimierung auch .EnforceConstraints reaktiviert werden (#74) - selbiges wird nämlich implizit durch .BeginLoadData() ausgesetzt. Wie gesagt: dieses nur beim Laden - beim Abspeichern wird ja vorher returnt (#70).
      Hingegen die BindingSources werden für beide Arten Db-Access temporär deaktiviert - bei vielen Datensätzen ergibt sich hierraus eine enorme Performance-Verbesserung. Bei Reaktivierung müssen aber dann alle Bindings neu ausgelesen werden - erzwungen durch #67.

      Faszit
      Ja, so geht das, wenn man von einer DB ausgehend sich mittels Connector ein typisiertes Dataset generiert.
      SqlServer ist sogar etwas einfacher, denn ein SqlClient.SqlDataAdapter kann bei einem Insert den neuen Primkey selbständig abrufen und einpflegen - das Verarbeiten des RowUpdated-Events entfällt daher.
      Für MySql gibts auch Connectoren, nur muss man die extra downloaden und installieren, das sind - festhalten! - über 1GB, die sich da auf Platte breitmachen!
      SqLite ähnlich (nur nicht >1GB), allerdings ich kenne niemanden, der den entsprechenden Connector erfolgreich installiert hat. Diese Connectoren sind ja auch beim nächsten Windows, nächsten Framework alle gleich wieder obsolet.
      Und auch für SqlCe solls wohl Connectoren geben.
      Übrigens bei Server-Datenbanken, also wo man keine Db-Datei ins Projekt includet, da geht man übers "Daten-Fenster - DatenQuelle-hinzufügen". Oder man fügt dem Server-Explorer eine Connection hinzu, und kann dann aus dem ServerExplorer Tabellen auf ein leeres typDataset ziehen.

      Zum Connector-Problem kommt noch hinzu, dass die meisten DB-Systeme gar keine vernünftigen Tools bereitstellen, mit denen man überhaupt ein Datenmodell entwerfen kann.
      Access ist in dieser Hinsicht vorbildlich - in der Beziehungs-Ansicht kann man die Relationen konfigurieren, ebensogut, wie man es im DatasetOnly-Ansatz im typisierten Dataset direkt macht.
      SqlServer geeeht so, nur muss man dafür die monströse SqlServer-Management-Konsole installieren. In dieser Konsole kann man sog. "Db-Diagramme" generieren, wo man ebenfalls Relationen ziehen kann.
      Tja, die anderen DB-Systeme (MySql, SqLite, SqlCe, weitere?) haben sowas nicht, sondern die haben ganz murkelige Tools (ähnlich Php-Admin), wo man Beziehungen tw. nur konfigurieren kann, indem man natives Sql im jeweiligen Dialekt abfährt, und HoffeDassKlappt.
      In meinen Augen ein Armuts-Zeugnis, denn immerhin sinds relationale Datenbanken - aber grad die Unterstützung des wesentlichen, der Relationen nämlich, kann man nur als jämmerlich bezeichnen.

      Egal - ich rate ja eh von allem ab, was ich in diesem Tutorial erklärt hab :P

      Meine Empfehlung ist: Immer datasetOnly entwickeln, und nicht mit der DB im Projekt anfangen, sondern - wenn überhaupt - erst gegen Ende, mit der DB-Anbindung ein Projekt abschließen.
      Vorführen und begründen tu ich das hier: Daten laden und speichern, und das hiesige Tut ist eigentlich nur für die Unbelehrbaren, die unbedingt drauf bestehen, die DB als Ausgangspunkt zu nehmen statt als Endpunkt, und die folglich während der ganzen Entwicklung ihre Db mit durchzuschleppen haben, mit allen Konsequenzen (siehe "Vorbemerkung" im og. Link).
      Wie dem auch sei - das allerwichtigste ist, dass man mit typisierten Datenklassen arbeitet, also entweder EntityFramework oder typDataset.
      Und dafür ist dieses Tut: dass auch den Database-first-Anhängern aufgezeigt ist, wie sie die unerhörten Vorzüge typisierter Datenbank-Programmierung für sich verfügbar bekommen.
      Einen Einblick, was "Vorzüge typisierter Datenbank-Programmierung" meint: vier Views-Videos (wers nicht kennt: unbedingt paar der Vids angucken, sonst versteht man kaum, worums hier eigentlich geht)

      Und statt des Gemurkels mit genannten DBMS-Tools empfehle ich (in aller Bescheidenheit freilich) mein Tool: Database-Generator.
      Das dreht den Connector-Spieß nämlich um: Statt aus einer Db ein typDataset zu generieren, generiert DbGenerator aus einem typDataset die DB :D . So kann man einfach mittm Dataset-Designer das Datenmodell relational aufbauen, ohne mit Php-Admin und Konsorten rum-murkeln zu müssen.
      Und Connectoren, die für jede DataTable eine eigene TableAdapter-Klasse generieren, erübrigen sich, wenn man meine DbExtensions einbindet (jetzt noch bescheidener!! :saint: ) - die können DataAdapter nämlich dynamrisch konfigurieren, und noch einiges mehr.
      Beide unterstüten alles, was an DB ich auf meim System ans Laufen brachtete: Access, ODBC, SqlServer, SqlCe, MySql - (SqLite bugt leider bei mir rum, obwohl das eiglich von den datei-basierten Dbs die beste ist).
      Wie gesagt: Ich empfehle, erst datasetOnly zu entwickeln, und evtl. am Ende die Db dazu generieren und hinterlegen - wie gezeigt in: Migration DatasetOnly->Datenbank.
      Also ich sag nicht, dasses ein Spaziergang sei - ein ziemliches Spektrum an Grundlagen ist unverzichtbar. Aber es ist glaub der leichteste Weg, und die Grundlagen wird man sich auf allen anderen Wegen ebenso erarbeiten müssen, sonst kann eiglich nur Chaos entstehen.

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „ErfinderDesRades“ ()

      DataDirectory verstehen und codeseitig festlegen

      In post#1 tritt ja ein Problem mit dem generierten Connectionstring auf, der korrekt so lauten muss:
      Provider=Microsoft.ACE.OLEDB.12.0;Data Source=|DataDirectory|\Phonebook.mdb;Persist Security Info=True
      Darin die Angabe |DataDirectory| ist ein Platzhalter, für den der DbProvider einen gültigen Pfad einsetzt, den er von der Programmumgebung erhält. Standardmäßig - und so ists in Post#1 auch konfiguriert - ist das das Ausführungsverzeichnis, also in der Debug-Version ists der \bin\Debug\ - Ordner.
      Hingegen wenn man noch vorm ersten Db-zugriff (etwa im Form_Load()) - folgendes ausführt:

      VB.NET-Quellcode

      1. #If DEBUG Then
      2. AppDomain.CurrentDomain.SetData("DataDirectory", System.IO.Path.GetFullPath(@"..\..\Data")) ' Data-Ordner relativ zum Projektverzeichnis
      3. #Else
      4. 'on Release use Standard-DataDirectory (where-ever that may be), to avoid accessing forbidden Directories
      5. #End If
      Dann legt das das DataDirectory auf einen \Data\ - Ordner fest innerhalb des Projektverzeichnisses.
      Also bei mir würde das etwa festlegen, dass die DataAdapter die Db zugreifen unter:
      C:\Programming\VS10\FormsVb\WithDataBase\DbDemo\Data\MostSimple.mdb
      statt wie zuvor in:
      C:\Programming\VS10\FormsVb\WithDataBase\DbDemo\bin\Debug\MostSimple.mdb.
      (Natürlich muss man auch Sorge tragen, dass an diesem Pfad die mdb dann auch vorhanden ist)

      Übrigens die bedingte Compiler-Direktive #If DEBUG then führt den Code nur im Debug-Modus aus, denn bei Auslieferung eines Programms sollte man ein anderes DataDirectory konfigurieren (wie und welches weiß ich garnet, hängt auch von den Umständen ab).

      Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „ErfinderDesRades“ ()