CouchDB
 sql >> Datenbank >  >> NoSQL >> CouchDB

Synchronisierung im CouchDB-Stil und Konfliktlösung auf Postgres mit Hasura

Wir haben über Offline-First mit Hasura und RxDB (im Wesentlichen Postgres und PouchDB darunter) gesprochen.

Dieser Beitrag taucht weiter in das Thema ein. Es ist eine Diskussion und Anleitung zur Implementierung von Konfliktlösungen im CouchDB-Stil mit Postgres (zentrale Backend-Datenbank) und PouchDB (Frontend-App-Benutzer). Datenbank).

Hier ist, worüber wir sprechen werden:

  • Was ist Konfliktlösung?
  • Benötigt meine App eine Konfliktlösung?
  • Konfliktlösung mit PouchDB erklärt
  • Einfache Replikation und Konfliktmanagement für Pouchdb (Frontend) und Postgres (Backend) mit RxDB und Hasura
    • Hasura einrichten
    • Clientseitige Einrichtung
    • Konfliktlösung implementieren
    • Ansichten verwenden
    • Postgres-Trigger verwenden
  • Benutzerdefinierte Konfliktlösungsstrategien mit Hasura
    • Benutzerdefinierte Konfliktlösung auf dem Server
    • Benutzerdefinierte Konfliktlösung auf dem Client
  • Schlussfolgerung

Was ist Konfliktlösung?

Nehmen wir als Beispiel ein Trello-Board. Angenommen, Sie haben den Verantwortlichen auf einer Trello-Karte offline geändert. In der Zwischenzeit bearbeitet Ihr Kollege die Beschreibung derselben Karte. Wenn Sie wieder online sind, möchten Sie beide Änderungen sehen. Angenommen, Sie haben beide gleichzeitig die Beschreibung geändert, was sollte in diesem Fall passieren? Eine Möglichkeit besteht darin, einfach den letzten Schreibvorgang zu übernehmen – das heißt, die frühere Änderung mit der neuen zu überschreiben. Eine andere besteht darin, den Benutzer zu benachrichtigen und ihn die Karte mit einem zusammengeführten Feld (wie git!) aktualisieren zu lassen.

Dieser Aspekt, mehrere gleichzeitige Änderungen vorzunehmen (die widersprüchlich sein können) und sie zu einer Änderung zusammenzuführen, wird als Konfliktlösung bezeichnet.

Welche Art von Apps können Sie erstellen, wenn Sie über gute Replikations- und Konfliktlösungsfunktionen verfügen?

Replikations- und Konfliktlösungsinfrastruktur ist mühsam in das Front-End und Back-End einer Anwendung zu integrieren. Aber sobald es eingerichtet ist, werden einige wichtige Anwendungsfälle realisierbar! Tatsächlich ist die Replikation (und damit die Konfliktlösung) für bestimmte Arten von Anwendungen entscheidend für die Funktionalität der App!

  1. Echtzeit:Änderungen, die von den Benutzern auf verschiedenen Geräten vorgenommen werden, werden miteinander synchronisiert
  2. Kollaborativ:Verschiedene Benutzer arbeiten gleichzeitig an denselben Daten
  3. Offline-first:Derselbe Benutzer kann mit seinen Daten arbeiten, auch wenn die App nicht mit der zentralen Datenbank verbunden ist

Beispiele:Trello, E-Mail-Clients wie Gmail, Superhuman, Google Docs, Facebook, Twitter usw.

Hasura macht es super einfach, leistungsstarke, sichere Echtzeitfunktionen zu Ihrer bestehenden Postgres-basierten Anwendung hinzuzufügen. Zur Unterstützung dieser Anwendungsfälle muss keine zusätzliche Backend-Infrastruktur bereitgestellt werden! In den nächsten Abschnitten erfahren Sie, wie Sie PouchDB/RxDB im Frontend verwenden und mit Hasura koppeln können, um leistungsstarke Apps mit großartiger Benutzererfahrung zu erstellen.

Konfliktlösung mit PouchDB erklärt

Versionsverwaltung mit PouchDB

PouchDB - das RxDB darunter verwendet - verfügt über einen leistungsstarken Versionierungs- und Konfliktverwaltungsmechanismus. Jedem Dokument in PouchDB ist ein Versionsfeld zugeordnet. Versionsfelder haben das Format <depth>-<object-hash> zum Beispiel 2-c1592ce7b31cc26e91d2f2029c57e621 . Tiefe gibt hier die Tiefe im Revisionsbaum an. Objekt-Hash ist eine zufällig generierte Zeichenfolge.

