Получите транзакции Knex.js, работающие с ES7 async/wait

Я пытаюсь подключить ES7 async/await с помощью транзакций knex.js.

Хотя я могу легко обойтись без транзакционного кода, я изо всех сил пытаюсь заставить транзакции работать правильно, используя вышеупомянутую структуру async/wait.

Я использую этот модуль для имитации async/wait

Здесь у меня есть:

Не транзакционная версия:

работает нормально, но не транзакционно

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 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));
  });
}

Попытка сделать транзакцию

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
  }
}),

кажется, что await(knex.transaction()) возвращает эту ошибку:

[TypeError: container is not a function]

Ответ 1

Async/await основан на promises, поэтому похоже, что вам просто нужно обернуть все методы knex, чтобы вернуть объекты, совместимые с обещаниями.

Вот описание того, как вы можете конвертировать произвольные функции для работы с promises, чтобы они могли работать с async/await:

Попытка понять, как работает работа с BlueBird

По существу, вы хотите сделать это:

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

Это связано с тем, что "async/await требует либо функции с одним аргументом обратного вызова, либо обещанием", тогда как knex.transaction выглядит следующим образом:

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

В качестве альтернативы вы можете создать новую функцию async и использовать ее следующим образом:

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

Это может быть полезно также: Knex Transaction с Promises

(Также обратите внимание: я не знаком с knex API, поэтому не уверен, какие параметры переданы в knex.transaction, приведенные выше, например).

Ответ 2

Я не мог найти твердый ответ для этого где угодно (с откатами и фиксациями), поэтому здесь мое решение.

Сначала вам нужно "Promisify" функцию knex.transaction. Для этого есть библиотеки, но для быстрого примера я сделал это:

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

В этом примере создается сообщение в блоге и комментарий, и он возвращается назад, если есть ошибка.

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();
}

Ответ 3

Я думаю, что нашел более элегантное решение проблемы.

Заимствуя из документации по knex Transaction, я сопоставлю их стиль обещаний со стилем async/await, который работал для меня.

Promise Style

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.
})

Документы утверждают:

Выдача ошибки непосредственно из функции обработчика транзакции автоматически откатывает транзакцию, так же как и возврат отклоненного обещания.

Похоже, что функция обратного вызова транзакции, как ожидается, не будет возвращать ничего или Promise. Объявление обратного вызова как асинхронной функции означает, что она возвращает Обещание.

Одним из преимуществ этого стиля является то, что вам не нужно вызывать откат вручную. Возврат отклоненного обещания автоматически приведет к откату.

Обязательно передайте все результаты, которые вы хотите использовать в другом месте, для окончательного вызова trx.commit().

Я проверил этот шаблон в моей собственной работе, и он работает, как ожидалось.

Ответ 4

Добавляя к sf77 отличный ответ, я реализовал этот шаблон в TypeScript для добавления нового пользователя, для которого необходимо выполнить следующее в одной транзакции:

  1. создание пользовательской записи в таблице USER
  2. создание записи для входа в таблицу 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) 
	}
}

Ответ 5

Для тех, кто приезжает в 2019 году.

После того, как я обновил Knex до версии 0.16.5. Ответ sf77 больше не работает из-за изменения функции transaction Knex:

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

Решение

Сохранить функцию обещания promisify:

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

Обновление trx

от

const trx = await promisify(db.transaction);

в

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