Фон

Во время моей недавней разработки функции синхронизации данных с использованием метода bulkCreate в Sequelize я столкнулся с проблемой неверных возвращаемых идентификаторов при использовании параметра updateOnDuplicate: true. Это заставило меня заподозрить, что Sequelize генерирует возвращаемые идентификаторы посредством логического вывода. Чтобы подтвердить свою гипотезу, я решил исследовать исходный код Sequelize.

Как Sequelize генерирует возвращаемые идентификаторы в bulkCreate

Несмотря на обширную кодовую базу Sequelize, найти метод bulkCreate было несложно. Глобальный поиск в кодовой базе привел меня прямо к файлу src/model.js, где я нашел соответствующий комментарий, подтверждающий мое первоначальное предположение.

/*
  * The success handler is passed an array of instances, but please notice that these may not completely represent the state of the rows in the DB. This is because MySQL
  * and SQLite do not make it easy to obtain back automatically generated IDs and other default values in a way that can be mapped to multiple records.
  * To obtain Instances for the newly created values, you will need to query for them again.
  */

Затем я приступил к изучению этого метода, следуя приведенным ниже фрагментам кода:

// src/model.js
const results = await model.queryInterface.bulkInsert(model.getTableName(options), records, options, fieldMappedAttributes);

static get queryInterface() {
  return this.sequelize.getQueryInterface();
}
// src/sequelize.js
this.queryInterface = this.dialect.queryInterface;
// src/dialects/sqlite/index.js
// Just take SQLite as an example.
this.queryInterface = new SQLiteQueryInterface(
  sequelize,
  this.queryGenerator
);

// src/dialects/sqlite/query-interface.js
class SQLiteQueryInterface extends QueryInterface { 
  // ...
}
// src/dialects/abstract/query-generator.js
async bulkInsert(tableName, records, options, attributes) {
  options = { ...options };
  options.type = QueryTypes.INSERT;

  const results = await this.sequelize.query(
    this.queryGenerator.bulkInsertQuery(tableName, records, options, attributes),
    options
  );
  return results[0];
}
// src/sequelize.js
async query(sql, options) {
  // ...
  const query = new this.dialect.Query(connection, this, options);
  // ...
  return await query.run(sql, bindParameters);
  // ...
}
// src/dialects/sqlite/query.js
async run(sql, parameters) {
  // ...
  resolve(query._handleQueryResponse(this, columnTypes, executionError, results, errForStack.stack));
  // ...
}

И, наконец, я получил то, к чему стремился:

// src/dialects/sqlite/query.js
_handleQueryResponse(metaData, columnTypes, err, results, errStack) {
  // ...
  // add the inserted row id to the instance
  if (this.isInsertQuery(results, metaData) || this.isUpsertQuery()) {
    this.handleInsertQuery(results, metaData);
    if (!this.instance) {
      // handle bulkCreate AI primary key
      if (
        metaData.constructor.name === 'Statement'
        && this.model
        && this.model.autoIncrementAttribute
        && this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
        && this.model.rawAttributes[this.model.primaryKeyAttribute]
      ) {
        const startId = metaData[this.getInsertIdField()] - metaData.changes + 1;
        result = [];
        for (let i = startId; i < startId + metaData.changes; i++) {
          result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: i });
        }
      } else {
        result = metaData[this.getInsertIdField()];
      }
    }
  }
  // ...
}

При использовании метода _handleQueryResponse стало очевидно, что Sequelize использует последний вставленный идентификатор и выводит оставшиеся идентификаторы при использовании bulkCreate. Следовательно, при использовании опции updateOnDuplicate: true есть вероятность получения неверных идентификаторов в возврате.

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

Риск использования updateOnDuplicate: true

При использовании параметра updateOnDuplicate: true я заметил, что хотя повторяющиеся строки пропускаются, ключ autoIncrement продолжает увеличиваться. Из документации MySQL сказано:

Эффекты не совсем идентичны: для таблицы InnoDB, где a является столбцом с автоинкрементом, инструкция INSERT увеличивает значение автоинкремента, а UPDATE — нет.

База данных следует за процессом попытки вставки, увеличения идентификатора, а затем обнаруживается дубликат. Однако после автоматического увеличения идентификатора его нельзя будет откатить.

Эта ситуация представляет потенциальный риск при использовании архитектуры базы данных master-slave. Поскольку идентификатор автоматически увеличивается, но обнаруживается дубликат, операция не оставляет журналов бинов, существует вероятность возникновения конфликтов ключей, когда ведущий выходит из строя, а подчиненный становится новым ведущим.