Ein kleiner Einblick in die PouchDB-Revisionen

PouchDB stellt APIs bereit, um den Revisionsverlauf eines Dokuments abzurufen. Wir können den Revisionsverlauf auf diese Weise abfragen:

todos.pouch.get(todo.id, {
    revs: true
})

Dadurch wird ein Dokument zurückgegeben, das _revisions enthält Feld:

{
  "id": "559da26d-ad0f-42bc-a172-1821641bf2bb",
  "_rev": "4-95162faab173d1e748952179e0db1a53",
  "_revisions": {
    "ids": [
      "95162faab173d1e748952179e0db1a53",
      "94162faab173d1e748952179e0db1a53",
      "9055e63d99db056a95b61936f0185c8c",
      "de71900ec14567088bed5914b2439896"
    ],
    "start": 4
  }
}

Hier ids enthält eine Revisionshierarchie von Revisionen (einschließlich der aktuellen) und start enthält die "Präfixnummer" für die aktuelle Revision. Jedes Mal, wenn eine neue Revision hinzugefügt wird start wird inkrementiert und ein neuer Hash wird am Anfang der ids hinzugefügt Array.

Wenn ein Dokument mit einem Remote-Server synchronisiert wird, _revisions und _rev Felder müssen enthalten sein. Auf diese Weise haben alle Clients letztendlich die vollständige Versionshistorie. Dies geschieht automatisch, wenn PouchDB für die Synchronisierung mit CouchDB eingerichtet ist. Die obige Pull-Anforderung ermöglicht dies auch bei der Synchronisierung über GraphQL.

Beachten Sie, dass nicht alle Clients notwendigerweise alle Revisionen haben, aber alle von ihnen werden schließlich die neuesten Versionen und den Verlauf der Revisions-IDs für diese Versionen haben.

Konfliktlösung

Ein Konflikt wird erkannt, wenn zwei Revisionen denselben Elternteil haben, oder einfacher, wenn zwei beliebige Revisionen dieselbe Tiefe haben. Wenn ein Konflikt erkannt wird, verwenden CouchDB und PouchDB denselben Algorithmus, um automatisch einen Gewinner auszuwählen:

  1. Überarbeitungen mit dem höchsten Tiefenfeld auswählen, die nicht als gelöscht markiert sind
  2. Wenn es nur 1 solches Feld gibt, behandeln Sie es als Gewinner
  3. Wenn es mehr als 1 gibt, sortieren Sie die Überarbeitungsfelder in absteigender Reihenfolge und wählen Sie das erste aus.

Hinweis zum Löschen: PouchDB &CouchDB löschen niemals Revisionen oder Dokumente, stattdessen wird eine neue Revision mit einem auf true gesetzten _deleted-Flag erstellt. Daher werden in Schritt 1 des obigen Algorithmus alle Ketten, die mit einer als gelöscht markierten Revision enden, ignoriert.

Ein nettes Merkmal dieses Algorithmus ist, dass keine Koordination zwischen Clients oder dem Client und dem Server erforderlich ist, um einen Konflikt zu lösen. Es ist auch kein zusätzlicher Marker erforderlich, um eine Version als Gewinner zu markieren. Jeder Client und der Server wählen unabhängig voneinander den Gewinner aus. Aber der Gewinner wird dieselbe Revision sein, weil sie denselben deterministischen Algorithmus verwenden. Selbst wenn bei einem der Clients einige Überarbeitungen fehlen, wird schließlich, wenn diese Überarbeitungen synchronisiert werden, dieselbe Überarbeitung als Gewinner ausgewählt.

Implementieren benutzerdefinierter Konfliktlösungsstrategien

Aber was, wenn wir eine alternative Konfliktlösungsstrategie wollen? Zum Beispiel „nach Feldern zusammenführen“ – Wenn zwei widersprüchliche Revisionen verschiedene Schlüssel des Objekts geändert haben, möchten wir automatisch zusammenführen, indem wir eine Revision mit beiden Schlüsseln erstellen. Der empfohlene Weg, dies in PouchDB zu tun, ist:

  1. Erstellen Sie diese neue Revision für eine der Ketten
  2. Fügen Sie eine Überarbeitung hinzu, bei der _deleted auf true gesetzt ist, zu jeder der anderen Ketten

