In Frontends nicht noch einmal eine Datenbank bauen
(sqlsync.dev)- Komplexe Frontend-Apps beginnen mit einem Cache für API-Antworten und landen fast dabei, pro Projekt eine kleine Datenbank neu zu implementieren – inklusive manueller Indizes und Cache-Invalidierung
- In deklarativen Frameworks wie React werden lokale Zustände oder eine Redux-Schicht als Cache verwendet, damit nicht bei jedem Rendern APIs aufgerufen werden; diese Schicht übernimmt nach und nach die Rolle eines zentralen Speichers
- Eine ID-basierte Ablage und Strukturen für Abfragen nach Datum beschleunigen Zugriffe, aber die Konsistenz zwischen mehreren Strukturen wie
CACHEundENTRIES_BY_DATEwird zu einer wachsenden Last für Tests und Code-Reviews - Optimistische Änderungen aktualisieren die UI sofort vor der Serverantwort und erhöhen so die gefühlte Geschwindigkeit, bringen aber Konsistenzkosten mit sich wie doppelte Client-/Server-Logik, Nachverfolgung laufender Änderungen, Fehler-Rollbacks und Abgleich nach einem App-Neustart
- SQLSync will mit einer lokalen SQLite-basierten Datenbank und einer an Git Rebase angelehnten Synchronisierung persistenten Cache, Indizes, Constraints, optimistische Änderungen und reaktive Queries innerhalb des Frontend-Stacks bereitstellen
Wie Frontend-Caches zu Datenbanken anwachsen
- Datenverwaltung im Frontend kann mit einem einfachen Cache beginnen, der API-Antworten in lokalen Variablen speichert
- Deklarative Frameworks wie React rendern den Baum während der Benutzerinteraktion mehrfach neu
- Damit nicht bei jedem Rendern API-Anfragen gesendet werden, lassen sich Anfrageergebnisse oder Fehler mit
useStateunduseEffectim Komponentenstatus speichern - Das Beispiel ist der Klarheit halber vereinfacht; in der Praxis gibt es auch die Option, bewährte API-Bibliotheken zu verwenden
- Der Cache kann in eine höhere Ebene des UI-Baums oder ganz außerhalb der UI verschoben werden
- Redux ist eine React-State-Management-Bibliothek, die Zustand zusammenführt und atomare Änderungen über die Zeit koordiniert
- Das Redux-Ökosystem wurde um Werkzeuge und Muster zur Verwaltung des Cachings von API-Daten erweitert
- Diese Nutzungsweise zielt darauf ab, Cache-Logik zu zentralisieren, Aktualisierungen zu koordinieren und Cache-Ergebnisse zwischen Komponenten zu teilen
- Je größer die Caching-Schicht wird, desto mehr ähnelt sie einem zentralen Speichersystem, das Daten passend zur Rendering-Engine und zu Benutzeraktionen effizient verarbeitet
Manuelle Indizes und die Last der Konsistenz
- Auch im Frontend lassen sich vom Server empfangene Daten in Objekten mit ID als Schlüssel speichern, um schnelle Abfragen und Änderungen zu ermöglichen
- In Apps mit REST-API werden Daten häufig im Batch gelesen und benötigte Objekte anschließend zusätzlich angereichert
- Wenn Objekte nach ID gespeichert werden, lassen sich API-Ergebnisse leicht in den Cache zusammenführen
- Diese Struktur ist für Create, Read, Update und Delete nach ID optimiert
- Filter, die über mehrere Einträge laufen müssen, führen dazu, dass separate Indizes angelegt werden, um vollständige Prüfungen aller Einträge zu vermeiden
- Wenn man auf Basis von Jahr/Monat/Tag von
createdAteine StrukturENTRIES_BY_DATEanlegt, lassen sich Einträge eines bestimmten Datums schnell finden - Dafür muss jedoch die Konsistenz zwischen
CACHEundENTRIES_BY_DATElaufend erhalten bleiben - Für Datumsbereichsabfragen sind mehrere Zugriffe nötig; ein nach Datum sortiertes Array und komplexere Abfrage-/Update-Logik können die bessere Struktur sein
- Wenn man auf Basis von Jahr/Monat/Tag von
- Je mehr Indizes es gibt, desto mehr Logik für Erzeugung, Aktualisierung und Abfrage wird für jeden einzelnen Index benötigt
- Die Prüfung der Korrektheit wird zu einer größeren Last für Tests und Code-Reviews
- Wenn in einem Index eine Löschung oder Aktualisierung vergessen wird, können schwer auffindbare Bugs entstehen
- Am Ende kann mehr Zeit in die Infrastruktur zur Beherrschung dieser Komplexität fließen als in neue Anwendungsfunktionen
- Reale Datenbankindizes sind weit komplexer, als Daten nur in einer anderen Form abzulegen
- Dazu gehören Elemente wie Statistiksammlung, Datenversionierung, Transaktionskontrolle, Sperren und die Interaktion mit der Query-Optimierung
Konsistenzprobleme durch optimistische Änderungen
- Optimistische Änderungen simulieren die Wirkung einer bestimmten Aktion lokal schon vor der Serverantwort
- Die UI kann dadurch sofort reagieren, als gäbe es keine Netzwerklatenz
- Wenn der Server anders entscheidet als erwartet oder ein Fehler auftritt, muss die UI die Änderung möglicherweise zurückrollen und den Benutzer zur Korrektur des Problems auffordern
- Das ist ein starkes Werkzeug, wenn der Client Serverergebnisse gut vorhersagen kann, Fehler clientseitig behandelbar sind und die Logik eng synchronisiert ist
- Optimistische Updates laufen meist in vier Schritten ab
- Die UI löst einen Schreibvorgang aus
- In der Annahme, dass der Server zustimmt, wird die Änderung auf den lokalen Cache angewendet und die UI sofort neu gerendert
- Die Änderungsoperation wird asynchron an den Server gesendet
- Die Serverantwort wird in den lokalen Cache gemergt, überschreibt frühere optimistische Änderungen und rendert die UI bei Bedarf erneut
- Um die Konsistenz mit dem Server zu erhalten, entstehen mehrere Lasten
- Zur Vorhersage der Ergebnisse muss Logik auf Client und Server dupliziert werden
- Um asynchrone Fehler oder Serverabweichungen zu behandeln, muss jede laufende Änderung nachverfolgt werden
- Für eine bessere User Experience muss der optimistische Teil des Caches eventuell persistent gemacht werden, damit Änderungen nach einem App-Neustart abgeglichen werden können
- Dieser Prozess erhöht Entwicklungszeit und Kosten der Korrektheitsprüfung, sodass Datenverwaltung gegenüber Nutzerwert oder differenzierenden Features in den Vordergrund geraten kann
Die Komplexität rekursiver Cache-Invalidierung
- In datenreichen Apps taucht dieselbe Information an mehreren Stellen im Cache auf
- Der Beispiel-Cache speichert gemeinsam
projects,tasksundusers - Nach dem Abschließen einer Aufgabe können mehrere Bereiche betroffen sein, etwa Projektfortschritt, einem Benutzer zugewiesene Aufgaben oder neue Aufgabeninformationen
- Der Beispiel-Cache speichert gemeinsam
- Um den Cache nach Abschluss einer Aufgabe mit dem Server abzugleichen, können mehrere Roundtrips nötig sein
- Dem Server wird der Abschluss der Aufgabe gemeldet
- Da sich der Projektfortschritt geändert hat, wird das Projekt aktualisiert
- Es wird geprüft, ob neue Aufgaben zugewiesen wurden
- Falls es neue Zuweisungen gibt, werden diese Aufgaben abgefragt
- Komplexere APIs können die Zahl der Roundtrips senken, aber am Ende bleiben API oder Client-Logik an das zugrunde liegende Datenmodell gekoppelt
- GraphQL ist ein Ansatz für dieses Problem, aber keine vollständige Lösung
- Eine Struktur, in der die UI bei jeder Änderung wissen muss, welche Teile des Caches betroffen sind, wird mit wachsendem Umfang fragil
- Datenbeziehungen und Aggregate können mehrere Teile des lokalen Caches beeinflussen
- Wenn das Engineering-Team wächst, kann das Problem Teamgrenzen überschreiten und sich anfühlen wie veränderliche globale Variablen in großen Softwareprojekten
- In Kombination mit optimistischen Änderungen repliziert der Client noch mehr Backend-Logik, um Serveränderungen vorherzusagen
- Im Beispiel könnte versucht werden, task 1 aus user 1 zu entfernen und den Fortschritt mithilfe der Gesamtzahl der Aufgaben als neues Verhältnis zu berechnen
- Je stärker verschachtelte Änderungen lokal vorhergesagt werden sollen, desto mehr dupliziert der Client den Backend-Stack
Der Frontend-Datenbank-Stack, den SQLSync vorschlägt
- SQLSync ist ein für Frontends optimierter Datenbank-Stack, der auf SQLite aufbaut
- Die Synchronisierungs-Engine basiert auf Ideen aus Git und verteilten Systemen
- Er wurde so entworfen, dass er sich nahtlos in populäre Frontend-Frameworks wie React, Vue, Next.js integrieren lässt
- Ziel ist es, schwierige Probleme der Datenverwaltung zu übernehmen, damit Entwickler sich auf die einzigartigen Funktionen ihrer Anwendung konzentrieren können
- Die Beispiel-Todo app implementiert die komplette Datenebene mit 60 Zeilen Rust und einigen über Komponenten verteilten SQL-Queries
- SQLSync bietet persistenten Cache, Indizes, Constraints, Trigger und Query-Optimierung von SQLite, optimistische Änderungen, intelligente Cache-Invalidierung und reaktive Queries
- Lokale Daten werden in einer oder mehreren SQLite-Datenbanken gespeichert
- Indizes lassen sich leicht erstellen und bleiben automatisch mit den Daten synchronisiert
- Die Datenbank kann wie im Backend Indizes automatisch nutzen, um Queries zu beschleunigen
- SQL kann komplexe Datenabfragen ausdrücken, und Funktionen wie triggers, foreign keys, constraints, full-text search sind ebenfalls verfügbar
- Optimistische Änderungen werden über einen reducer verarbeitet
- Die Struktur ähnelt den Kernkonzepten von Redux
- Der reducer kann in jeder Sprache geschrieben werden, die sich nach WebAssembly kompilieren lässt
- SQLSync führt Änderungen auf dem Client optimistisch aus und auf dem Server in einer global konsistenten Reihenfolge
- Anschließend synchronisiert sich der Client mit dem Server über einen Vorgang, der Git Rebase ähnelt
- Diese Architektur hat den Vorteil, dass sie die Notwendigkeit rekursiver Cache-Invalidierung beseitigt
- Die gesamte Logik für Datenänderungen wird in einem reducer geschrieben, der sich leicht zwischen Client und Server teilen lässt
- Alle Datenänderungen, die während einer Änderung entstehen, werden automatisch sichtbar
- Da die Synchronisierung wie Git Rebase funktioniert, ist garantiert, dass der Client zum selben konsistenten Ergebnis gelangt, selbst wenn der Server andere Änderungen als der Client vorgenommen hat
Verwandte Arbeiten
- Riffles „Building data-centric apps with a reactive relational database“ behandelt die Idee, den gesamten Anwendungszustand einschließlich UI-State in einer einzigen reaktiven Datenbank zu speichern
- Reaktive Queries bieten ein sauberes Denkmodell und passen gut zu deklarativen Systemen wie React
- Probleme der Client-App-Entwicklung werden mit Ideen aus der Datenbank-Community angegangen
- Es werden die Vorteile behandelt, Zustand mit einem relationalen Datenmodell und echten Indizes zu modellieren
- Stepan von Instant.db hat zwei Texte über Datenbanken im Browser geschrieben
- Database in the Browser, a Spec
- A Graph-Based Firebase
- Beide Texte behandeln ähnliche Probleme mit stärkerem Fokus auf die Beziehung zwischen Frontend- und Backend-Stack und erklären die Motivation für Instant.db als graphbasierten Nachfolger von Firebase
- Matt Wonlaws CR-SQLite ist eine SQLite-Erweiterung
- Sie verwendet Conflict-free Replicated Data Types (CRDTs) und ein kausal geordnetes Event-Log, um Daten konsistent zusammenzuführen
- Damit können Peer-to-Peer-Apps ohne zentralen Koordinator Daten in SQLite speichern und kollaborativ nutzen
- Zugleich ist es ein Beispiel dafür, SQLite im Browser auszuführen
- Matt Wonlaw untersucht auch verwandte Ideen
- incremental computation
- Verbesserung der SQL-Benutzbarkeit über typed-sql
1 Kommentare
Meinungen auf Hacker News
Ich kenne dieses Projekt gut, und der Entwickler ist ein Freund von mir, also werde ich versuchen, ihn hierherzubringen, damit er Fragen beantworten kann.
Er ist ein erfahrener Datenbankarchitekt. Mit SQLsync hat er es Frontend-Entwicklern ermöglicht, eine entfernte Datenbank abzufragen und zu aktualisieren, als befände sie sich vollständig im Browser. Tatsächlich ist es fast so: Dank WASM kann man eine komplette SQLite-Datenbank in den Browser schicken. Der Kern liegt in einem cleveren, aber einfachen reaktiven Algorithmus, der die Synchronisierung über mehrere Clients hinweg übernimmt.
Wenn man einen großen Teil der Entwicklungsarbeit als Datensynchronisierung betrachtet, lassen sich auch React und REST-APIs als eine Art Synchronisationsverfahren verstehen, und dieser Ansatz eröffnet neue Möglichkeiten. Statt erneut seltsame maßgeschneiderte Datenbanken aus Objektbäumen zu bauen, die von einer API geholt und gecacht wurden, kann man mit der Stärke einer relationalen Datenbank direkt lokal aktualisieren und abfragen.
Allerdings ist die Einführung in klassischen Web-Unternehmen wegen der spezialisierten Backend-/Frontend-Teams schwierig. Man ersetzt gewissermaßen Datenbank-, Backend-, Transport- und Authentifizierungsschicht durch ein einziges Blocksystem, und die meisten Systemarchitekten kommen aus dem Backend und kennen dieses Problem nicht gut. Weil es tief in beide Seiten eingreift, passt es schlecht zu bestehenden Systemen und eignet sich am Ende eher für Neuentwicklungen. Das Backend ist weder ein AWS- noch ein Azure-Service und auch nicht Lambda-freundlich; die Architektentypen, denen ich begegne, würden deshalb meist die Finger davon lassen.
In gewisser Weise gibt es diesen Ansatz bereits mit der älteren Technologie CouchDB+PouchDB. Für manche Anwendungsfälle passt das ziemlich gut, aber das Abfragesystem ist nicht ideal, und die Art, wie Authentifizierung und Datenumfang festgelegt werden, ist den meisten fremd. Am einfachsten ist es, wenn Daten vollständig einem einzelnen Nutzer gehören und man das Datenbank-pro-Nutzer-Modell direkt verwendet. Wenn man Daten mit CRDTs stark partitioniert, reduziert das auch viele Konfliktprobleme.
Es gibt jedoch Skalierungsprobleme. Wenn bei CouchDB 10.000 bis 100.000 Nutzer verbunden sind, ist der CPU-Bedarf sehr hoch; die Technologie ist zudem alt, wird aber weiterhin gepflegt. Aus Sicht des Systemdesigns steigt die Komplexität sprunghaft, sobald man beginnt, Daten zwischen Nutzern zu teilen. Dann verschiebt man die Komplexität eher, als sie zu lösen, wodurch die Eignung abnimmt.
Dieser Ansatz scheint auf dasselbe Ziel abzuzielen, dürfte aber wahrscheinlich ähnliche Skalierungsprobleme bekommen. Ich bin gespannt, wie er sich weiterentwickelt; es wirkt wie ein erster Schritt.
Ich erinnere mich, dass Chrome früher versucht hat, buchstäblich eine SQL-Datenbank in den Browser zu integrieren; das hat nicht wirklich funktioniert, und localStorage wurde zum Mainstream. Ich will den Nutzen nicht kleinreden, normalerweise wählt man aber eher das, was der Browser bereitstellt. Auf WASM und darauf, was mit zunehmender Reife oder mehr Funktionen in den Browser gebracht werden kann, setze ich große Hoffnungen.
Ein früherer Arbeitgeber nutzte Projektmanagementsoftware mit einem Checkout-/Check-in-Mechanismus für Änderungen. Wenn man ein Projekt auscheckte, lud man eine Kopie zur lokalen Bearbeitung herunter; beim Einchecken lud man sie wieder auf den Server hoch. Während des Checkouts war das Projekt gesperrt. Im Zeitalter von Apps mit Live-Updates empfanden das alle als veraltet.
Nachdem ich nun aber zehn Jahre lang SPA-Webapps gebaut habe, fühlt sich diese Art der Datensynchronisierung an, als sei sie ihrer Zeit voraus gewesen.
Am Ende läuft es darauf hinaus, ob man ein Verfahren implementieren kann, das Inkonsistenzen zwischen mehreren gleichzeitigen Updates konsistent auflöst. Manchmal ist das möglich, manchmal nicht; es hängt stärker von den Geschäftsregeln ab als von der technischen Fähigkeit.
Wenn sich aufgrund der Geschäftsregeln kein Auflösungsmechanismus implementieren lässt, braucht man eine Sperre, damit immer nur ein Update auf einmal möglich ist, selbst wenn man technisch in der Lage wäre, gleichzeitige Updates zu unterstützen.
Es ist aber schwer zu vermitteln, dass genau das tatsächlich gewünscht ist. Man verfällt leicht der großen Illusion, dass alles jederzeit verfügbar sein müsse. In der Realität nimmt aber meist eine Person nach der anderen Änderungen vor, und wenn zwei oder mehr Personen daran arbeiten müssen, müssen sie ohnehin miteinander sprechen oder sich abstimmen.
Selbst bei vollständig verteilter Entwicklung wie mit Git lassen sich Konflikte nicht automatisch auf magische Weise lösen. Um die richtige Änderung auszuwählen, muss man weiterhin mit anderen kommunizieren und den Kontext verstehen.
Manche Dinge brauchen bewährte Lösungen.
Ich erinnere mich, dass sich einer meiner Kollegen ärgerte, als die Firma früher von RCS auf CVS umstieg, weil CVS keinen gesperrten Checkout unterstützte.
https://en.wikipedia.org/wiki/Concurrent_Versions_System
Ich denke, eine Sperrstrategie mit einem einzigen Besitzer ließe sich mit SQLSync ebenfalls simulieren. Je nach App ist das aber vielleicht gar nicht nötig. Wenn das Ziel Offline-Arbeiten und anschließendes Mergen ist, sobald man bereit ist, bietet SQLSync dieses Muster von Haus aus. Wenn das Ziel ist, dass nur ein Client Änderungen vornehmen kann, braucht man ein zentrales Sperrmuster; auch das ließe sich möglicherweise über SQLSync koordinieren.
Hier sind das Prinzip „Was gemessen wird, wird gemanagt“ und der Sunk-Cost-Fehlschluss miteinander verwoben.
Das eigentliche Problem von Datenbanken ist Komplexität. Einzelne Features sind für sich genommen meist sicher, aber sobald Zuverlässigkeit, Caching und Indizes ineinandergreifen, explodiert die Komplexität, und in der Regel ergibt es keinen Sinn, eine domänenspezifische DB zu implementieren.
Wenn ein Unternehmen jedoch erkennt, dass es bereits in die Implementierung dieser drei Features investiert und viele Ressourcen hineingesteckt hat, ist es politisch schwierig, zu empfehlen, das wieder herauszureißen; außerdem sind die realen Kosten hoch, die technische Schuld auf einmal zu beseitigen.
Das eigentliche Problem sehe ich in der SQL-Syntax. Wäre die Nutzung einer normalen relationalen Datenbank so angenehm wie eine vertraute C-artige Syntax statt wie kaputtes Englisch, gäbe es einen stärkeren Anreiz, eine DB zu verwenden, statt selbst eine zu bauen. NoSQL-Datenbanken waren ein guter Schritt in diese Richtung, haben sich aber meist zu stark auf Big Data statt auf alltägliche Nützlichkeit konzentriert. Dinge wie Redis haben sich etabliert und sind in Ordnung.
SQL leicht ausführbar zu machen, ist ein vernünftiger Ansatz, aber gute Datenbanken, zum Beispiel mein bevorzugtes Postgres, haben SQL als Standardsprache, sodass es schwierig ist, Effizienz zu erreichen, ohne diese Sprache zu verwenden. Was wirklich nötig wäre, ist eine Datenbank wie PostgresPostSQL, die Postgres perfekt nachbildet, deren Standard-Parser aber eine Sprache mit guter Syntax unterstützt.
In der allgemeinen Programmierung werden Dutzende Sprachen genutzt und sie entwickeln sich ständig weiter. Selbst JavaScript, das vom Browser ausgeführt wird und schwer zu ändern ist, weil man den Browser der Nutzer nicht kontrollieren kann, entwickelt sich über Transpiler und WebAssembly weiter.
Bei Datenbanken gibt es jedoch praktisch nur SQL. Es gibt Alternativen, aber keine kommt SQL beim Nutzungsumfang nahe. Vielleicht ist SQL also gar nicht so schlecht.
Der Grund könnte sein, dass das relationale Modell wirklich gut ist. Versuche, davon abzuweichen, funktionieren wahrscheinlich nur in Nischen. Auch der deklarative Stil ist sehr gut, und davon abzuweichen dürfte kaum großen Erfolg bringen. Wenn am Ende nur ein SQL mit anderer Syntax entsteht, ist das für die meisten vermutlich keine so große Verbesserung, dass sie ihre Arbeitsweise ändern würden.
Eine Anwendung, die gegen diese API geschrieben ist, könnte eine SQL-DB implementieren. Man müsste SQL parsen und einen Query Planner implementieren, der Query Plans ausgibt, die zu dieser API passen.
Ich bin der Autor. Ich habe die meisten Fragen gerade so überflogen und werde regelmäßig weiter nachsehen, ob ich welche übersehen habe. Mich würde auch interessieren, ob jemand eine bessere Möglichkeit gebaut hat, HN-Diskussionen zu verfolgen.
Die bisherige Diskussion freut mich sehr. Der erste Beitrag konzentrierte sich weniger darauf, wie SQLSync konkret funktioniert, sondern auf die Motivation aus dem Frontend Engineering, die mich dazu gebracht hat, SQLSync zu bauen. Im nächsten Beitrag werde ich die Funktionsweise behandeln.
Man sollte Nutzern kein mentales Modell geben, mit dem sie die Realität massiv oder unsichtbar kaputtmachen können.
Ich befürchte, dass das Synchronisieren von Datenbanken anstelle eines Client-Server-Modells so ein Fall sein könnte. Der Synchronisationsmechanismus könnte einfach dahinschmelzen, oder es könnten tiefe Annahmen dahinterstehen, die nicht erfüllt sind.
Wenn man ein schnelles UI braucht, fühlt es sich sicherer an, eine Sammlung von CRDT-Primitiven zu bauen und zu nutzen; der Rest würde wohl bei Formular-Submits bleiben.
Die Zustandssynchronisation zwischen Client und Server ist ein verfluchtes Problem.
Wenn man bereit ist, ein wenig User Experience zu opfern und zu einem Modell zurückkehrt, das näher an PHP/Server-Side Rendering liegt, kann man dieses Problem komplett vermeiden. SPAs sind nett, aber Multipart-Formularübermittlung funktioniert immer noch. Mit sehr wenig JavaScript kann man die meisten verbleibenden Ecken und Kanten glätten.
In den aktuellen Webprodukten besteht der clientseitige Zustand nur aus Authentifizierungs-Claims eines Third-Party-IdP, einer First-Party-Session-ID in den Query-Parametern und dem aktuellen Dokument. Wo das Erste gespeichert wird, weiß ich ehrlich gesagt selbst nicht. Das ist ein Microsoft-Problem, nicht unseres. Der gesamte übrige Zustand liegt auf dem Server.
Wir behandeln den Client wie ein dummes Terminal, das den ganzen Tag nur Eingaben ausspuckt. Wir verwenden weder First-Party-Cookies noch Local Storage. Dieser Ansatz hat die Entwicklungserfahrung für iOS/Safari deutlich verbessert.
Daher möchte ich fragen, welche Erfahrung man eigentlich bieten will und warum sie es rechtfertigt, Client- und Serverzustand zu trennen.
Siehe: https://news.ycombinator.com/item?id=37584049
Auf SQLite basierendes Offline-/Local-first scheint gerade heiß zu sein. Das ist schon das dritte Mal, dass ich diese Woche darüber lese, und es sieht gut aus
Aber wie schneidet es im Vergleich zu ElectricSQLhttps://electric-sql.com/ und PowerSynchttps://powersync.com/ ab?
ElectricSQL und PowerSync befassen sich beide mit dem sehr schwierigen Problem der partiellen Replikation. Sie versuchen, eine allgemeine Lösung zu schaffen, bei der eine traditionelle zentrale Datenbank nur das, was der Client benötigt, bidirektional synchronisiert und gleichzeitig optimistische Änderungen sowie die dazugehörige Konsistenz- und Konfliktbehandlung unterstützt
Der Nachteil ist die Komplexität der Implementierung. Man muss genau nachverfolgen, welche Teilmenge der gesamten Datenbank jeder Client besitzt, damit Änderungen nur an diese Teilmenge gepusht werden können. Außerdem braucht man eine neue DSL, um anzugeben, welche Teilmenge des Datenbankzustands heruntergeladen werden soll, und auch diese muss neu gelernt und optimiert werden. Trotzdem ist es erfreulich, dass sie ein sehr schwieriges Problem lösen, und wenn SQLSync bereit ist, partielle Replikation zu unterstützen, werden die Best Practices bereits herausgearbeitet sein
SQLSync dagegen unterstützt derzeit nur Synchronisierung der gesamten DB. Alle Clients sehen eine konsistente Sicht auf die gesamte Datenbank. Man kann sich sofort fragen, ob das eine gute Idee ist, und für manche Apps passt es nicht. Wenn man aber an eine App für persönliche Finanzen denkt, sind die Kernziele Synchronisierung zwischen Geräten, Cloud-Backup und Offline-Fähigkeit; da kann es sogar genau der gewünschte Ansatz sein, die gesamte DB auf allen Geräten zu speichern. Auch ein dokumentorientiertes Datenmodell wie bei Airtable kann als Beispiel dienen. Wenn man jedes Airtable als eigene Datenbank betrachtet, kann der Client verwalten, um welche Tabellen er sich kümmert
Wenn man sich auf die Synchronisierung der gesamten DB konzentriert, wird die Sync-Engine deutlich einfacher als bei Lösungen, die partielle Replikation unterstützen. Einer der Vorteile ist, dass das Backend sehr leichtgewichtig ist. Die aktuelle Demo (https://sqlsync-todo.pages.dev) läuft vollständig in Cloudflare Durable Objects und verbraucht dabei nur sehr wenig Storage und CPU-Zeit
Damit SQLSync solche Anwendungsfälle ermöglichen kann, gibt es noch viel zu tun, und es ist weiterhin eher ein Prototyp, aber die ersten Tests waren sehr positiv
Bei großen Multi-Tenant-Apps, bei denen die einzelnen Datensätze relativ klein sind, hatte ich mehrfach den Gedanken: „Warum schicken wir nicht einfach die Datenbank an den Client?“ Es wirkte ausreichend wie ein verfluchtes Architekturpattern außerhalb aller Standards, dass ich es nicht weiterverfolgt habe. Es wäre schön zu erfahren, dass ich falschlag
Um es wirklich zu beweisen, ist noch viel zu tun, aber ich bin ziemlich gespannt darauf, weiter in diese Richtung zu gehen und zu sehen, wohin es führt
Das wirkt wie eines dieser Probleme, die komplett verschwinden, wenn man SPA aufgibt
Mit Lösungen aus der Hotwire- oder htmx-Familie werden Abfragen einfach zu Serverabfragen, und das Problem, solche Abfragen schnell zu machen, ist viel besser verstanden
Ich habe es zusammen mit ocaml + web components verwendet, und es war eine Produktivität-10/10-Erfahrung. Man braucht nur ein Build-Tool, das schneller kompiliert, als man blinzeln kann, und es ist wirklich produktiv, weil man keine JSON-Mapping-Verkabelung zwischen Frontend und Backend braucht
Persönlich bevorzuge ich InertiaJs https://inertiajs.com. Es ist eine Art Frontend-Router-System, das den Zustand auf „alte Weise“ mit dem Server synchronisiert
Das gilt besonders für Produkte, die auch in Regionen mit instabilem Internet funktionieren müssen
Ich schreibe gerade an einem sehr ähnlichen Text über „Full-Stack-Datenbanken“. Er behandelt das Muster, dass viele Apps die Logik von Backend und Datenbank im Frontend-Client-Code erneut nachbauen. Unsere empfohlene Lösung ist, eine Datenbank zu wählen, die sowohl auf dem Server als auch auf dem Client laufen kann, und beide miteinander zu synchronisieren.
Der Grund, warum wir in unserem Produkt nicht SQLite verwenden, ist ehrlich gesagt, dass SQL nicht das passende Werkzeug ist, um Anwendungsdaten abzufragen. Es passt nicht gut zu den Datenstrukturen, die man im Client-Code haben möchte, und fast alle SQL-Datenbanken bieten keine Möglichkeit, Änderungen an Abfragen zu abonnieren, ohne die Abfrage wiederholt zu pollen.
Wenn euch die Idee gefällt, eine vollständige Datenbank auf dem Client zu haben, und ihr eine tiefe Integration mit TypeScript/JavaScript wollt, schaut euch gern https://github.com/aspen-cloud/triplit an, das wir entwickeln.
https://github.com/cpursley/walex
Ich nutze das sehr simpel: Wenn sich Daten in den zugrunde liegenden Tabellen ändern, führe ich die Abfrage automatisch erneut aus. Das ist zwar nicht so effizient wie eine inkrementelle Aktualisierung der Ergebnisse, aber SQLite-Abfragen sind meist so schnell, dass ich das nicht als großes Problem sehe.