none
Mehrbenutzerfähigkeit bei XML-serialisierten Daten? RRS feed

  • Frage

  • Hallo,

    hätte gern euren Rat.

    Es ist eine private UWP Anwendung mit einer Vielzahl von Anwendungsobjektklassen. Diese Klassen sind alle von einer gemeinsamen Basisklasse abgeleitet. In dieser ist z.B. der eindeutige ID (ein GUID mit Datum und Zeit der Anlage) gespeichert.

    Die Objekte einer Klasse sind jeweils in einer Sorted List gespeichert wobei der ID der Key ist. Diese Sorted List wird zum Speichern in ein Array umgewandelt und mit XML serialisiert.

    Beim Start der Anwendung werden alle diese Dateien gelesen und zum Füllen der Objektliste einer Klasse verwendet. Bisher wird wenn ein Objekt bearbeitet oder gelöscht wird oder hinzukommt jeweils immer die ganze Liste aller Objekte dieser Klasse gespeichert.
    Die Verbindungen zwischen den Objekten sind in Assoziationsklassen gespeichert. Klassen-ID-Objekt-von, ID-Objekt-von, Klassen-ID-Objekt-zu, ID-Objekt-zu. Auch diese Verbindungen sind in Sorted Lists gespeichert und werden mit XML serialisiert. Jede Anwendungsklasse hat eine Assoziationstabelle in der die Verbindungen ihrer Objekte zu anderen Objekten gespeichert sind. Beim Hinzufügen oder Löschen oder Verschieben eines Objekts wird die gesamte Assoziationstabelle der Klasse serialisiert und gespeichert.
    Auch wenn es sich aufwendig anhört so funktioniert es. Die Datenbestände sind bis auf einen mit 15 000 Datensätze relativ klein bisher.

    Kann so etwas aber mehrbenutzerfähig gemacht werden?

    Ich hatte vor dem UWP für eine Anwendung den SQL-Server verwendet und bin auf die Serialisierung zum Speichern der Objektlisten gekommen weil unter UWP die Verwendung von SQL-Server nicht mehr ohne weiteres möglich ist. Jedenfalls erschien es mir so.
    Als Ansatz für die Umsetzung des Mehrbenutzerbetriebs würde ich 2 Möglichkeiten sehen.

    1. Speichern des Zeitpunkts der letzten Änderung eines Objekts und Exception wenn versucht wird ein Objekt welches vorher
    gelesen wurde zurückzuschreiben. Was ist dann aber wenn ein Benutzer ein Objekt löscht welches ein anderer gerade bearbeitet?

    2. Öffnen eines Objekts explizit zum Bearbeiten. Nur ein Benutzer kann zu einer Zeit ein Objekt bearbeiten und dieses ist für diese Zeit für andere Benutzer für das Bearbeiten gesperrt.
    Das wäre der Ansatz den ich nehmen würde.

    In SQL wäre das Sperren des Satzes ein Select ob der Satz gesperrt ist. Und anschließend ein SQL-Update zum Schreiben des Sperr-flags und des Benutzernamens.
    Bisher habe ich bei den serialisierten Daten den Zugriff auf ein einzelnes Objekt nicht realisiert. Ich denke dann müsste die Datei gelesen werden, das serialisierte Objekt daraus entfernt, neu serialisiert und am Ende der Datei angefügt werden.
    Ich hoffe ihr habt verstanden was ich meine. Was würdet ihr raten? Umstellen auf WPF und verwenden des SQL-Server?
    Oder den Zugriff auf ein einzelnes Objekt in der serialisierten Datei programmieren?

    Danke, 
    Markus

    Samstag, 25. Januar 2020 13:26