Die zusammengeführte Revision ist nun automatisch die Gewinner-Revision gemäß dem obigen Algorithmus. Wir können eine benutzerdefinierte Auflösung entweder auf dem Server oder dem Client durchführen. Wenn die Revisionen synchronisiert werden, sehen alle Clients und der Server die zusammengeführte Revision als die gewinnende Revision.

Konfliktlösung mit Hasura und RxDB

Um die obige Konfliktlösungsstrategie zu implementieren, benötigen wir Hasura, um auch den Revisionsverlauf zu speichern und damit RxDB Revisionen während der Replikation mit GraphQL synchronisiert.

Hasura einrichten

Fahren Sie mit dem Todo-App-Beispiel aus dem vorherigen Beitrag fort. Wir müssen das Schema für die Todos-Tabelle wie folgt aktualisieren:

todo (
  id: text primary key,
  userId: text,
  text: text, <br/>
  createdAt: timestamp,
  isCompleted: boolean,
  deleted: boolean,
  updatedAt: boolean,
  _revisions: jsonb,
  _rev: text primary key,
  _parent_rev: text,
  _depth: integer,
)

Beachten Sie die zusätzlichen Felder:

  • _rev stellt die Überarbeitung des Datensatzes dar.
  • _parent_rev stellt die übergeordnete Revision des Datensatzes dar
  • _depth ist die Tiefe des Datensatzes im Revisionsbaum
  • _revisions enthält die vollständige Historie der Überarbeitungen des Datensatzes.

Der Primärschlüssel für die Tabelle ist (id , _rev ).

Genau genommen brauchen wir nur die _revisions Feld, da die anderen Informationen daraus abgeleitet werden können. Die Verfügbarkeit der anderen Felder erleichtert jedoch die Konflikterkennung und -lösung.

Clientseitige Einrichtung

Wir müssen syncRevisions setzen auf true setzen, während die Replikation eingerichtet wird


    async setupGraphQLReplication(auth) {
        const replicationState = this.db.todos.syncGraphQL({
            url: syncURL,
            headers: {
                'Authorization': `Bearer ${auth.idToken}`
            },
            push: {
                batchSize,
                queryBuilder: pushQueryBuilder
            },
            pull: {
                queryBuilder: pullQueryBuilder(auth.userId)
            },

            live: true,

            liveInterval: 1000 * 60 * 10,
            deletedFlag: 'deleted',
            syncRevisions: true,
        });

       ...
    }

Wir müssen auch ein Textfeld last_pulled_rev hinzufügen zum RxDB-Schema. Dieses Feld wird intern vom Plug-in verwendet, um zu vermeiden, dass vom Server abgerufene Revisionen zurück auf den Server verschoben werden.

const todoSchema = {
    ...
    'properties': {
        ...
        'last_pulled_rev': {
            'type': 'string'
        }
    },
    ...
};

Schließlich müssen wir die Pull- und Push-Abfragegeneratoren ändern, um revisionsbezogene Informationen zu synchronisieren

Pull-Abfragegenerator

const pullQueryBuilder = (userId) => {
    return (doc) => {
        if (!doc) {
            doc = {
                id: '',
                updatedAt: new Date(0).toUTCString()
            };
        }

        const query = `{
            todos(
                where: {
                    _or: [
                        {updatedAt: {_gt: "${doc.updatedAt}"}},
                        {
                            updatedAt: {_eq: "${doc.updatedAt}"},
                            id: {_gt: "${doc.id}"}
                        }
                    ],
                    userId: {_eq: "${userId}"} 
                },
                limit: ${batchSize},
                order_by: [{updatedAt: asc}, {id: asc}]
            ) {
                id
                text
                isCompleted
                deleted
                createdAt
                updatedAt
                userId
                _rev
                _revisions
            }
        }`;
        return {
            query,
            variables: {}
        };
    };
};

Wir rufen jetzt die Felder _rev &_revisions ab. Das aktualisierte Plugin verwendet diese Felder, um lokale PouchDB-Revisionen zu erstellen.

Push-Abfragegenerator


