web-dev-qa-db-fra.com

Obtenez les transactions Knex.js fonctionnant avec ES7 async / wait

J'essaie de coupler l'async/wait d'ES7 avec transactions knex.js.

Bien que je puisse facilement jouer avec du code non transactionnel, j'ai du mal à faire fonctionner correctement les transactions en utilisant la structure asynchrone/attente susmentionnée.

J'utilise ce module pour simuler asynchrone/attendre

Voici ce que j'ai actuellement:

Version non transactionnelle:

fonctionne bien mais n'est pas transactionnel

app.js

// assume `db` is a knex instance

app.post("/user", async((req, res) => {
  const data = {
   idUser: 1,
   name: "FooBar"
  }

  try {
    const result = await(user.insert(db, data));
    res.json(result);
  } catch (err) {
    res.status(500).json(err);
  }
}));

user.js

insert: async (function(db, data) {
  // there's no need for this extra call but I'm including it
  // to see example of deeper call stacks if this is answered

  const idUser =  await(this.insertData(db, data));
  return {
    idUser: idUser
  }
}),

insertData: async(function(db, data) {
  // if any of the following 2 fails I should be rolling back

  const id = await(this.setId(db, idCustomer, data));
  const idCustomer = await(this.setData(db, id, data));

  return {
    idCustomer: idCustomer
  }
}),

// DB Functions (wrapped in Promises)

