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:
fonctionne bien mais n'est pas transactionnel
// 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);
}
}));
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));
});
}
// 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]
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).
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();
}
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));
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.
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);
});
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.
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:
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)
}
}