Batching speichert transparent

Sep 23 2020

Wir verwenden die folgenden Frameworks und Versionen:

  • jOOQ 3.11.1
  • Spring Boot 2.3.1.RELEASE
  • Spring 5.2.7.RELEASE

Ich habe ein Problem, bei dem ein Teil unserer Geschäftslogik in logische Einheiten unterteilt ist, die wie folgt aussehen:

  • Eine Anfrage mit einer Benutzertransaktion wird empfangen
  • Diese Anfrage enthält verschiedene Informationen, wie z. B. die Art der Transaktion, welche Produkte Teil dieser Transaktion sind, welche Art von Zahlungen getätigt wurden usw.
  • Diese Attribute werden dann einzeln in der Datenbank gespeichert.

Im Code sieht dies ungefähr wie folgt aus:

TransactionRecord transaction = transactionRepository.create();
transaction.create(creationCommand);`

In Transaction#create(das transaktional ausgeführt wird) tritt Folgendes auf:

storeTransaction();
storePayments();
storeProducts();
// ... other relevant information

Eine bestimmte Transaktion kann viele verschiedene Arten von Produkten und Attributen aufweisen, die alle gespeichert sind. Viele dieser Attribute führen zu UPDATEAnweisungen, während einige zu INSERTAnweisungen führen können - es ist schwierig, dies im Voraus vollständig zu wissen.

Beispielsweise storeProductssieht die Methode ungefähr wie folgt aus:

products.forEach(product -> {
    ProductRecord record = productRepository.findProductByX(...);
    if (record == null) {
        record = productRepository.create();
        record.setX(...);
        record.store();
    } else {
      // do something else
    }
});

Wenn die Produkte neu sind, werden sie INSERTbearbeitet. Andernfalls können andere Berechnungen stattfinden. Abhängig von der Größe der Transaktion kann diese Einzelbenutzertransaktion offensichtlich zu bis zu O(n)Datenbankaufrufen / Roundtrips führen, und noch mehr, je nachdem, welche anderen Attribute vorhanden sind. Bei Transaktionen, bei denen eine große Anzahl von Attributen vorhanden ist, kann dies zu mehr als Hunderten von Datenbankaufrufen für eine einzelne Anforderung (!) Führen. Ich möchte dies so nahe wie möglich bringen O(1), um eine vorhersehbarere Belastung unserer Datenbank zu erreichen.

Natürlich kommen hier Batch- und Bulk-Inserts / Updates in den Sinn. Was ich tun möchte, ist, alle diese Anweisungen mit einem einzigen Stapel zu stapeln jOOQund nach erfolgreichem Methodenaufruf vor dem Festschreiben auszuführen. Ich habe mehrere Beiträge ( SO Post , jOOQ API , jOOQ GitHub Feature Request ) gefunden, in denen dieses Thema implizit erwähnt wird, und einen Beitrag für Benutzergruppen , der explizit mit meinem Problem zusammenhängt.

Da ich Springzusammen mit verwende jOOQ, glaube ich, dass meine ideale Lösung (vorzugsweise deklarativ) ungefähr so ​​aussehen würde:

@Batched(100) // batch size as parameter, potentially
@Transactional
public void createTransaction(CreationCommand creationCommand) {
    // all inserts/updates above are added to a batch and executed on successful invocation
}

Damit dies funktioniert, stelle ich , ich brauche einen scoped (Verwaltung ThreadLocal/ Transactional/ Sessionscope) Ressource , die den Überblick über den aktuellen Stapel halten , so dass:

  1. Vor dem Verfahren der Eingabe ein leerer Stapel erstellt wird , wenn die Methode ist @Batched,
  2. Ein benutzerdefinierter DSLContext(möglicherweise erweiterter DefaultDSLContext) ThreadLocalCode, der über DI verfügbar gemacht wird, verfügt über ein Flag, das festhält, ob und wenn aktuelle Anweisungen gestapelt werden sollen oder nicht
  3. Fangen Sie die Anrufe ab und fügen Sie sie dem aktuellen Stapel hinzu, anstatt sie sofort auszuführen.

In Schritt 3 müsste jedoch ein großer Teil unseres Codes aus der (IMO) relativ lesbaren Datei neu geschrieben werden:

records.forEach(record -> {
    record.setX(...);
    // ...
    record.store();
}

zu:

userObjects.forEach(userObject -> {
    dslContext.insertInto(...).values(userObject.getX(), ...).execute();
}

Dies würde den Zweck dieser Abstraktion in erster Linie zunichte machen, da die zweite Form auch mit DSLContext#batchStoreoder umgeschrieben werden kann DSLContext#batchInsert. IMO, Batching und Bulk-Insertion sollten jedoch nicht Sache des einzelnen Entwicklers sein und auf einer höheren Ebene (z. B. durch das Framework) transparent gehandhabt werden können.

Ich finde die Lesbarkeit der jOOQAPI ein erstaunlicher Vorteil bei der Verwendung, aber es scheint, dass sie sich (soweit ich das beurteilen kann) in solchen Fällen nicht sehr gut zum Abfangen / Erweitern eignet. Ist es mit der jOOQ 3.11.1(oder sogar der aktuellen) API möglich, ein ähnliches Verhalten wie bei der transparenten Stapel- / Massenbehandlung zu erzielen? Was würde das bedeuten?


BEARBEITEN:

Eine mögliche, aber äußerst hackige Lösung, die in den Sinn kommt, um eine transparente Stapelverarbeitung von Geschäften zu ermöglichen, wäre etwa die folgende:

  1. Erstellen Sie ein RecordListenerund fügen Sie es als Standard hinzu, Configurationwenn die Stapelverarbeitung aktiviert ist.
  2. In RecordListener#storeStart, fügen Sie die Abfrage der aktuellen Transaktion der Charge (zB in a ThreadLocal<List>)
  3. Das AbstractRecordhat ein changedFlag, das vor dem Speichern überprüft ( org.jooq.impl.UpdatableRecordImpl#store0, org.jooq.impl.TableRecordImpl#addChangedValues) wird. Durch Zurücksetzen (und Speichern für die spätere Verwendung) wird der Speichervorgang zum No-Op.
  4. Schließlich nach erfolgreichem Methodenaufruf, jedoch vor dem Festschreiben:
  • Setzen Sie die changesFlags der jeweiligen Datensätze auf die richtigen Werte zurück
  • Rufen Sie org.jooq.UpdatableRecord#storediesmal ohne RecordListeneroder während des Überspringens der storeStartMethode auf (verwenden Sie möglicherweise ein anderes ThreadLocalFlag, um zu überprüfen, ob bereits eine Stapelverarbeitung durchgeführt wurde).

Soweit ich sagen kann, ist dieser Ansatz sollte funktionieren, in der Theorie. Offensichtlich ist es extrem hackig und bruchanfällig, da sich die Interna der Bibliothek jederzeit ändern können, wenn der Code von Reflection abhängt, um zu funktionieren.

Kennt jemand einen besseren Weg, nur die öffentliche jOOQAPI zu verwenden?

Antworten

1 LukasEder Sep 24 2020 at 19:41

jOOQ 3.14 Lösung

Sie haben bereits die relevante Funktionsanforderung Nr. 3419 entdeckt , die dies auf JDBC-Ebene ab jOOQ 3.14 löst. Sie können entweder das BatchedConnectiondirekt verwenden und Ihre eigene Verbindung umschließen, um das Folgende zu implementieren, oder diese API verwenden:

ctx.batched(c -> {

    // Make sure all records are attached to c, not ctx, e.g. by fetching from c.dsl()
    records.forEach(record -> {
        record.setX(...);
        // ...
        record.store();
    }
});

jOOQ 3.13 und vor der Lösung

Bis # 3419 implementiert ist (dies wird in jOOQ 3.14 der Fall sein), können Sie dies vorerst selbst als Problemumgehung implementieren. Sie würden an den Proxy haben eine JDBC Connectionund PreparedStatementund ...

... alle abfangen:

  • Ruft auf Connection.prepareStatement(String)und gibt eine zwischengespeicherte Proxy-Anweisung zurück, wenn die SQL-Zeichenfolge mit der zuletzt vorbereiteten Anweisung übereinstimmt, oder führt die zuletzt vorbereitete Anweisung im Batch aus und erstellt eine neue.
  • Anrufe an PreparedStatement.executeUpdate()und execute()und ersetzen diese durch Anrufe anPreparedStatement.addBatch()

... alle delegieren:

  • Ruft eine andere API auf, z. B. Connection.createStatement()die oben gepufferten Stapel leeren soll, und ruft stattdessen die Delegaten-API auf.

Ich würde nicht empfehlen, sich in jOOQs RecordListenerund anderen SPIs zurechtzufinden. Ich denke, das ist die falsche Abstraktionsebene, um Datenbankinteraktionen zu puffern. Sie möchten auch andere Anweisungstypen stapeln.

Beachten Sie, dass jOOQ standardmäßig UpdatableRecordversucht, generierte Identitätswerte abzurufen (siehe Settings.returnIdentityOnUpdatableRecord), was das Stapeln verhindert. Solche store()Aufrufe müssen sofort ausgeführt werden, da Sie möglicherweise erwarten, dass der Identitätswert verfügbar ist.