Antworten

  • Hallo Markus,

    kein Programm sollte direkten zugriff auf die Datenbanken erhalten, sondern immer nur eins das dann wiederum APIs zu den Datensätzen bereitstellt. Meist nimmt man für sowas eine WebApi die z.B. im intranet für alle Clients erreichbar ist.

    Ich löse solche Szenarien meist mit dieser Projektstruktur.

    Projekt

    - NET Standard Shared Projekt für alle Models

    - ASP.NET Core WebApi mit Verweis auf Shared

    - UWP/Xamarin Projekt mit Verweis auf Shared

    ASP.NET Core und das Clientprojekt nutzen alle das Entity Framework mit den Models aus Shared und erstellen ihre eigenen Datenbanken. Wichtig ist das jeder Datensatz seine eigene lokale und globale Id hat. Für die globale Id nutze ich wie Du einen Guid. Unter UWP nutze ich als DB SQLite und auf dem Server entweder auch SQLite, MS SQL oder z.B. MariaDB.

    Jetzt braucht man nur noch eine Synchronisation zwischen Client und Server. Dazu nutze ich meist eine DB die eine Tabelle hat. Das Model sieht so aus

    public class Tracking
    {
        public int TrackingId { get; set; }
        public string GlobalId { get; set; } = Guid.NewGuid().ToString();
        public TrackingType TrackingType { get; set; }
        public string Table { get; set; }
        public string NewStateJson { get; set; }
        public string UserName { get; set; }
        public long DateTick { get; set; }
    }
    
    public enum TrackingType
    {
        Add,
        Update,
        Remove
    }

    Dabei wird der Datensatz als JSON in die Tabelle gelegt. Natürlich wäre auch XML möglich nur hat XML einen höheren overhead. Cool daran ist auch das man so eine Versionshistory aufbaut und jederzeit einen zustand wiederherstellen kann. 

    Der Client hat auch seine eigene Tracking DB, speichert in dieser nur seine eigenen Änderungen. Die Synchronisation sieht bei mir meist so aus. 

    • Client ruft Anhang der letzten TrackingId alle neunen Tracks vom Server ab
    • Zudem werden alle Tracks aus der lokalen TrackingDb gelesen und mit der List vom Server verglichen
    • Sollte in beiden Listen der gleiche Datensatz vorhanden sein, wird der ältere davon entfernt
    • Die Tracks von Server werden in die lokale DB geschrieben und die lokalen Track an den Server gesendet
    • Erst nach erfolgreicher Übergabe an den Server werden alle lokalen Tracks aus der Liste aus der DB entfernt
    • Die letzte TrackingId von Server wird auf dem Client gespeichert und ein neuer Zyklus kann beginnen

    Früher oder später muss man natürlich die globale TrackingDB komprimieren


    Gruß Thomas
    13 Millionen Schweine landen jährlich im Müll
    Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings



    Samstag, 25. Januar 2020 18:26
  • Hallo Markus,

    wenn neue Datensätze immer die alten überschreiben und der sync von Server vor dem sync zum Server ist, dann kann es keinen älteren Datensatz geben. Denn dieser wurde schon überschrieben und aus der liste zum Server entfernt.


    Gruß Thomas
    13 Millionen Schweine landen jährlich im Müll
    Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings

    • Als Antwort markiert Markus222 Montag, 27. Januar 2020 14:26
    Montag, 27. Januar 2020 12:25
  • Dann wäre der Datensatz von A der aktuellste und DataChangeProg müsste alle diesen neuen Datensatz melden. Es spielt für mich keine rolle das der neue Datensatz von A nicht auf den Datensatz von B aufbaut.

    Der Datensatz von B wäre hier auch nicht verloren sondern würde in der History landen. Je nach Komplexität der Datensätze und der Anwendung wäre ein Mergen dieser beiden existierend Datensätze eh nur durch einen User möglich. In so einem fall könntest Du ein Problem melden.

    Meiner Erfahrung nach kommt dieser zustand nicht oft vor. Bei der Quellcodeverwaltung Git müsse in so einem fall auch der User entscheiden was gemacht werden soll.

    Deine events ReadAllowed und ReadActive verkomplizieren das ganze und sind nur dann nötig wenn nur ein User gleichzeitig auf einen Datensatz zugreifen darf (Dateien Lock). Ich persönlich würde mir ein System überlegen in dem dieses nicht nötig ist (Warteschlange). Dein DataChangeProg sollte das einzige Programm sein das Daten lesen und schreiben darf. Dann könnte man auch zahlreiche events einbauen und mögliche zukünftige Probleme schnell beheben. 

    Man könnte natürlich darüber nachdenken die Dateien solange sie von einem User bearbeiten werden zu locken. Was ist aber wenn das Programm abstürzt? Dann müsste man sich überlegen nach welcher Zeit das locken wieder aufgehoben werden kann. Was ist aber wenn der User über diese Zeit eine Datei bearbeitet? Man könnte auch festlegen das das Programm des User alle x Sekunden einen Ping schicken muss um zu signalisieren das er noch online ist. Was ist aber wenn der Ping wegen einem kurzem Netzwerkausfall nicht durchkommt und damit der User den geändert Datensatz für dem er 30min gebraucht hat nicht mehr wegschicken kann.

    Eine wirkliche 100% Lösung gibt es nicht. Deswegen habe ich mich für das Verfahren von oben entschieden. 


    Gruß Thomas
    13 Millionen Schweine landen jährlich im Müll
    Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings


    Montag, 27. Januar 2020 13:30