const pushQueryBuilder = doc => {
    const query = `
        mutation InsertTodo($todo: [todos_insert_input!]!) {
            insert_todos(objects: $todo){
                returning {
                  id
                }
            }
       }
    `;

    const depth = doc._revisions.start;
    const parent_rev = depth == 1 ? null : `${depth - 1}-${doc._revisions.ids[1]}`

    const todo = Object.assign({}, doc, {
        _depth: depth,
        _parent_rev: parent_rev
    })

    delete todo['updatedAt']

    const variables = {
        todo: todo
    };

    return {
        query,
        variables
    };
};

Mit dem aktualisierten Plugin wird der Eingabeparameter doc enthält jetzt _rev und _revisions Felder. In der GraphQL-Abfrage geben wir an Hasura weiter. Wir fügen die Felder _depth hinzu , _parent_rev zu doc bevor Sie dies tun.

Früher haben wir ein Upsert verwendet, um ein todo einzufügen oder zu aktualisieren Aufzeichnung auf Hasura. Da nun jede Version ein neuer Datensatz wird, verwenden wir stattdessen die einfache alte Insert-Mutation.

Konfliktlösung implementieren

Wenn jetzt zwei verschiedene Clients widersprüchliche Änderungen vornehmen, werden beide Revisionen synchronisiert und in Hasura vorhanden sein. Beide Clients erhalten schließlich auch die andere Revision. Da die Konfliktlösungsstrategie von PouchDB deterministisch ist, wählen beide Clients dieselbe Version wie die "gewinnende Revision".

Wie können wir diese siegreiche Revision auf dem Server finden? Wir müssen denselben Algorithmus in SQL implementieren.

Implementierung des Konfliktlösungsalgorithmus von CouchDB auf Postgres

Schritt 1:Nicht als gelöscht markierte Blattknoten finden

Dazu müssen wir alle Versionen ignorieren, die eine untergeordnete Revision haben, und alle Versionen, die als gelöscht markiert sind:

    SELECT
        id,
        _rev,
        _depth
    FROM
        todos
    WHERE
        NOT EXISTS (
            SELECT
                id
            FROM
                todos AS t
            WHERE
                todos.id = t.id
                AND t._parent_rev = todos._rev)
            AND deleted = FALSE

Schritt 2:Die Kette mit der maximalen Tiefe finden

Angenommen, wir haben die Ergebnisse aus der obigen Abfrage in einer Tabelle (oder Ansicht oder einer with-Klausel) namens Blätter, können wir feststellen, dass die Kette mit maximaler Tiefe einfach ist:

    SELECT
        id,
        MAX(_depth) AS max_depth
    FROM
        leaves
    GROUP BY
        id

Schritt 3:Gewinnende Überarbeitungen unter Überarbeitungen mit gleicher maximaler Tiefe finden

Unter der Annahme, dass sich die Ergebnisse der obigen Abfrage in einer Tabelle (oder einer Ansicht oder einer with-Klausel) namens max_depths befinden, können wir die siegreiche Revision wie folgt finden:

    SELECT
        leaves.id,
        MAX(leaves._rev) AS _rev
    FROM
        leaves
        JOIN max_depths ON leaves.id = max_depths.id
            AND leaves._depth = max_depths.max_depth
    GROUP BY
        leaves.id

Erstellen einer Ansicht mit erfolgreichen Überarbeitungen

Wenn wir die obigen drei Abfragen zusammenstellen, können wir eine Ansicht erstellen, die uns die erfolgreichen Revisionen wie folgt zeigt:

CREATE OR REPLACE VIEW todos_current_revisions AS
WITH leaves AS (
    SELECT
        id,
        _rev,
        _depth
    FROM
        todos
    WHERE
        NOT EXISTS (
            SELECT
                id
            FROM
                todos AS t
            WHERE
                todos.id = t.id
                AND t._parent_rev = todos._rev)
            AND deleted = FALSE
),
max_depths AS (
    SELECT
        id,
        MAX(_depth) AS max_depth
    FROM
        leaves
    GROUP BY
        id
),
winning_revisions AS (
    SELECT
        leaves.id,
        MAX(leaves._rev) AS _rev
    FROM
        leaves
        JOIN max_depths ON leaves.id = max_depths.id
            AND leaves._depth = max_depths.max_depth
    GROUP BY
        (leaves.id))
SELECT
    todos.*
FROM
    todos
    JOIN winning_revisions ON todos._rev = winning_revisions._rev;

Da Hasura Aufrufe nachverfolgen kann und deren Abfrage über GraphQL ermöglicht, können die erfolgreichen Revisionen nun anderen Clients und Diensten zur Verfügung gestellt werden.

