Batching stores de manière transparente

Sep 23 2020

Nous utilisons les frameworks et versions suivants:

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

J'ai un problème où une partie de notre logique métier est divisée en unités logiques qui se présentent comme suit:

  • La demande contenant une transaction utilisateur est reçue
  • Cette demande contient diverses informations, telles que le type de transaction, quels produits font partie de cette transaction, quel type de paiement a été effectué, etc.
  • Ces attributs sont ensuite stockés individuellement dans la base de données.

Dans le code, cela ressemble approximativement à ceci:

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

Dans Transaction#create(qui s'exécute de manière transactionnelle), quelque chose comme ce qui suit se produit:

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

Une transaction donnée peut avoir de nombreux types de produits et d'attributs, qui sont tous stockés. Un grand nombre de ces attributs aboutissent à des UPDATEdéclarations, tandis que certains peuvent aboutir à des INSERTdéclarations - il est difficile de savoir à l'avance.

Par exemple, la storeProductsméthode ressemble approximativement à ce qui suit:

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

Si les produits sont neufs, ils sont INSERTédités. Sinon, d'autres calculs peuvent avoir lieu. En fonction de la taille de la transaction, cette transaction à un seul utilisateur pourrait évidemment entraîner jusqu'à des O(n)appels / allers-retours dans la base de données, et même plus en fonction des autres attributs présents. Dans les transactions où un grand nombre d'attributs sont présents, cela peut entraîner plus de centaines d'appels à la base de données pour une seule requête (!). Je voudrais ramener cela le plus près possible O(1)afin d'avoir une charge plus prévisible sur notre base de données.

Naturellement, les insertions / mises à jour par lots et en vrac viennent à l'esprit ici. Ce que je voudrais faire est de regrouper toutes ces instructions en un seul lot en utilisant jOOQ, et de les exécuter après un appel de méthode réussi avant la validation. J'ai trouvé plusieurs articles ( SO Post , jOOQ API , jOOQ GitHub Feature Request ) où ce sujet est implicitement mentionné, et un article de groupe d'utilisateurs qui semblait explicitement lié à mon problème.

Puisque j'utilise Springavec jOOQ, je pense que ma solution idéale (de préférence déclarative) ressemblerait à ceci:

@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
}

Pour que cela fonctionne, j'imagine que je devrais gérer une ressource de portée ( ThreadLocal/ Transactional/ Sessionscope) qui peut garder une trace du lot actuel de telle sorte que:

  1. Avant d'entrer la méthode, un lot vide est créé si la méthode est @Batched,
  2. Une personnalisation DSLContext(peut-être une extension DefaultDSLContext) qui est rendue disponible via DI a un ThreadLocalindicateur qui garde une trace de savoir si les instructions actuelles doivent être groupées ou non, et si c'est le cas
  3. Interceptez les appels et ajoutez-les au lot actuel au lieu de les exécuter immédiatement.

Cependant, l'étape 3 nécessiterait de réécrire une grande partie de notre code à partir de (IMO) relativement lisible:

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

à:

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

ce qui irait à l'encontre de l'objectif d'avoir cette abstraction en premier lieu, puisque la seconde forme peut également être réécrite en utilisant DSLContext#batchStoreou DSLContext#batchInsert. Cependant, l'OMI, le traitement par lots et l'insertion en bloc ne devraient pas être du ressort du développeur individuel et devraient pouvoir être traités de manière transparente à un niveau supérieur (par exemple par le cadre).

Je trouve que la lisibilité de l' jOOQAPI est un avantage incroyable de son utilisation, mais il semble qu'elle ne se prête pas très bien (pour autant que je sache) à l'interception / extension pour de tels cas. Est-il possible, avec l' jOOQ 3.11.1API (ou même actuelle), d'obtenir un comportement similaire au précédent avec une gestion transparente des lots / vrac? Qu'est-ce que cela impliquerait?


ÉDITER:

Une solution possible mais extrêmement pirate qui me vient à l'esprit pour permettre un traitement par lots transparent des magasins serait quelque chose comme la suivante:

  1. Créez un RecordListeneret ajoutez-le par défaut à Configurationchaque fois que le traitement par lots est activé.
  2. Dans RecordListener#storeStart, ajoutez la requête au lot de la transaction en cours (par exemple dans a ThreadLocal<List>)
  3. Le AbstractRecorda un changeddrapeau qui est vérifié ( org.jooq.impl.UpdatableRecordImpl#store0, org.jooq.impl.TableRecordImpl#addChangedValues) avant le stockage. Réinitialiser ceci (et l'enregistrer pour une utilisation ultérieure) rend l'opération de stockage un no-op.
  4. Enfin, lors d'un appel de méthode réussi mais avant la validation:
  • Réinitialisez les changesindicateurs des enregistrements respectifs aux valeurs correctes
  • Appelez org.jooq.UpdatableRecord#store, cette fois sans RecordListenerou en sautant la storeStartméthode (peut-être en utilisant un autre ThreadLocalindicateur pour vérifier si le traitement par lots a déjà été effectué).

Autant que je sache, cette approche devrait fonctionner, en théorie. De toute évidence, il est extrêmement piraté et susceptible de se casser car les composants internes de la bibliothèque peuvent changer à tout moment si le code dépend de Reflection pour fonctionner.

Quelqu'un connaît-il un meilleur moyen, en utilisant uniquement l' jOOQAPI publique ?

Réponses

1 LukasEder Sep 24 2020 at 19:41

Solution jOOQ 3.14

Vous avez déjà découvert la demande de fonctionnalité pertinente # 3419 , qui résoudra ce problème au niveau JDBC à partir de jOOQ 3.14. Vous pouvez soit utiliser BatchedConnectiondirectement, encapsulant votre propre connexion pour implémenter ce qui suit, soit utiliser cette API:

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 et avant solution

Pour le moment, jusqu'à ce que # 3419 soit implémenté (ce sera le cas dans jOOQ 3.14), vous pouvez l'implémenter vous-même comme solution de contournement. Vous devrez proxy un JDBC Connectionet PreparedStatementet ...

... intercepter tout:

  • Les appels à Connection.prepareStatement(String), renvoyant une instruction proxy mise en cache si la chaîne SQL est la même que pour la dernière instruction préparée, ou exécutent par lots la dernière instruction préparée et en créent une nouvelle.
  • Appels vers PreparedStatement.executeUpdate()et execute(), et remplacez-les par des appels versPreparedStatement.addBatch()

... déléguez tout:

  • Appels à une autre API, comme par exemple Connection.createStatement(), qui doit vider les lots mis en mémoire tampon ci-dessus, puis appeler l'API déléguée à la place.

Je ne recommanderais pas de pirater votre chemin autour de jOOQ RecordListeneret d'autres SPI, je pense que c'est le mauvais niveau d'abstraction pour tamponner les interactions de base de données. En outre, vous souhaiterez également regrouper d'autres types d'instructions.

Notez que par défaut, jOOQ UpdatableRecordessaie de récupérer les valeurs d'identité générées (voir Settings.returnIdentityOnUpdatableRecord), ce qui empêche le traitement par lots. Ces store()appels doivent être exécutés immédiatement, car vous pouvez vous attendre à ce que la valeur d'identité soit disponible.