Alle Antworten

  • Hallo Markus,

    kein Programm sollte direkten zugriff auf die Datenbanken erhalten, sondern immer nur eins das dann wiederum APIs zu den Datensätzen bereitstellt. Meist nimmt man für sowas eine WebApi die z.B. im intranet für alle Clients erreichbar ist.

    Ich löse solche Szenarien meist mit dieser Projektstruktur.

    Projekt

    - NET Standard Shared Projekt für alle Models

    - ASP.NET Core WebApi mit Verweis auf Shared

    - UWP/Xamarin Projekt mit Verweis auf Shared

    ASP.NET Core und das Clientprojekt nutzen alle das Entity Framework mit den Models aus Shared und erstellen ihre eigenen Datenbanken. Wichtig ist das jeder Datensatz seine eigene lokale und globale Id hat. Für die globale Id nutze ich wie Du einen Guid. Unter UWP nutze ich als DB SQLite und auf dem Server entweder auch SQLite, MS SQL oder z.B. MariaDB.

    Jetzt braucht man nur noch eine Synchronisation zwischen Client und Server. Dazu nutze ich meist eine DB die eine Tabelle hat. Das Model sieht so aus

    public class Tracking
    {
        public int TrackingId { get; set; }
        public string GlobalId { get; set; } = Guid.NewGuid().ToString();
        public TrackingType TrackingType { get; set; }
        public string Table { get; set; }
        public string NewStateJson { get; set; }
        public string UserName { get; set; }
        public long DateTick { get; set; }
    }
    
    public enum TrackingType
    {
        Add,
        Update,
        Remove
    }

    Dabei wird der Datensatz als JSON in die Tabelle gelegt. Natürlich wäre auch XML möglich nur hat XML einen höheren overhead. Cool daran ist auch das man so eine Versionshistory aufbaut und jederzeit einen zustand wiederherstellen kann. 

    Der Client hat auch seine eigene Tracking DB, speichert in dieser nur seine eigenen Änderungen. Die Synchronisation sieht bei mir meist so aus. 

    • Client ruft Anhang der letzten TrackingId alle neunen Tracks vom Server ab
    • Zudem werden alle Tracks aus der lokalen TrackingDb gelesen und mit der List vom Server verglichen
    • Sollte in beiden Listen der gleiche Datensatz vorhanden sein, wird der ältere davon entfernt
    • Die Tracks von Server werden in die lokale DB geschrieben und die lokalen Track an den Server gesendet
    • Erst nach erfolgreicher Übergabe an den Server werden alle lokalen Tracks aus der Liste aus der DB entfernt
    • Die letzte TrackingId von Server wird auf dem Client gespeichert und ein neuer Zyklus kann beginnen

    Früher oder später muss man natürlich die globale TrackingDB komprimieren


    Gruß Thomas
    13 Millionen Schweine landen jährlich im Müll
    Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings



    Samstag, 25. Januar 2020 18:26
  • Hallo Thomas,

    Würde es in etwa so probieren:
    Es gibt eine separate Anwendung DataChangeProg welche die Aufgabe hat Datenänderungen von verschiedenen Benutzern in den Datenbestand einzuarbeiten. Nur diese Anwendung ändert den Datenbestand.
    Es gibt ein separates Verzeichnis DataChanges. Dort gibt es drei Unterverzeichnisse.
    1. DataChangeIDs.
    In diesem Verzeichnis erzeugt DataChangeProg leere Dateien deren Dateinamen die nächsten freien Datenänderungs-IDs sind. Z.B. 000001, 000002 u.s.w.

    2. DataChangeData.
    In diesem Verzeichnis sind die vom Andwendungsprogramm von den verschiedenen Benutzern erzeugten Datenbestandsänderungen.
    Für eine DatenänderungsID können das mehrere Dateien sein.
    Die Dateinamen:
    DatenänderungsID - Folgenummer - KlassenID - ObjektID - Änderungsart - Benuterzname

    3. MsgToUser
    In diesem Verzeichnis sind Meldungen der DataChangeProg an einen oder alle die Benutzer falls es beim Schreiben der Daten Probleme gab.

    Das Anwendungsprogramm liest bei seinem Start nur wenn das DateChangeProg ReadAllowed signalisiert.
    Das DateChangeProg macht nur Änderungen solange kein Anwendungsprogramm ReadActive signalisert.

    Das Andwendungsprogramm macht eine Änderung der intern im Hauptspeicher befindlichen Daten.
    Die Betroffenen Objektinstanzen werden in jeweils einen separaten String serialisiert.
    Dieser String wird dann in eine DataChangeData Datei geschrieben.
    Das DataChangeProrg arbeitet diese Änderungen nacheinander in den Datenbestand auf der Festplatte ein. Letzte eingearbeitete DatenändungsID und Folgenummer werden Stand und Bestandteil des Datenbestands.
    Das Anwendungsprogramm welches auf verschiedenen Rechnern läuft liest am Anfang den Datenbestand und setzt ab der nächsten DatenänderungsID oder Folgenummer mit dem Update seiner Daten auf.
    Beim DataChange vergleicht DataChangeProg Datum und Uhrzeit der letzten Datenänderung des aktuell gespeicherten Datensatzes mit dem nun zu speichernden.

    Sollte der Zeitstempel das zu speichernden Datensatzes kleiner als der des gespeicherten Datensatzes sein handelt es sich um eine sich überschneidende Änderung und DataChangeProg weist diese zurück. Irgendwie muss nun das betreffende Anwendungsprogramm diese Änderung intern in seinen Daten ebenfalls zurück nehmen.

    Montag, 27. Januar 2020 11:52
  • Hallo Markus,

    wenn neue Datensätze immer die alten überschreiben und der sync von Server vor dem sync zum Server ist, dann kann es keinen älteren Datensatz geben. Denn dieser wurde schon überschrieben und aus der liste zum Server entfernt.


    Gruß Thomas
    13 Millionen Schweine landen jährlich im Müll
    Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings

    • Als Antwort markiert Markus222 Montag, 27. Januar 2020 14:26
    Montag, 27. Januar 2020 12:25
  • Meinte wenn Benutzer A einen Datensatz (= Objektinstanz) zum Editieren aufruft und danach Benutzer B den gleichen Datensatz zum Editieren aufruft und kurz vor Benutzer A speichert.

    Das Anwendungsprogramm von B würde einen DataChangeData Satz schreiben und kurz darauf würde das von A einen DataChangeData Satz mit einer größeren DatenänderungsID schreiben.

    Aber hätte der von A dann nicht noch die alten Daten vor der Änderung von B? (Und auch Datum/Uhrzeit/Benutzer der vorletzten Änderung.)

    Montag, 27. Januar 2020 12:41
  • Dann wäre der Datensatz von A der aktuellste und DataChangeProg müsste alle diesen neuen Datensatz melden. Es spielt für mich keine rolle das der neue Datensatz von A nicht auf den Datensatz von B aufbaut.

    Der Datensatz von B wäre hier auch nicht verloren sondern würde in der History landen. Je nach Komplexität der Datensätze und der Anwendung wäre ein Mergen dieser beiden existierend Datensätze eh nur durch einen User möglich. In so einem fall könntest Du ein Problem melden.

    Meiner Erfahrung nach kommt dieser zustand nicht oft vor. Bei der Quellcodeverwaltung Git müsse in so einem fall auch der User entscheiden was gemacht werden soll.

    Deine events ReadAllowed und ReadActive verkomplizieren das ganze und sind nur dann nötig wenn nur ein User gleichzeitig auf einen Datensatz zugreifen darf (Dateien Lock). Ich persönlich würde mir ein System überlegen in dem dieses nicht nötig ist (Warteschlange). Dein DataChangeProg sollte das einzige Programm sein das Daten lesen und schreiben darf. Dann könnte man auch zahlreiche events einbauen und mögliche zukünftige Probleme schnell beheben. 

    Man könnte natürlich darüber nachdenken die Dateien solange sie von einem User bearbeiten werden zu locken. Was ist aber wenn das Programm abstürzt? Dann müsste man sich überlegen nach welcher Zeit das locken wieder aufgehoben werden kann. Was ist aber wenn der User über diese Zeit eine Datei bearbeitet? Man könnte auch festlegen das das Programm des User alle x Sekunden einen Ping schicken muss um zu signalisieren das er noch online ist. Was ist aber wenn der Ping wegen einem kurzem Netzwerkausfall nicht durchkommt und damit der User den geändert Datensatz für dem er 30min gebraucht hat nicht mehr wegschicken kann.

    Eine wirkliche 100% Lösung gibt es nicht. Deswegen habe ich mich für das Verfahren von oben entschieden. 


    Gruß Thomas
    13 Millionen Schweine landen jährlich im Müll
    Dev Apps von mir: UWP Segoe MDL2 Assets, UI Strings


    Montag, 27. Januar 2020 13:30