Wann immer Sie die Ansicht abfragen, ersetzt Postgres einfach die Ansicht durch die Abfrage in der Ansichtsdefinition und führt die resultierende Abfrage aus. Wenn Sie die Ansicht häufig abfragen, kann dies zu einer Menge verschwendeter CPU-Zyklen führen. Wir können dies optimieren, indem wir Postgres-Trigger verwenden und die erfolgreichen Revisionen in einer anderen Tabelle speichern.

Verwendung von Postgres-Triggern zur Berechnung erfolgreicher Überarbeitungen

Schritt 1:Erstellen Sie eine neue Tabelle todos_current_revisions

Das Schema ist dasselbe wie das der todos Tisch. Der Primärschlüssel ist jedoch die id Spalte anstelle von (id, _rev)

Schritt 2:Postgres-Trigger erstellen

Wir können die Abfrage für den Trigger schreiben, indem wir mit der Ansichtsabfrage beginnen. Da die Triggerfunktion für jeweils eine Zeile ausgeführt wird, können wir die Abfrage vereinfachen:

CREATE OR REPLACE FUNCTION calculate_winning_revision ()
    RETURNS TRIGGER
    AS $BODY$
BEGIN
    INSERT INTO todos_current_revisions WITH leaves AS (
        SELECT
            id,
            _rev,
            _depth
        FROM
            todos
        WHERE
            NOT EXISTS (
                SELECT
                    id
                FROM
                    todos AS t
                WHERE
                    t.id = NEW.id
                    AND t._parent_rev = todos._rev)
                AND deleted = FALSE
                AND id = NEW.id
        ),
        max_depths AS (
            SELECT
                MAX(_depth) AS max_depth
            FROM
                leaves
        ),
        winning_revisions AS (
            SELECT
                MAX(leaves._rev) AS _rev
            FROM
                leaves
                JOIN max_depths ON leaves._depth = max_depths.max_depth
        )
        SELECT
            todos.*
        FROM
            todos
            JOIN winning_revisions ON todos._rev = winning_revisions._rev
    ON CONFLICT ON CONSTRAINT todos_winning_revisions_pkey
        DO UPDATE SET
            _rev = EXCLUDED._rev,
            _revisions = EXCLUDED._revisions,
            _parent_rev = EXCLUDED._parent_rev,
            _depth = EXCLUDED._depth,
            text = EXCLUDED.text,
            "updatedAt" = EXCLUDED."updatedAt",
            deleted = EXCLUDED.deleted,
            "userId" = EXCLUDED."userId",
            "createdAt" = EXCLUDED."createdAt",
            "isCompleted" = EXCLUDED."isCompleted";
    RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;

CREATE TRIGGER trigger_insert_todos
    AFTER INSERT ON todos
    FOR EACH ROW
    EXECUTE PROCEDURE calculate_winning_revision ()

Das ist es! Wir können jetzt die Gewinnerversionen sowohl auf dem Server als auch auf dem Client abfragen.

Benutzerdefinierte Konfliktlösung

Sehen wir uns nun die Implementierung einer benutzerdefinierten Konfliktlösung mit Hasura und RxDB an.

Benutzerdefinierte Konfliktlösung auf der Serverseite

Nehmen wir an, wir möchten die Todos nach Feldern zusammenführen. Wie gehen wir vor? Das Wesentliche unten zeigt uns dies:

Dieses SQL sieht nach viel aus, aber der einzige Teil, der sich mit der eigentlichen Merge-Strategie befasst, ist folgender:

CREATE OR REPLACE FUNCTION merge_revisions (item1 jsonb, item2 jsonb)
    RETURNS jsonb
    AS $$
BEGIN
    IF NOT item1 ? 'id' THEN
        RETURN item2;
    ELSE
        RETURN item1 || (item2 -> 'diff');
    END IF;
END;
$$
LANGUAGE plpgsql;

CREATE OR REPLACE AGGREGATE agg_merge_revisions (jsonb) (
    INITCOND = '{}',
    STYPE = jsonb,
    SFUNC = merge_revisions
);

Hier deklarieren wir eine benutzerdefinierte Postgres-Aggregatfunktion agg_merge_revisions Elemente zu verschmelzen. Die Funktionsweise ähnelt einer 'Reduce'-Funktion:Postgres initialisiert den Aggregatwert auf '{}' , führen Sie dann merge_revisions aus Funktion mit dem aktuellen Aggregat und dem nächsten zusammenzuführenden Element. Wenn wir also 3 widersprüchliche Versionen zusammenführen müssten, wäre das Ergebnis:

