Давайте сначала разберемся, что такое транзакция и когда необходимо использовать транзакцию?

Транзакции — важная особенность реляционных баз данных, поскольку они позволяют корректно восстанавливаться после сбоев и поддерживать согласованность базы данных даже в случае сбоя системы. Все запросы внутри транзакции выполняются при одном и том же подключении к базе данных и выполняют весь набор запросов как единую единицу работы. Любой сбой будет означать, что база данных откатит любые запросы, выполненные в этом соединении, к состоянию до транзакции.

Когда я работал над одним из своих проектов, я заметил при операции обновления, которая обновляла несколько таблиц, иногда операция завершалась ошибкой, но половина таблиц все равно обновлялась, потому что я выполнял операции промисов параллельно. Теперь, если ваша операция включает единственную таблицу, будет достаточно простой операции try-catch-async-await.

Но все по-другому, когда вы обновляете несколько таблиц за одну операцию.

Давайте углубимся и поймем, как настроить транзакции с помощью Knex.js.

Отдельные транзакции

Я определяю метод transaction, который не привязан ни к какой модели. Собственный метод Knex transaction не поддерживает «цепочку», поэтому мы определяем новый метод.

checkIsProjectTransaction(mayBeTrx) {
return Boolean(mayBeTrx && mayBeTrx.executionPromise);
}
// Transaction Unified Logic
transaction(trxOrCallback, mayBeCallback) {
if(!this.db.engine) {
throw new Error('Transaction is not initialized');
}
if(mayBeCallback === undefined) {
  if(typeof trxOrCallback !== 'function') {
    throw new Error('Invalid Transaction call');
  }
  return this.db.engine.transaction(trxOrCallback);
}
if(this.checkIsProjectTransaction(trxOrCallback) {
   return mayBeCallback(trxOrCallback);
}
return this.db.engine.transaction(mayBeCallback);
}

Приведенный выше код инициализирует объект транзакции, который впоследствии можно использовать в вашем приложении для операций обновления/сохранения. Лучшим подходом будет сохранение вышеуказанной логики внутри службы, а затем использование экземпляра службы для использования объекта транзакции (trx).

Одна вещь, на которую следует обратить внимание при использовании транзакции Knex, заключается в том, что мы должны передавать объект транзакции в каждом запросе, который взаимодействует с операцией БД.

А теперь самое интересное, давайте посмотрим, как мы можем использовать вышеуказанный объект транзакции и как его использовать.

Для простоты я выполняю операцию сохранения/обновления только с использованием транзакции, но вы можете поместить любое количество операций БД в транзакцию. Просто помните, что если одна из операций завершается неудачей, вся операция откатывается.

yourFunctionName(params, idFieldName) {
try{
await this.transaction(async(trx) => { 
// Very important to pass trx object
/* Check to see if the value exists then do an update else create a new entry */
if(!isNew) {
   await trx.update(params).into(this.tableName).where(idFieldName,                                  params.id);
  logger.info('Update Operation Successful');
} else {
  await trx.insert(params).into(this.tableName);
  logger.info('Insert Operation Successful');
}
}).then(() => {
  logger.info('Transaction Complete');
});
}catch((err) => {           // Catch Error if something fails
  logger.error(err);
});

В приведенной выше операции, если какая-либо из операций завершается сбоем, вся транзакция откатывается к исходному состоянию.

Просто чтобы дать вам представление о том, что за кулисами (вам не нужно выполнять какие-либо настройки в кодовой базе ниже, логика предназначена только для дополнительных знаний), вся операция фиксации и отката происходит в транзакции. js, который Knex предоставляет нам из коробки. Внутри файла все операции происходят внутри контейнера, который имеет две основные операции.

  1. совершить
  2. откат

После того, как все операции выполнены успешно и результат имеет тип «функция», выполняется еще откат.

async _evaluateContainer(config,container) {
// logic for acquiring connection to DB
// logic for forming transactor
/* If we've returned a "thenable" from the trasnaction container, assume the rollback and commit are chained to this object's success / failure. Directly thrown errors are treated as automatic rollbacks. */
let result;
try{
   result = container(transactor);
} catch(err) {
   result = Promise.reject(err);
}
  if(result && result.then && typeof result.then === 'function'){
   result.then((val) => {
    return transactor.commit(val);
})
  .catch((err) => {
     return transactor.rollback(err);
});
}

Не забывайте trx

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

Как я уже сказал, когда мы забываем trx, мы обязательно будем использовать как минимум два подключения к базе данных: одно для транзакции и одно для запроса, забытого вне транзакции. Имейте эти настройки в своем проекте:

const knex = require('knex')({
client: 'postgres',
connection: {
host: '127.0.0.1',
user: 'your_database_user',
password: 'your_database_password',
database: 'myapp_test',
},
pool: { min: 1, max: 1 },
})

Приведенный выше параметр конфигурации Knex также поможет вам выполнить миграцию в базе данных.

Помимо этого, обязательно запустите свои модульные тесты, это отличный способ обнаружить упущения. Если вы забудете, тест не закончится и попадет в тайм-аут.

Я надеюсь, что это поможет, благодаря этим методам Knex наша команда теперь может без проблем обновлять нашу БД с помощью больших объектов. Надеюсь это поможет.