Давайте сначала разберемся, что такое транзакция и когда необходимо использовать транзакцию?
Когда я работал над одним из своих проектов, я заметил при операции обновления, которая обновляла несколько таблиц, иногда операция завершалась ошибкой, но половина таблиц все равно обновлялась, потому что я выполнял операции промисов параллельно. Теперь, если ваша операция включает единственную таблицу, будет достаточно простой операции 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 предоставляет нам из коробки. Внутри файла все операции происходят внутри контейнера, который имеет две основные операции.
- совершить
- откат
После того, как все операции выполнены успешно и результат имеет тип «функция», выполняется еще откат.
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 наша команда теперь может без проблем обновлять нашу БД с помощью больших объектов. Надеюсь это поможет.