merge_revisions(merge_revisions(merge_revisions('{}', v1), v2), v3)

Wenn wir eine andere Strategie implementieren wollen, müssen wir die merge_revisions ändern Funktion. Wenn wir zum Beispiel die Strategie „Last Write Wins“ implementieren möchten:

CREATE OR REPLACE FUNCTION merge_revisions (item1 jsonb, item2 jsonb)
    RETURNS jsonb
    AS $$
BEGIN
    IF NOT (item1 ? 'id') THEN
        RETURN item2;
    ELSE
        IF (item2 -> 'updatedAt') > (item1 -> 'updatedAt') THEN
            RETURN item2
        ELSE
            RETURN item1
        END IF;
    END IF;
END;
$$
LANGUAGE plpgsql;

Die Einfügeabfrage im obigen Kern kann in einem Post-Insert-Trigger ausgeführt werden, um Konflikte automatisch zusammenzuführen, wann immer sie auftreten.

Hinweis: Oben haben wir SQL verwendet, um eine benutzerdefinierte Konfliktlösung zu implementieren. Ein alternativer Ansatz ist die Verwendung einer Write-an-Action:

  1. Erstellen Sie anstelle der standardmäßig automatisch generierten Insert-Mutation eine benutzerdefinierte Mutation, um die Insertion zu handhaben.
  2. Erstellen Sie im Aktionshandler die neue Revision des Datensatzes. Wir können dafür die Hasura-Insert-Mutation verwenden.
  3. Alle Revisionen für das Objekt mit einer Listenabfrage abrufen
  4. Erkennen Sie Konflikte, indem Sie den Revisionsbaum durchlaufen.
  5. Schreiben Sie die zusammengeführte Version zurück.

Dieser Ansatz wird Ihnen gefallen, wenn Sie diese Logik lieber in einer anderen Sprache als SQL schreiben möchten. Ein anderer Ansatz besteht darin, eine SQL-Ansicht zu erstellen, um die widersprüchlichen Revisionen anzuzeigen, und die verbleibende Logik im Aktionshandler zu implementieren. Dies vereinfacht Schritt 4. oben, da wir jetzt einfach die Ansicht zum Erkennen von Konflikten abfragen können.

Benutzerdefinierte Konfliktlösung auf der Clientseite

Es gibt Szenarien, in denen Sie einen Benutzereingriff benötigen, um einen Konflikt lösen zu können. Wenn wir beispielsweise etwas wie die Trello-App entwickeln und zwei Benutzer die Beschreibung derselben Aufgabe ändern, möchten Sie dem Benutzer möglicherweise beide Versionen zeigen und ihn eine zusammengeführte Version erstellen lassen. In diesen Szenarien müssen wir den Konflikt auf der Clientseite lösen.

Die clientseitige Konfliktlösung ist einfacher zu implementieren, da PouchDB APIs bereits offenlegt, um widersprüchliche Revisionen abzufragen. Wenn wir uns die todos ansehen RxDB-Sammlung aus dem vorherigen Beitrag, hier ist, wie wir die widersprüchlichen Versionen abrufen können:

todos.pouch.get(todo.id, {
    conflicts: true
})

Die obige Abfrage würde die widersprüchlichen Revisionen in _conflicts auffüllen Feld im Ergebnis. Wir können diese dann dem Benutzer zur Lösung vorlegen.

Fazit

PouchDB wird mit einem flexiblen und leistungsstarken Konstrukt für Versionierungs- und Konfliktmanagementlösungen geliefert. Dieser Beitrag zeigte uns, wie man diese Konstrukte mit Hasura/Postgres verwendet. In diesem Beitrag haben wir uns darauf konzentriert, dies mit plpgsql zu tun. Wir werden einen Folgebeitrag veröffentlichen, der zeigt, wie Sie dies mit Actions tun, damit Sie die Sprache Ihrer Wahl im Backend verwenden können!

Hat Ihnen dieser Artikel gefallen? Begleiten Sie uns auf Discord für weitere Diskussionen über Hasura &GraphQL!

Melden Sie sich für unseren Newsletter an, um zu erfahren, wann wir neue Artikel veröffentlichen.