Метод version().upgrade()

Метод version().upgrade() в Dexie.js предназначен для выполнения миграционного кода при переходе между версиями базы данных. Он позволяет явно определить логику преобразования данных, когда изменяется схема, индексы или структура хранимых объектов.

Каждое изменение версии базы данных в Dexie сопровождается потенциальной несовместимостью старых данных с новой схемой. Описание схемы через version(n).stores({...}) фиксирует структуру таблиц, но не решает задачу преобразования уже существующих записей.

Метод upgrade() закрывает этот разрыв, предоставляя возможность:

  • модифицировать существующие записи;
  • удалять или добавлять поля;
  • перераспределять данные между хранилищами;
  • выполнять трансформации индексов косвенным образом через изменение данных;
  • обеспечивать консистентность при миграции.

Сигнатура и размещение в цепочке версий

Метод вызывается в цепочке описания версии:

db.version(2)
  .stores({
    users: "++id,name,age"
  })
  .upgrade(tx => {
    // миграционная логика
  });

Общая форма:

db.version(n)
  .stores(schema)
  .upgrade(callback);

Где callback получает доступ к транзакции текущей версии.

Контекст выполнения upgrade()

Функция, передаваемая в upgrade(), выполняется:

  • один раз при переходе на указанную версию;
  • в рамках транзакции;
  • после применения новой схемы;
  • до того, как база становится доступной приложению.

Контекст выполнения содержит доступ ко всем таблицам текущей версии через db.tableName.

Пример доступа к таблице:

db.version(2)
  .stores({
    users: "++id,name,fullName"
  })
  .upgrade(tx => {
    return tx.table("users").toCollection().modify(user => {
      user.fullName = user.name;
      delete user.name;
    });
  });

Поведение при изменении схемы

При обновлении версии происходит последовательность шагов:

  1. Открывается IndexedDB транзакция.
  2. Применяется новая схема stores().
  3. Выполняется upgrade().
  4. После успешного завершения транзакции база становится доступной.

Если upgrade() выбрасывает исключение или возвращает отклонённый Promise, вся миграция откатывается.

Работа с данными внутри upgrade()

Основной сценарий — модификация существующих записей через коллекции.

Модификация записей

db.version(2)
  .stores({
    users: "++id,firstName,lastName,fullName"
  })
  .upgrade(tx => {
    return tx.table("users").toCollection().modify(user => {
      user.fullName = `${user.firstName} ${user.lastName}`;
    });
  });

Метод modify() изменяет записи пакетно, что важно для больших таблиц.

Удаление полей

db.version(2)
  .stores({
    users: "++id,name"
  })
  .upgrade(tx => {
    return tx.table("users").toCollection().modify(user => {
      delete user.tempField;
    });
  });

Фильтрация и выборочная миграция

db.version(2)
  .stores({
    logs: "++id,type,timestamp"
  })
  .upgrade(tx => {
    return tx.table("logs")
      .where("type")
      .equals("debug")
      .delete();
  });

Асинхронное выполнение upgrade()

Функция upgrade() может возвращать Promise, что позволяет выполнять цепочки асинхронных операций:

db.version(2)
  .stores({
    users: "++id,name,normalized"
  })
  .upgrade(async tx => {
    const users = await tx.table("users").toArray();

    await tx.table("users").clear();

    await tx.table("users").bulkAdd(
      users.map(u => ({
        ...u,
        normalized: u.name.toLowerCase()
      }))
    );
  });

При использовании async/await транзакция остаётся активной до завершения Promise.

Порядок выполнения нескольких миграций

При переходе сразу через несколько версий (например, с 1 на 4) Dexie выполняет все промежуточные upgrade() последовательно:

  • version(2).upgrade()
  • version(3).upgrade()
  • version(4).upgrade()

Каждый шаг получает собственный транзакционный контекст своей версии.

Ограничения транзакции

Внутри upgrade() действуют ограничения IndexedDB:

  • невозможен доступ к другим базам данных;
  • нельзя открывать параллельные транзакции на те же таблицы;
  • все операции должны завершиться до окончания текущего upgrade-процесса.

Типовые сценарии миграций

Переименование поля

db.version(2)
  .stores({
    users: "++id,first_name,last_name"
  })
  .upgrade(tx => {
    return tx.table("users").toCollection().modify(user => {
      user.first_name = user.firstName;
      user.last_name = user.lastName;

      delete user.firstName;
      delete user.lastName;
    });
  });

Введение нового индекса через преобразование данных

db.version(2)
  .stores({
    users: "++id,email,emailDomain"
  })
  .upgrade(tx => {
    return tx.table("users").toCollection().modify(user => {
      user.emailDomain = user.email.split("@")[1];
    });
  });

Нормализация структуры данных

db.version(2)
  .stores({
    orders: "++id,itemsCount"
  })
  .upgrade(tx => {
    return tx.table("orders").toCollection().modify(order => {
      order.itemsCount = order.items.length;
    });
  });

Ошибки и откаты

Если миграция прерывается:

  • изменения схемы не фиксируются;
  • данные остаются в прежнем состоянии;
  • последующие версии не применяются.

Типичные причины ошибок:

  • обращение к несуществующей таблице;
  • нарушение структуры данных;
  • некорректная работа асинхронного кода;
  • превышение лимитов транзакции.

Особенности производительности

При работе с большими наборами данных критично учитывать:

  • toCollection().modify() эффективнее по памяти, чем ручная итерация;
  • bulkAdd() быстрее последовательных add();
  • удаление полей внутри modify() дешевле полной пересборки таблицы;
  • минимизация числа проходов по данным снижает время миграции.

Организация сложных миграций

При значительном изменении структуры данных предпочтительно:

  • разделять логику по версиям;
  • избегать перекрывающихся трансформаций;
  • хранить миграции атомарными;
  • не смешивать схему и данные в одной операции без необходимости.