setId: function(db, data) {
  return new Promise(function (resolve, reject) {
    db.insert(data)
    .into("ids")
    .then((result) => resolve(result)
    .catch((err) => reject(err));
  });
},

setData: function(db, id, data) {
  data.id = id;

  return new Promise(function (resolve, reject) {
    db.insert(data)
    .into("customers")
    .then((result) => resolve(result)
    .catch((err) => reject(err));
  });
}

Tenter de le rendre transactionnel

user.js

// Start transaction from this call

insert: async (function(db, data) {
 const trx = await(knex.transaction());
 const idCustomer =  await(user.insertData(trx, data));

 return {
    idCustomer: idCustomer
  }
}),

il semble que await(knex.transaction()) retourne cette erreur:

[TypeError: container is not a function]

17
Nik Kyriakides

Async/Wait est basé sur des promesses, il semble donc que vous ayez juste besoin d'encapsuler toutes les méthodes knex pour renvoyer des objets "compatibles avec les promesses".

Voici une description de la façon dont vous pouvez convertir des fonctions arbitraires pour qu'elles fonctionnent avec des promesses, afin qu'elles puissent fonctionner avec async/wait:

Essayer de comprendre comment fonctionne la promisification avec BlueBird

Essentiellement, vous voulez le faire:

var transaction = knex.transaction;
knex.transaction = function(callback){ return knex.transaction(callback); }

Cela est dû au fait que "asynchrone/attendent nécessite soit une fonction avec un seul argument de rappel, soit une promesse", tandis que knex.transaction ressemble à ça:

function transaction(container, config) {
  return client.transaction(container, config);
}

Alternativement, vous pouvez créer une nouvelle fonction async et l'utiliser comme ceci:

async function transaction() {
  return new Promise(function(resolve, reject){
    knex.transaction(function(error, result){
      if (error) {
        reject(error);
      } else {
        resolve(result);
      }
    });
  });
}

// Start transaction from this call

insert: async (function(db, data) {
 const trx = await(transaction());
 const idCustomer =  await(person.insertData(trx, authUser, data));

 return {
    idCustomer: idCustomer
  }
})

Cela peut également être utile: Transaction Knex avec promesses

(Notez également que je ne suis pas familier avec l'API de knex, donc je ne suis pas sûr de ce que les paramètres sont passés à knex.transaction, ceux ci-dessus ne sont que par exemple).

10
Lance Pollard

Je n'ai pas pu trouver de réponse solide à cela n'importe où (avec des rollbacks et des commits) alors voici ma solution.

Vous devez d'abord "Promettre" le knex.transaction fonction. Il existe des bibliothèques pour cela, mais pour un exemple rapide, je l'ai fait:

const promisify = (fn) => new Promise((resolve, reject) => fn(resolve));

Cet exemple crée un article de blog et un commentaire, et annule les deux en cas d'erreur avec l'un ou l'autre.

const trx = await promisify(db.transaction);

try {
  const postId = await trx('blog_posts')
  .insert({ title, body })
  .returning('id'); // returns an array of ids

  const commentId = await trx('comments')
  .insert({ post_id: postId[0], message })
  .returning('id'); 

  await trx.commit();
} catch (e) {
  await trx.rollback();
}
27
sf77

Pour ceux qui viennent en 2019.

Après avoir mis à jour Knex vers la version 0.16.5. La réponse de sf77 ne fonctionne plus en raison du changement dans la fonction transaction de Knex:

transaction(container, config) {
  const trx = this.client.transaction(container, config);
  trx.userParams = this.userParams;
  return trx;
}

Solution

Gardez la fonction promisify de sf77:

const promisify = (fn) => new Promise((resolve, reject) => fn(resolve));

Mettre à jour trx

à partir de

const trx = await promisify(db.transaction);

à

const trx =  await promisify(db.transaction.bind(db));
4
Peter Vu

Je pense avoir trouvé une solution plus élégante au problème.

Empruntant aux knex Transaction docs , je comparerai leur style de promesse avec le style asynchrone/attente qui a fonctionné pour moi.

Style de promesse

var Promise = require('bluebird');

// Using trx as a transaction object:
knex.transaction(function(trx) {

  var books = [
    {title: 'Canterbury Tales'},
    {title: 'Moby Dick'},
    {title: 'Hamlet'}
  ];

  knex.insert({name: 'Old Books'}, 'id')
    .into('catalogues')
    .transacting(trx)
    .then(function(ids) {
      return Promise.map(books, function(book) {
        book.catalogue_id = ids[0];

        // Some validation could take place here.

        return knex.insert(book).into('books').transacting(trx);
      });
    })
    .then(trx.commit)
    .catch(trx.rollback);
})
.then(function(inserts) {
  console.log(inserts.length + ' new books saved.');
})
.catch(function(error) {
  // If we get here, that means that neither the 'Old Books' catalogues insert,
  // nor any of the books inserts will have taken place.
  console.error(error);
});

style asynchrone/attente

var Promise = require('bluebird'); // import Promise.map()

// assuming knex.transaction() is being called within an async function
const inserts = await knex.transaction(async function(trx) {

  var books = [
    {title: 'Canterbury Tales'},
    {title: 'Moby Dick'},
    {title: 'Hamlet'}
  ];

  const ids = await knex.insert({name: 'Old Books'}, 'id')
    .into('catalogues')
    .transacting(trx);

  const inserts = await Promise.map(books, function(book) {
        book.catalogue_id = ids[0];

        // Some validation could take place here.

        return knex.insert(book).into('books').transacting(trx);
      });
    })
  await trx.commit(inserts); // whatever gets passed to trx.commit() is what the knex.transaction() promise resolves to.
})

Les documents indiquent:

Lancer une erreur directement à partir de la fonction de gestionnaire de transactions annule automatiquement la transaction, tout comme renvoyer une promesse rejetée.

Il semble que la fonction de rappel de transaction ne devrait renvoyer rien ou une promesse. Déclarer le rappel en tant que fonction asynchrone signifie qu'il renvoie une promesse.

Un avantage de ce style est que vous n'avez pas à appeler manuellement la restauration. Le retour d'une promesse rejetée déclenchera automatiquement la restauration.

Assurez-vous de transmettre tous les résultats que vous souhaitez utiliser ailleurs à l'appel final trx.commit ().

J'ai testé ce modèle dans mon propre travail et il fonctionne comme prévu.

4
nigel.smk

Ajoutant à l'excellente réponse de sf77, j'ai implémenté ce modèle dans TypeScript pour ajouter un nouvel utilisateur où vous devez effectuer les opérations suivantes en 1 transaction:

  1. création d'un enregistrement utilisateur dans la table USER
  2. création d'un enregistrement de connexion dans la table LOGIN
public async addUser(user: User, hash: string): Promise<User> {

        //transform knex transaction such that can be used with async-await
        const promisify = (fn: any) => new Promise((resolve, reject) => fn(resolve));
        const trx: knex.Transaction  = <knex.Transaction> await promisify(db.transaction);

        try {
                let users: User [] = await trx
                        .insert({
                                name: user.name,
                                email: user.email,
                                joined: new Date()})
                        .into(config.DB_TABLE_USER)
                        .returning("*")

                await trx
                        .insert({
                                email: user.email,
                                hash
                        }).into(config.DB_TABLE_LOGIN)
                        .returning("email")
                await trx.commit();
                return Promise.resolve(users[0]);
        }
        catch(error) { 
                await trx.rollback;
                return Promise.reject("Error adding user: " + error) 
        }
}
3
gomisha