Переименование полей и преобразование данных

Переименование полей и преобразование данных в Dexie.js играет ключевую роль при эволюции структуры IndexedDB-базы без потери совместимости и пользовательских данных. В отличие от традиционных SQL-баз, IndexedDB не предоставляет встроенных механизмов миграции схемы, поэтому любые изменения структуры данных — включая изменение названий полей или формата хранения — реализуются на уровне приложения через версионирование базы и миграционные скрипты.

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

Ключевая особенность:

Dexie не модифицирует записи при изменении схемы — разработчик обязан явно описать миграцию.

Это означает, что переименование поля или изменение формата данных требует дополнительного шага трансформации, обычно через upgrade().


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

В IndexedDB данные хранятся как JavaScript-объекты, сериализованные по ключам. Если поле firstName было переименовано в name, база не понимает семантики этого изменения.

Пример исходной схемы:

db.version(1).stores({
  users: "++id, firstName, lastName"
});

Обновлённая версия:

db.version(2).stores({
  users: "++id, name, lastName"
});

После такого изменения:

  • поле firstName остаётся в старых записях
  • новое поле name отсутствует в старых данных
  • приложение получает разнородную структуру объектов

Без миграции возникает смешение форматов.


Использование upgrade() для миграции структуры

Механизм upgrade() позволяет выполнять произвольный JavaScript-код при обновлении версии базы. Это основной инструмент для переименования полей.

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

Здесь происходит:

  • чтение всех записей таблицы users
  • создание нового поля name
  • удаление устаревшего поля firstName

Важно: операция выполняется внутри транзакции, что обеспечивает атомарность миграции.


Особенности метода modify()

Метод modify() работает на уровне коллекции и позволяет изменять каждую запись без её полного извлечения и повторной записи вручную.

Поведение:

  • изменения применяются in-place
  • не требуется put() для каждой записи
  • поддерживается массовая обработка данных

Пример переименования нескольких полей:

db.version(3).upgrade(tx => {
  return tx.table("users").toCollection().modify(user => {
    user.fullName = `${user.name} ${user.lastName}`;
    delete user.name;
    delete user.lastName;
  });
});

Преобразование форматов данных

Помимо переименования, часто требуется изменение структуры данных. Это может включать:

  • преобразование строк в числа
  • нормализацию дат
  • изменение вложенных структур
  • переход от плоских объектов к вложенным

Пример преобразования даты

Исходная версия хранит дату как строку:

createdAt: "2025-01-10"

Новая версия требует Date:

db.version(2).upgrade(tx => {
  return tx.table("orders").toCollection().modify(order => {
    order.createdAt = new Date(order.createdAt);
  });
});

Преобразование вложенных структур

Иногда данные требуют реорганизации структуры объекта.

Исходная модель:

{
  id: 1,
  city: "Almaty",
  street: "Abay",
  house: "10"
}

Новая модель:

{
  id: 1,
  address: {
    city: "Almaty",
    street: "Abay",
    house: "10"
  }
}

Миграция:

db.version(2).upgrade(tx => {
  return tx.table("users").toCollection().modify(user => {
    user.address = {
      city: user.city,
      street: user.street,
      house: user.house
    };

    delete user.city;
    delete user.street;
    delete user.house;
  });
});

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

Dexie выполняет версии строго последовательно. Если существует цепочка:

  • version(1)
  • version(2)
  • version(3)

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

Это позволяет строить накопительные преобразования:

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

Каждый шаг работает только с изменениями предыдущей версии.


Ограничения миграций данных

При работе с переименованием и преобразованием следует учитывать ограничения IndexedDB:

1. Отсутствие серверных транзакций

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

2. Ограничения по времени выполнения

Большие коллекции могут вызывать блокировку UI при синхронной обработке через modify().

3. Неатомарность между версиями

Каждая версия мигрирует данные отдельно, но не существует глобальной транзакции между версиями.


Оптимизация массовых преобразований

Для больших таблиц часто используется поэтапная миграция через курсоры:

db.version(2).upgrade(async tx => {
  const table = tx.table("logs");

  await table.toCollection().modify(log => {
    if (log.level === "warn") {
      log.level = "warning";
    }
  });
});

В более сложных случаях можно обрабатывать данные порционно:

db.version(3).upgrade(async tx => {
  const chunkSize = 100;
  const table = tx.table("events");

  let lastId = 0;

  while (true) {
    const chunk = await table
      .where("id")
      .above(lastId)
      .limit(chunkSize)
      .toArray();

    if (chunk.length === 0) break;

    await Promise.all(chunk.map(item => {
      item.timestamp = Date.parse(item.timestamp);
      return table.put(item);
    }));

    lastId = chunk[chunk.length - 1].id;
  }
});

Совместимость старых и новых полей

В переходный период часто требуется поддержка двух форматов одновременно. Это позволяет избежать сбоев в приложении при частичном обновлении данных.

function getUserName(user) {
  return user.name || user.firstName;
}

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


Безопасное удаление старых полей

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

Типичный шаблон:

  1. Добавление нового поля
  2. Копирование данных
  3. Обновление логики приложения
  4. Удаление старого поля в следующей версии
db.version(3).upgrade(tx => {
  return tx.table("users").toCollection().modify(user => {
    if (user.firstName && !user.name) {
      user.name = user.firstName;
    }
  });
});
db.version(4).upgrade(tx => {
  return tx.table("users").toCollection().modify(user => {
    delete user.firstName;
  });
});

Роль индексов при преобразовании данных

При изменении структуры данных необходимо учитывать обновление индексов. Если поле участвует в индексе, его изменение требует пересоздания схемы через stores().

Пример:

db.version(2).stores({
  users: "++id, name, age"
});

Если ранее индекс был:

firstName

его замена на name означает:

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

Практика атомарного переименования

Наиболее безопасная стратегия состоит из трёх шагов:

  • добавление нового поля
  • миграция данных
  • удаление старого поля в отдельной версии

Это снижает риск потери данных и упрощает откат изменений на уровне версий IndexedDB.