Indexeddb что это за папка
Перейти к содержимому

Indexeddb что это за папка

  • автор:

Что такое IndexedDB

Что такое IndexedDB? Давайте разберемся в этом вопросе.

Первое, что нужно понимать — это то, что IndexedDB — это просто база данных, которая встроена в некоторые браузеры, в первую очередь это браузер Google Chrome. Но, в других браузера тоже есть аналоги такой базы данных.

IndexedDB — это noSQL база данных. Мы не можем выполнять SQL запросы к этой базе данных. Делать какие-то выборки с помощью запросов SQL синтаксиса. Общаться с этой базой данных можно с помощью так называемого API интерфейса, который есть у этой базы данных.

Взаимодействие осуществляется через выполнение определенных команд. Добавлять туда что-то, удалять или обновлять. IndexedDB — это объектно-ориентированная база данных. Это очень похоже на документо-ориентированные базы данных.

Подробнее посмотреть о том, что это такое можно здесь:

В таких базах данных информация храниться в виде документов. Документы представлены в виде объектов. Объекты — обычные JSON объекты. С которыми мы можем как-то взаимодействовать.

Объекты — это обычные JSON объекты, которые содержат информацию в виде ключа + значение.

Объекты могут иметь сложную вложенную структуру.

Если у нас храниться там такая информация, в такой базе данных не может быть relation (связей) — это не реляционная база данных. Здесь также нет таблиц, которые есть в реляционных базах данных. Таких как MySQL, PostgreSQL и.т.д.

Здесь есть только объекты, с которыми мы можем взаимодействовать.

В IndexedDB можно хранить строки, числа, даты, объекты и даже файлы.

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

Что касается объема хранения данных, которые мы можем хранить в этой базе данных, то здесь объем практически не ограничен и мы можем хранить миллионы записей, без проблем. Это рассчитывается по сложной формуле, но примерно это 50% от свободного места на диске.

Но, есть у этой базы данных и недостатки. Одним из таких недостатков в том, что данные хранятся на стороне клиента. Т.е. данные хранятся в том браузере, в котором вы работаете.

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

Соответственно, сфера применения таких баз данных несколько ограничена и, как правило, применяется для development разработки. Т.е. если вы не собираетесь публиковать свое приложение в production т.е. на каком-то сервере, чтобы он был доступен в Интернет, то для среды разработки вы можете вполне использовать эту базу данных.

Т.е. быстро создать какое-то приложение, которое будет работать у вас в браузере.

Еще одна сфера применения — это разработка расширений для браузера.

См. видео выше, чтобы посмотреть как получить доступ к IndexedDB внутри браузера.

Готовим IndexedDB

На Хабре уже рассказывали про IndexedDB — стандарт хранения больших структурированных данных на клиенте. Но это было давно и API сильно изменился. Несмотря на это в поиске статья всплывает одной из первых и вводит в заблуждение многих, кто начинает пытатся работать с этой технологией. Поэтому я решил написать новую статью с информацией об актуальном API.

Что такое IndexedDB

IndexedDB — это объектная база данных, которая намного мощнее, эффективнее и надежней, чем веб-хранилище пар ключ/значение, доступное посредством прикладного интерфейса Web Storage. Как и в случае прикладных интерфейсов к веб-хранилищам и файловой системе, доступность базы данных определяется происхождением создавшего ее документа.

Для каждого происхождения может быть создано произвольное число баз данных IndexedDB. Каждая база данных имеет имя, которое должно быть уникальным для данного происхождения. С точки зрения прикладного интерфейса IndexedDB база данных является простой коллекцией именованных хранилищ объектов. Каждый объект должен иметь ключ, под которым он сохраняется и извлекается из хранилища. Ключи должны быть уникальными и они должны иметь естественный порядок следования, чтобы их можно было сортировать. IndexedDB может автоматически генерировать уникальные ключи для каждого объекта, добавляемого в базу данных. Однако, часто объекты, сохраняемые в хранилище объектов, уже будут иметь свойство, пригодное для использования в качестве ключа. В этом случае при создании хранилища объектов достаточно просто определить ключевое свойство.

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

IndexedDB гарантирует атомарность операций: операции чтения и записи в базу данных объединяются в транзакции, благодаря чему либо они все будут успешно выполнены, либо ни одна из них не будет выполнена, и база данных никогда не останется в неопределенном, частично измененном состоянии.

Приступим

IE > 9, Firefox > 15 и Chrome > 23 поддерживают работу без префиксов, но все-таки лучше проверять все варианты:

var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; 

Подключение к базе данных

Работа с базой данных начинается с запроса на открытие:

var request = indexedDB.open("myBase", 1); 
  • onerror;
  • onsuccess;
  • onupgradeneeded.

Onerror будет вызван в случае возникновения ошибки и получит в параметрах объект ошибки.

Onsuccess будет вызван если все прошло успешно, но экземпляр открытой базы данных в качестве параметра метод не получит. Открытая БД доступна из объекта запроса: request.result.

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

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

function connectDB(f)< var request = indexedDB.open("myBase", 1); request.onerror = function(err)< console.log(err); >; request.onsuccess = function() < // При успешном открытии вызвали коллбэк передав ему объект БД f(request.result); >request.onupgradeneeded = function(e)< // Если БД еще не существует, то создаем хранилище объектов. e.currentTarget.result.createObjectStore("myObjectStore", < keyPath: "key" >); connectDB(f); > > 

где f — это функция, которой будет передана открытая база данных.

Структура базы данных

IndexedDB оперирует не таблицами, а хранилищами объектов: ObjectStore. При создании ObjectStore можно указывать его имя и параметры: имя ключевого поля (строковое свойство объекта настроек: keyPath) и автогенерацию ключа (булево свойство объекта настроек: autoIncrement).

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

Создавать ObjectStore можно с помощью метода createObjectStore. При создании ObjectStore можно указать его имя и параметры, например, ключевое поле. Индекс базы данных можно создавать с помощью метода createIndex. При создании индекса можно указать его имя, поле по которому его необходимо построить, и параметры, например, уникальность ключа:

objectStore.createIndex("name", "name", < unique: false >); 

Работа с записями

Как уже говорилось во введении, любые операции с записями в IndexedDB происходят в рамках транзакции. Транзакция открывается методом transaction. В методе необходимо указать какие ObjectStore вам нужны и режим доступа: чтение, чтение и запись, смена версии. Режим смены версии по сути аналогичен методу onupgradeneeded.

Конкретные цифры не замерял, но думаю с точки зрения производительности лучше внимательно подходить к выставлению параметров транзакции: открывать только нужные вам ObjectStore и не просить запись, когда вам достаточно только чтения.

db.transaction(["myObjectStore"], "readonly"); 
  • add — добавляет строго новую запись, если попытаться добавить запись с уже существующим ключом, то получим ошибку;
  • put — перезаписывает или создает новую запись по указанному ключу;
  • get — возвращает запись по ключу;
  • delete — удаляет запись по указанному ключу.
Курсор

Метод get удобно использовать, если вы знаете ключ по которому хотите получить данные. Если вы хотите пройти через все записи в ObjectStore, то можно воспользоваться курсором:

var customers = []; objectStore.openCursor().onsuccess = function(event) < var cursor = event.target.result; if (cursor) < customers.push(cursor.value); cursor.continue(); >else < alert("Got all customers: " + customers); >>; 

Но ребятам из Mozilla, как и мне, такой способ получения всех записей показался неудобным и они сделали метод который сразу возвращает все содержимое ObjectStore: mozGetAll. Надеюсь, в будущем и остальные браузеры его реализуют.

Индекс

Если вы хотите получить значение используя индекс, то все тоже довольно просто:

var index = objectStore.index("name"); index.get("Donna").onsuccess = function(event) < alert("Donna's SSN is " + event.target.result.ssn); >; 

Ограничения

Размер

По размеру ограничений почти что нет. Firefox ограничивает только размерами жесткого диска, но при условии, что на каждые дополнительные 50 мегабайт потребуется подтверждение пользователя. Chrome может занять под базы данных всех веб-страниц, которые их создали, половину жесткого диска, при этом ограничивая каждую базу данных 20% от этой половины.

Поддержка браузерами

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

Пример

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

var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB, IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction, baseName = "filesBase", storeName = "filesStore"; function logerr(err) < console.log(err); >function connectDB(f) < var request = indexedDB.open(baseName, 1); request.onerror = logerr; request.onsuccess = function()< f(request.result); >request.onupgradeneeded = function(e)< e.currentTarget.result.createObjectStore(storeName, < keyPath: "path" >); connectDB(f); > > function getFile(file, f) < connectDB(function(db)< var request = db.transaction([storeName], "readonly").objectStore(storeName).get(file); request.onerror = logerr; request.onsuccess = function()< f(request.result ? request.result : -1); >>); > function getStorage(f)< connectDB(function(db)< var rows = [], store = db.transaction([storeName], "readonly").objectStore(storeName); if(store.mozGetAll) store.mozGetAll().onsuccess = function(e)< f(e.target.result); >; else store.openCursor().onsuccess = function(e) < var cursor = e.target.result; if(cursor)< rows.push(cursor.value); cursor.continue(); >else < f(rows); >>; >); > function setFile(file) < connectDB(function(db)< var request = db.transaction([storeName], "readwrite").objectStore(storeName).put(file); request.onerror = logerr; request.onsuccess = function()< return request.result; >>); > function delFile(file) < connectDB(function(db)< var request = db.transaction([storeName], "readwrite").objectStore(storeName).delete(file); request.onerror = logerr; request.onsuccess = function()< console.log("File delete from DB:", file); >>); > 

Заключение

IndexedDB уже в полной мере поддерживается браузерами и готово к употреблению. Это прекрасный инструмент для создания автономных веб-приложений, но использовать его нужно все-таки с умом. Где можно обойтись WebStorage — лучше обойтись WebStorage. Где можно ничего не хранить на клиенте, лучше ничего не хранить на клиенте.

Сейчас становится все больше библиотек, которые инкапсулируют внутри себя работу с WebStorage, FileSystem API, IndexedDB и WebSQL, но, по-моему, лучше написать хотя бы раз свой код, чтобы потом не тащить, когда не нужно, кучу чужого кода без понимания его работы.

Больше информации на MDN.

  • javascript
  • indexeddb
  • html5 templates
  • Веб-разработка
  • JavaScript
  • Программирование

IndexedDB

IndexedDB – это встроенная база данных, более мощная, чем localStorage .

  • Хранит практически любые значения по ключам, несколько типов ключей
  • Поддерживает транзакции для надёжности.
  • Поддерживает запросы в диапазоне ключей и индексы.
  • Позволяет хранить больше данных, чем localStorage .

Для традиционных клиент-серверных приложений эта мощность обычно чрезмерна. IndexedDB предназначена для оффлайн приложений, можно совмещать с ServiceWorkers и другими технологиями.

Интерфейс для IndexedDB, описанный в спецификации https://www.w3.org/TR/IndexedDB, основан на событиях.

Мы также можем использовать async/await с помощью обёртки, которая основана на промисах, например https://github.com/jakearchibald/idb. Это очень удобно, но обёртка не идеальна, она не может полностью заменить события. Поэтому мы начнём с событий, а затем, когда разберёмся в IndexedDB, рассмотрим и обёртку.

Где хранятся данные?

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

У разных браузеров и пользователей на уровне ОС есть своё собственное независимое хранилище.

Открыть базу данных

Для начала работы с IndexedDB нужно открыть базу данных.

let openRequest = indexedDB.open(name, version);
  • name – название базы данных, строка.
  • version – версия базы данных, положительное целое число, по умолчанию 1 (объясняется ниже).

У нас может быть множество баз данных с различными именами, но все они существуют в контексте текущего источника (домен/протокол/порт). Разные сайты не могут получить доступ к базам данных друг друга.

После этого вызова необходимо назначить обработчик событий для объекта openRequest :

  • success : база данных готова к работе, готов «объект базы данных» openRequest.result , его следует использовать для дальнейших вызовов.
  • error : не удалось открыть базу данных.
  • upgradeneeded : база открыта, но её схема устарела (см. ниже).

IndexedDB имеет встроенный механизм «версионирования схемы», который отсутствует в серверных базах данных.

В отличие от серверных баз данных, IndexedDB работает на стороне клиента, в браузере, и у нас нет прямого доступа к данным. Но когда мы публикуем новую версию нашего приложения, возможно, нам понадобится обновить базу данных.

Если локальная версия базы данных меньше, чем версия, определённая в open , то сработает специальное событие upgradeneeded , и мы сможем сравнить версии и обновить структуры данных по мере необходимости.

Это событие также сработает, если базы данных ещё не существует, так что в этом обработчике мы можем выполнить инициализацию.

Допустим, мы опубликовали первую версию нашего приложения.

Затем мы можем открыть базу данных с версией 1 и выполнить инициализацию в обработчике upgradeneeded вот так:

let openRequest = indexedDB.open("store", 1); openRequest.onupgradeneeded = function() < // срабатывает, если на клиенте нет базы данных // . выполнить инициализацию. >; openRequest.onerror = function() < console.error("Error", openRequest.error); >; openRequest.onsuccess = function() < let db = openRequest.result; // продолжить работу с базой данных, используя объект db >;

Затем, позже, мы публикуем 2-ю версию.

Мы можем открыть его с версией 2 и выполнить обновление следующим образом:

let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = function(event) < // версия существующей базы данных меньше 2 (или база данных не существует) let db = openRequest.result; switch(event.oldVersion) < // существующая (старая) версия базы данных case 0: // версия 0 означает, что на клиенте нет базы данных // выполнить инициализацию case 1: // на клиенте версия базы данных 1 // обновить >>;

Таким образом, в openRequest.onupgradeneeded мы обновляем базу данных. Скоро подробно увидим, как это делается. А после того, как этот обработчик завершится без ошибок, сработает openRequest.onsuccess .

После openRequest.onsuccess у нас есть объект базы данных в openRequest.result , который мы будем использовать для дальнейших операций.

Удалить базу данных:

let deleteRequest = indexedDB.deleteDatabase(name) // deleteRequest.onsuccess/onerror отслеживает результат

А что, если открыть предыдущую версию?

Что если мы попробуем открыть базу с более низкой версией, чем текущая? Например, на клиенте база версии 3, а мы вызываем open(. 2) .

Возникнет ошибка, сработает openRequest.onerror .

Такое может произойти, если посетитель загрузил устаревший код, например, из кеша прокси. Нам следует проверить db.version и предложить ему перезагрузить страницу. А также проверить наши кеширующие заголовки, убедиться, что посетитель никогда не получит устаревший код.

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

Раз уж мы говорим про версионирование, рассмотрим связанную с этим небольшую проблему.

  1. Посетитель открыл наш сайт во вкладке браузера, с базой версии 1 .
  2. Затем мы выпустили обновление, так что наш код обновился.
  3. И затем тот же посетитель открыл наш сайт в другой вкладке.

Так что есть две вкладки, на которых открыт наш сайт, но в одной открыто соединение с базой версии 1, а другая пытается обновить версию базы в обработчике upgradeneeded .

Проблема заключается в том, что база данных всего одна на две вкладки, так как это один и тот же сайт, один источник. И она не может быть одновременно версии 1 и 2. Чтобы обновить на версию 2, все соединения к версии 1 должны быть закрыты.

Чтобы это можно было организовать, при попытке обновления на объекте базы возникает событие versionchange . Нам нужно слушать его и закрыть соединение к базе (а также, возможно, предложить пользователю перезагрузить страницу, чтобы получить обновлённый код).

Если мы его не закроем, то второе, новое соединение будет заблокировано с событием blocked вместо success .

Код, который это делает:

let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = . ; openRequest.onerror = . ; openRequest.onsuccess = function() < let db = openRequest.result; db.onversionchange = function() < db.close(); alert("База данных устарела, пожалуйста, перезагрузите страницу.") >; // . база данных готова, используйте ее. >; openRequest.onblocked = function() < // это событие не должно срабатывать, если мы правильно обрабатываем onversionchange // это означает, что есть ещё одно открытое соединение с той же базой данных // и он не был закрыт после того, как для него сработал db.onversionchange >;

…Другими словами, здесь мы делаем две вещи:

  1. Обработчик db.onversionchange сообщает нам о попытке параллельного обновления, если текущая версия базы данных устарела.
  2. Обработчик OpenRequest.onblocked сообщает нам об обратной ситуации: в другом месте есть соединение с устаревшей версией, и оно не закрывается, поэтому новое соединение установить невозможно.

Мы можем более изящно обращаться с вещами в db.onversionchange , например предлагать посетителю сохранить данные до закрытия соединения и так далее.

Или альтернативным подходом было бы не закрывать базу данных в db.onversionchange , а вместо этого использовать обработчик onblocked (на новой вкладке), чтобы предупредить посетителя, что более новая версия не может быть загружена, пока они не закроют другие вкладки.

Такой конфликт при обновлении происходит редко, но мы должны как-то его обрабатывать, хотя бы поставить обработчик onblocked , чтобы наш скрипт не «умирал» молча, удивляя посетителя.

Хранилище объектов

Чтобы сохранить что-то в IndexedDB, нам нужно хранилище объектов.

Хранилище объектов – это основная концепция IndexedDB. В других базах данных это «таблицы» или «коллекции». Здесь хранятся данные. В базе данных может быть множество хранилищ: одно для пользователей, другое для товаров и так далее.

Несмотря на то, что название – «хранилище объектов», примитивы тоже могут там храниться.

Мы можем хранить почти любое значение, в том числе сложные объекты.

IndexedDB использует стандартный алгоритм сериализации для клонирования и хранения объекта. Это как JSON.stringify , но более мощный, способный хранить гораздо больше типов данных.

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

Каждому значению в хранилище должен соответствовать уникальный ключ.

Ключ должен быть одним из следующих типов: number, date, string, binary или array. Это уникальный идентификатор: по ключу мы можем искать/удалять/обновлять значения.

Как мы видим, можно указать ключ при добавлении значения в хранилище, аналогично localStorage . Но когда мы храним объекты, IndexedDB позволяет установить свойство объекта в качестве ключа, что гораздо удобнее. Или мы можем автоматически сгенерировать ключи.

Но для начала нужно создать хранилище.

Синтаксис для создания хранилища объектов:

db.createObjectStore(name[, keyOptions]);

Обратите внимание, что операция является синхронной, использование await не требуется.

  • name – это название хранилища, например «books» для книг,
  • keyOptions – это необязательный объект с одним или двумя свойствами:
    • keyPath – путь к свойству объекта, которое IndexedDB будет использовать в качестве ключа, например id .
    • autoIncrement – если true , то ключ будет формироваться автоматически для новых объектов, как постоянно увеличивающееся число.

    Если при создании хранилища не указать keyOptions , то нам потребуется явно указать ключ позже, при сохранении объекта.

    Например, это хранилище объектов использует свойство id как ключ:

    db.createObjectStore('books', );

    Хранилище объектов можно создавать/изменять только при обновлении версии базы данных в обработчике upgradeneeded .

    Это техническое ограничение. Вне обработчика мы сможем добавлять/удалять/обновлять данные, но хранилища объектов могут быть созданы/удалены/изменены только во время обновления версии базы данных.

    Для обновления версии базы есть два основных подхода:

    1. Мы можем реализовать функции обновления по версиям: с 1 на 2, с 2 на 3 и т.д. Потом в upgradeneeded сравнить версии (например, была 2, сейчас 4) и запустить операции обновления для каждой промежуточной версии (2 на 3, затем 3 на 4).
    2. Или мы можем взять список существующих хранилищ объектов, используя db.objectStoreNames . Этот объект является DOMStringList, в нём есть метод contains(name) , используя который можно проверить существование хранилища. Посмотреть, какие хранилища есть и создать те, которых нет.

    Для простых баз данных второй подход может быть проще и предпочтительнее.

    Вот демонстрация второго способа:

    let openRequest = indexedDB.open("db", 2); // создаём хранилище объектов для books, если ешё не существует openRequest.onupgradeneeded = function() < let db = openRequest.result; if (!db.objectStoreNames.contains('books')) < // если хранилище "books" не существует db.createObjectStore('books', ); // создаём хранилище > >;

    Чтобы удалить хранилище объектов:

    db.deleteObjectStore('books')

    Транзакции

    Термин «транзакция» является общеизвестным, транзакции используются во многих видах баз данных.

    Транзакция – это группа операций, которые должны быть или все выполнены, или все не выполнены (всё или ничего).

    Например, когда пользователь что-то покупает, нам нужно:

    1. Вычесть деньги с его счёта.
    2. Отправить ему покупку.

    Будет очень плохо, если мы успеем завершить первую операцию, а затем что-то пойдёт не так, например отключат электричество, и мы не сможем завершить вторую операцию. Обе операции должны быть успешно завершены (покупка сделана, отлично!) или необходимо отменить обе операции (в этом случае пользователь сохранит свои деньги и может попытаться купить ещё раз).

    Транзакции гарантируют это.

    Все операции с данными в IndexedDB могут быть сделаны только внутри транзакций.

    Для начала транзакции:

    db.transaction(store[, type]);
    • store – это название хранилища, к которому транзакция получит доступ, например, «books» . Может быть массивом названий, если нам нужно предоставить доступ к нескольким хранилищам.
    • type – тип транзакции, один из:
      • readonly – только чтение, по умолчанию.
      • readwrite – только чтение и запись данных, создание/удаление самих хранилищ объектов недоступно.

      Есть ещё один тип транзакций: versionchange . Такие транзакции могут делать любые операции, но мы не можем создать их вручную. IndexedDB автоматически создаёт транзакцию типа versionchange , когда открывает базу данных, для обработчика upgradeneeded . Вот почему это единственное место, где мы можем обновлять структуру базы данных, создавать/удалять хранилища объектов.

      Почему существует несколько типов транзакций?

      Производительность является причиной, почему транзакции необходимо помечать как readonly или readwrite .

      Несколько readonly транзакций могут одновременно работать с одним и тем же хранилищем объектов, а readwrite транзакций – не могут. Транзакции типа readwrite «блокируют» хранилище для записи. Следующая такая транзакция должна дождаться выполнения предыдущей, перед тем как получит доступ к тому же самому хранилищу.

      После того, как транзакция будет создана, мы можем добавить элемент в хранилище, вот так:

      let transaction = db.transaction("books", "readwrite"); // (1) // получить хранилище объектов для работы с ним let books = transaction.objectStore("books"); // (2) let book = < id: 'js', price: 10, created: new Date() >; let request = books.add(book); // (3) request.onsuccess = function() < // (4) console.log("Книга добавлена в хранилище", request.result); >; request.onerror = function() < console.log("Ошибка", request.error); >;

      Мы сделали четыре шага:

      1. Создать транзакцию и указать все хранилища, к которым необходим доступ, строка (1) .
      2. Получить хранилище объектов, используя transaction.objectStore(name) , строка (2) .
      3. Выполнить запрос на добавление элемента в хранилище объектов books.add(book) , строка (3) .
      4. …Обработать результат запроса (4) , затем мы можем выполнить другие запросы и так далее.

      Хранилища объектов поддерживают два метода для добавления значений:

      • put(value, [key]) Добавляет значение value в хранилище. Ключ key необходимо указать, если при создании хранилища объектов не было указано свойство keyPath или autoIncrement . Если уже есть значение с таким же ключом, то оно будет заменено.
      • add(value, [key]) То же, что put , но если уже существует значение с таким ключом, то запрос не выполнится, будет сгенерирована ошибка с названием «ConstraintError» .

      Аналогично открытию базы, мы отправляем запрос: books.add(book) и после ожидаем события success/error .

      • request.result для add является ключом нового объекта.
      • Ошибка находится в request.error (если есть).

      Автоматическая фиксация транзакций

      В примере выше мы запустили транзакцию и выполнили запрос add . Но, как говорилось ранее, транзакция может включать в себя несколько запросов, которые все вместе должны либо успешно завершиться, либо нет. Как нам закончить транзакцию, обозначить, что больше запросов в ней не будет?

      Короткий ответ: этого не требуется.

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

      Когда все запросы завершены и очередь микрозадач пуста, тогда транзакция завершится автоматически.

      Как правило, это означает, что транзакция автоматически завершается, когда выполнились все её запросы и завершился текущий код.

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

      Такое автозавершение транзакций имеет важный побочный эффект. Мы не можем вставить асинхронную операцию, такую как fetch или setTimeout в середину транзакции. IndexedDB никак не заставит транзакцию «висеть» и ждать их выполнения.

      В приведённом ниже коде в запросе request2 в строке с (*) будет ошибка, потому что транзакция уже завершена, больше нельзя выполнить в ней запрос:

      let request1 = books.add(book); request1.onsuccess = function() < fetch('/').then(response =>< let request2 = books.add(anotherBook); // (*) request2.onerror = function() < console.log(request2.error.name); // TransactionInactiveError >; >); >;

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

      Авторы спецификации IndexedDB из соображений производительности считают, что транзакции должны завершаться быстро.

      В частности, readwrite транзакции «блокируют» хранилища от записи. Таким образом, если одна часть приложения инициирует readwrite транзакцию в хранилище объектов books , то другая часть приложения, которая хочет сделать то же самое, должна ждать: новая транзакция «зависает» до завершения первой. Это может привести к странным задержкам, если транзакции слишком долго выполняются.

      В приведённом выше примере мы могли бы запустить новую транзакцию db.transaction перед новым запросом (*) .

      Но ещё лучше выполнять операции вместе, в рамках одной транзакции: отделить транзакции IndexedDB от других асинхронных операций.

      Сначала сделаем fetch , подготовим данные, если нужно, затем создадим транзакцию и выполним все запросы к базе данных.

      Чтобы поймать момент успешного выполнения, мы можем повесить обработчик на событие transaction.oncomplete :

      let transaction = db.transaction("books", "readwrite"); // . выполнить операции. transaction.oncomplete = function() < console.log("Транзакция выполнена"); >;

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

      Чтобы вручную отменить транзакцию, выполните:

      transaction.abort();

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

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

      Запросы на запись могут выполниться неудачно.

      Мы должны быть готовы к этому, не только из-за возможных ошибок на нашей стороне, но и по причинам, которые не связаны с транзакцией. Например, размер хранилища может быть превышен. И мы должны быть готовы обработать такую ситуацию.

      При ошибке в запросе соответствующая транзакция отменяется полностью, включая изменения, сделанные другими её запросами.

      Если мы хотим продолжить транзакцию (например, попробовать другой запрос без отмены изменений), это также возможно. Для этого в обработчике request.onerror следует вызвать event.preventDefault() .

      В примере ниже новая книга добавляется с тем же ключом ( id ), что и существующая. Метод store.add генерирует в этом случае ошибку «ConstraintError» . Мы обрабатываем её без отмены транзакции:

      let transaction = db.transaction("books", "readwrite"); let book = < id: 'js', price: 10 >; let request = transaction.objectStore("books").add(book); request.onerror = function(event) < // ConstraintError возникает при попытке добавить объект с ключом, который уже существует if (request.error.name == "ConstraintError") < console.log("Книга с таким id уже существует"); // обрабатываем ошибку event.preventDefault(); // предотвращаем отмену транзакции // . можно попробовать использовать другой ключ. >else < // неизвестная ошибка // транзакция будет отменена >>; transaction.onabort = function() < console.log("Ошибка", transaction.error); >;

      Делегирование событий

      Нужны ли обработчики onerror/onsuccess для каждого запроса? Не всегда. Мы можем использовать делегирование событий.

      События IndexedDB всплывают: запрос → транзакция → база данных .

      Все события являются DOM-событиями с фазами перехвата и всплытия, но обычно используется только всплытие.

      Поэтому мы можем перехватить все ошибки, используя обработчик db.onerror , для оповещения пользователя или других целей:

      db.onerror = function(event) < let request = event.target; // запрос, в котором произошла ошибка console.log("Ошибка", request.error); >;

      …А если мы полностью обработали ошибку? В этом случае мы не хотим сообщать об этом.

      Мы можем остановить всплытие и, следовательно, db.onerror , используя event.stopPropagation() в request.onerror .

      request.onerror = function(event) < if (request.error.name == "ConstraintError") < console.log("Книга с таким id уже существует"); // обрабатываем ошибку event.preventDefault(); // предотвращаем отмену транзакции event.stopPropagation(); // предотвращаем всплытие ошибки >else < // ничего не делаем // транзакция будет отменена // мы можем обработать ошибку в transaction.onabort >>;

      Поиск по ключам

      Есть два основных вида поиска в хранилище объектов:

      1. По значению ключа или диапазону ключей. В нашем хранилище «books» это будет значение или диапазон значений book.id .
      2. С помощью другого поля объекта, например book.price . Для этого потребовалась дополнительная структура данных, получившая название «index».

      По ключу

      Сначала давайте разберёмся с первым типом поиска: по ключу.

      Методы поиска поддерживают либо точные ключи, либо так называемые «запросы с диапазоном» – IDBKeyRange объекты, которые задают «диапазон ключей».

      Диапазоны создаются с помощью следующих вызовов:

      • IDBKeyRange.lowerBound(lower, [open]) означает: >lower (или ≥lower , если open это true)
      • IDBKeyRange.upperBound(upper, [open]) означает:
      • IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen]) означает: между lower и upper , включительно, если соответствующий open равен true .
      • IDBKeyRange.only(key) – диапазон, который состоит только из одного ключа key , редко используется.

      Очень скоро мы увидим практические примеры их использования.

      Для выполнения фактического поиска существуют следующие методы. Они принимают аргумент query , который может быть либо точным ключом, либо диапазоном ключей:

      • store.get(query) – поиск первого значения по ключу или по диапазону.
      • store.getAll([query], [count]) – поиск всех значений, можно ограничить, передав count .
      • store.getKey(query) – поиск первого ключа, который удовлетворяет запросу, обычно передаётся диапазон.
      • store.getAllKeys([query], [count]) – поиск всех ключей, которые удовлетворяют запросу, обычно передаётся диапазон, возможно ограничить поиск, передав count .
      • store.count([query]) – получить общее количество ключей, которые удовлетворяют запросу, обычно передаётся диапазон.

      Например, в хранилище у нас есть множество книг. Помните, поле id является ключом, поэтому все эти методы могут искать по ключу id .

      // получить одну книгу books.get('js') // получить книги с 'css' 'js' books.getAllKeys(IDBKeyRange.lowerBound('js', true))

      Хранилище объектов всегда отсортировано

      Хранилище объектов внутренне сортирует значения по ключам.

      Поэтому запросы, которые возвращают много значений, всегда возвращают их в порядке сортировки по ключу.

      Поиск по индексированному полю

      Для поиска по другим полям объекта нам нужно создать дополнительную структуру данных, называемую «индекс» (index).

      Индекс является «расширением» к хранилищу, которое отслеживает данное поле объекта. Для каждого значения этого поля хранится список ключей для объектов, которые имеют это значение. Ниже будет более подробная картина.

      objectStore.createIndex(name, keyPath, [options]);
      • name – название индекса,
      • keyPath – путь к полю объекта, которое индекс должен отслеживать (мы собираемся сделать поиск по этому полю),
      • option – необязательный объект со свойствами:
        • unique – если true, тогда в хранилище может быть только один объект с заданным значением в keyPath . Если мы попытаемся добавить дубликат, то индекс сгенерирует ошибку.
        • multiEntry – используется только, если keyPath является массивом. В этом случае, по умолчанию, индекс обрабатывает весь массив как ключ. Но если мы укажем true в multiEntry , тогда индекс будет хранить список объектов хранилища для каждого значения в этом массиве. Таким образом, элементы массива становятся ключами индекса.

        В нашем примере мы храним книги с ключом id .

        Допустим, мы хотим сделать поиск по полю price .

        Сначала нам нужно создать индекс. Индексы должны создаваться в upgradeneeded , как и хранилище объектов:

        openRequest.onupgradeneeded = function() < // мы должны создать индекс здесь, в versionchange транзакции let books = db.createObjectStore('books', ); let index = books.createIndex('price_idx', 'price'); >;
        • Индекс будет отслеживать поле price .
        • Поле price не уникальное, у нас может быть несколько книг с одинаковой ценой, поэтому мы не устанавливаем опцию unique .
        • Поле price не является массивом, поэтому флаг multiEntry не применим.

        Представим, что в нашем books есть 4 книги. Вот картинка, которая показывает, что такое «индекс».

        Как уже говорилось, индекс для каждого значения price (второй аргумент) хранит список ключей, имеющих эту цену.

        Индексы автоматически обновляются, нам не нужно об этом заботиться.

        Сейчас, когда мы хотим найти объект по цене, мы просто применяем те же методы поиска к индексу:

        let transaction = db.transaction("books"); // readonly let books = transaction.objectStore("books"); let priceIndex = books.index("price_idx"); let request = priceIndex.getAll(10); request.onsuccess = function() < if (request.result !== undefined) < console.log("Книги", request.result); // массив книг с ценой 10 >else < console.log("Нет таких книг"); >>;

        Мы также можем использовать IDBKeyRange , чтобы создать диапазон и найти дешёвые/дорогие книги:

        // найдём книги, где цена < 5 let request = priceIndex.getAll(IDBKeyRange.upperBound(5));

        Индексы внутренне отсортированы по полю отслеживаемого объекта, в нашем случае по price . Поэтому результат поиска будет уже отсортированный по полю price .

        Удаление из хранилища

        Метод delete удаляет значения по запросу, формат вызова такой же как в getAll :

        • delete(query) – производит удаление соответствующих запросу значений.
        // удалить книгу с books.delete('js');

        Если нам нужно удалить книги, основываясь на цене или на любом другом поле, сначала нам надо найти ключ в индексе, а затем выполнить delete :

        // найдём ключ, где цена = 5 let request = priceIndex.getKey(5); request.onsuccess = function() < let let deleteRequest = books.delete(id); >;

        Чтобы удалить всё:

        books.clear(); // очищаем хранилище.

        Курсоры

        Такие методы как getAll/getAllKeys возвращают массив ключей/значений.

        Но хранилище объектов может быть огромным, больше, чем доступно памяти. Тогда метод getAll вернёт ошибку при попытке получить все записи в массиве.

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

        Объект cursor идёт по хранилищу объектов с заданным запросом (query) и возвращает пары ключ/значение по очереди, а не все сразу. Это позволяет экономить память.

        Так как хранилище объектов внутренне отсортировано по ключу, курсор проходит по хранилищу в порядке хранения ключей (по возрастанию по умолчанию).

        // как getAll, но с использованием курсора: let request = store.openCursor([query], [direction]); // чтобы получить ключи, не значения (как getAllKeys): store.openKeyCursor
        • query ключ или диапазон ключей, как для getAll .
        • direction необязательный аргумент, доступные значения:
          • "next" – по умолчанию, курсор будет проходить от самого маленького ключа к большему.
          • "prev" – обратный порядок: от самого большого ключа к меньшему.
          • "nextunique" , "prevunique" – то же самое, но курсор пропускает записи с тем же ключом, что уже был (только для курсоров по индексам, например, для нескольких книг с price=5, будет возвращена только первая).

          Основным отличием курсора является то, что request.onsuccess генерируется многократно: один раз для каждого результата.

          Вот пример того, как использовать курсор:

          let transaction = db.transaction("books"); let books = transaction.objectStore("books"); let request = books.openCursor(); // вызывается для каждой найденной курсором книги request.onsuccess = function() < let cursor = request.result; if (cursor) < let key = cursor.key; // ключ книги (поле id) let value = cursor.value; // объект книги console.log(key, value); cursor.continue(); >else < console.log("Книг больше нет"); >>;

          Основные методы курсора:

          • advance(count) – продвинуть курсор на count позиций, пропустив значения.
          • continue([key]) – продвинуть курсор к следующему значению в диапазоне соответствия (или до позиции сразу после ключа key, если указан).

          Независимо от того, есть ли ещё значения, соответствующие курсору или нет – вызывается onsuccess , затем в result мы можем получить курсор, указывающий на следующую запись или равный undefined .

          В приведённом выше примере курсор был создан для хранилища объектов.

          Но мы также можем создать курсор для индексов. Как мы помним, индексы позволяют искать по полю объекта. Курсоры для индексов работают так же, как для хранилищ объектов – они позволяют экономить память, возвращая одно значение в единицу времени.

          Для курсоров по индексам cursor.key является ключом индекса (например price), нам следует использовать свойство cursor.primaryKey как ключ объекта:

          let request = priceIdx.openCursor(IDBKeyRange.upperBound(5)); // вызывается для каждой записи request.onsuccess = function() < let cursor = request.result; if (cursor) < let key = cursor.primaryKey; // следующий ключ в хранилище объектов (поле id) let value = cursor.value; // следующее значение в хранилище объектов (объект "книга") let keyIndex = cursor.key; // следующий ключ индекса (price) console.log(key, value); cursor.continue(); >else < console.log("Книг больше нет"); >>;

          Обёртка для промисов

          Добавлять к каждому запросу onsuccess/onerror немного громоздко. Мы можем сделать нашу жизнь проще, используя делегирование событий, например, установить обработчики на все транзакции, но использовать async/await намного удобнее.

          Давайте далее в главе использовать небольшую обёртку над промисами https://github.com/jakearchibald/idb. Она создаёт глобальный idb объект с промисифицированными IndexedDB методами.

          Тогда вместо onsuccess/onerror мы можем писать примерно так:

          let db = await idb.openDb('store', 1, db => < if (db.oldVersion == 0) < // выполняем инициализацию db.createObjectStore('books', ); > >); let transaction = db.transaction('books', 'readwrite'); let books = transaction.objectStore('books'); try < await books.add(. ); await books.add(. ); await transaction.complete; console.log('сохранено'); >catch(err)

          Теперь у нас красивый «плоский асинхронный» код и, конечно, будет работать try..catch .

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

          Если мы не перехватим ошибку, то она «вывалится» наружу, вверх по стеку вызовов, до ближайшего внешнего try..catch .

          Необработанная ошибка становится событием «unhandled promise rejection» в объекте window .

          Мы можем обработать такие ошибки вот так:

          window.addEventListener('unhandledrejection', event => < let request = event.target; // объект запроса IndexedDB let error = event.reason; // Необработанный объект ошибки, как request.error . сообщить об ошибке. >);

          Подводный камень: «Inactive transaction»

          Как мы уже знаем, транзакции автоматически завершаются, как только браузер завершает работу с текущим кодом и макрозадачу. Поэтому, если мы поместим макрозадачу наподобие fetch в середину транзакции, транзакция не будет ожидать её завершения. Произойдёт автозавершение транзакции. Поэтому при следующем запросе возникнет ошибка.

          Для промисифицирующей обёртки и async/await поведение такое же.

          Вот пример fetch в середине транзакции:

          let transaction = db.transaction("inventory", "readwrite"); let inventory = transaction.objectStore("inventory"); await inventory.add(< id: 'js', price: 10, created: new Date() >); await fetch(. ); // (*) await inventory.add(< id: 'js', price: 10, created: new Date() >); // Ошибка

          Следующий inventory.add после fetch (*) не сработает, сгенерируется ошибка «inactive transaction», потому что транзакция уже завершена и закрыта к этому времени.

          Решение такое же, как при работе с обычным IndexedDB: либо создать новую транзакцию, либо разделить задачу на части.

          1. Подготовить данные и получить всё, что необходимо.
          2. Затем сохранить в базу данных.

          Получение встроенных объектов

          Внутренне обёртка выполняет встроенные IndexedDB запросы, добавляя к ним onerror/onsuccess , и возвращает промисы, которые отклоняются или выполняются с переданным результатом.

          Это работает в большинстве случаев. Примеры можно увидеть на странице библиотеки https://github.com/jakearchibald/idb.

          В некоторых редких случаях, когда нам нужен оригинальный объект request , мы можем получить к нему доступ, используя свойство promise.request :

          let promise = books.add(book); // получаем промис (без await, не ждём результата) let request = promise.request; // встроенный объект запроса let transaction = request.transaction; // встроенный объект транзакции // . работаем с IndexedDB. let result = await promise; // если ещё нужно

          Итого

          IndexedDB можно рассматривать как «localStorage на стероидах». Это простая база данных типа ключ-значение, достаточно мощная для оффлайн приложений, но простая в использовании.

          Лучшим руководством является спецификация, текущая версия 2.0, но также поддерживаются несколько методов из 3.0 (не так много отличий) версии.

          Использование можно описать в нескольких фразах:

          1. Подключить обёртку над промисами, например idb.
          2. Открыть базу данных: idb.openDb(name, version, onupgradeneeded)
            • Создайте хранилища объектов и индексы в обработчике onupgradeneeded или выполните обновление версии, если это необходимо
          3. Для запросов:
            • Создать транзакцию db.transaction('books') (можно указать readwrite, если надо).
            • Получить хранилище объектов transaction.objectStore('books') .
          4. Затем для поиска по ключу вызываем методы непосредственно у хранилища объектов.
            • Для поиска по любому полю объекта создайте индекс.
          5. Если данные не помещаются в памяти, то используйте курсор.

          IndexedDB

          IndexedDB — низкоуровневое API для клиентского хранилища большого объёма структурированных данных, включая файлы/blobs. Эти API используют индексы для обеспечения высоко-производительного поиска данных. Если DOM Storage (en-US) полезен для хранения небольшого количества данных, он менее выгоден для большого числа структурированных данных. IndexedDB предоставляет решение. Это основная страница на MDN, покрывающая IndexedDB — здесь мы предоставляем ссылки к полному списку API и руководствам по использованию, детали поддержки браузерами и некоторые объяснения ключевых концепций.

          Примечание: Эта возможность доступна в Web Workers

          Примечание: IndexedDB API мощные, но могут казаться слишком сложными для простых задач. Если вы предпочитаете простые API, попробуйте библиотеки, такие как localForage, dexie.js и ZangoDB, делающие IndexedDB более дружественным.

          Ключевые концепции и использование

          IndexedDB транзакционная система базы данных, как SQL-основанная RDBMS. Однако, в отличие от RDBMS, которая использует таблицы с фиксированными колонками, IndexedDB — JavaScript-основанная объектно-ориентированная база данных. IndexedDB позволяет сохранять и возвращать объекты, которые были проиндексированы с ключом; любой объект, поддерживаемый структурированным алгоритмом клонирования (en-US) может быть сохранён. Необходимо описать схему базы данных, установить соединение с ней и затем получить и обновить данные за несколько транзакций.

          • Читайте больше о Концепции IndexedDB (en-US).
          • Изучите асинхронное использование IndexedDB по первоначальным принципам с руководством Using IndexedDB.
          • Найдите рекомендации по разработке, чтобы заставить ваш сайт работать вне сети, на странице Offline Apps.

          Примечание: Как и большинство решений web-хранения, IndexedDB следует аналогичной same-origin policy. Поэтому вы имеете доступ к хранилищу данных в пределах одного домена, но не можете получать их с любого другого.

          Синхронность и асинхронность

          Выполнение операций использующих IndexedDB происходит асинхронно, т. е. не блокирует приложение. IndexedDB первоначально включал синхронные и асинхронные API. Синхронные API предназначались только для работы с Web Workers (en-US) , но были удалены из спецификации, потому что было неясно, нужны ли они. Однако, синхронные API могут быть повторно введены, если появится спрос со стороны веб разработчиков.

          Ограничения памяти и критерии освобождения

          Существует несколько веб-технологий, которые хранят данные того или иного вида на стороне клиента (т.е. на вашем локальном диске). Под IndexedDB чаще всего подразумевают одно. Процесс, в котором браузер вычисляет сколько места нужно выделить для хранения веб-данных. Ограничение памяти браузера и критерии освобождения пытаются объяснить как это работает, по крайней мере в случае с Firefox.

          Интерфейсы

          Чтобы получить доступ к базе данных, вызовите метод open() (en-US) y атрибута indexedDB (en-US) объекта window (en-US) . Этот метод возвращает объект IDBRequest (en-US) ; асинхронные операции связываются с вызывающим приложением, вызывая события объекта IDBRequest (en-US) .

          Подключение к базе данных

          Предоставляет доступ к функциям IndexedDB. Реализовано объектами window и worker .

          Предоставляет доступ к базе данных. Этот интерфейс представлен глобальным объектом indexedDB (en-US) . Он является точкой входа для API.

          Представляет запрос на открытие базы данных.

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

          Получение и изменение данных

          Представляет транзакцию. Вы создаёте транзакцию в базе данных, указываете область действия (например, к каким хранилищам объектов вы хотите получить доступ) и определяете тип доступа (только чтение или чтение/запись), который вам нужен.

          Generic interface that handles database requests and provides access to results.

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

          Позволяет получить доступ к подмножеству данных в IndexedDB, но вместо первичного ключа использует индекс для извлечения записи (записей). Иногда это быстрее, чем использование IDBObjectStore (en-US).

          Итерирует по хранилищам объектов и индексам.

          Итерирует по хранилищам объектов и индексам и возвращает текущее значение курсора.

          Определяет диапазон ключей, который можно использовать для извлечения данных из базы данных в определённом диапазоне.

          Определяет диапазон ключей, который можно использовать для извлечения данных из базы данных в определённом диапазоне, отсортированных в соответствии с правилами локали, указанной для определённого индекса (см. createIndex() 's optionalParameters (en-US) .). Этот интерфейс не входит в спецификацию 2.0.

          Пользовательские интерфейсы событий

          Эта спецификация запускает события со следующим настраиваемым интерфейсом:

          Интерфейс IDBVersionChangeEvent указывает, что версия базы данных изменилась в результате функции обработчика событий IDBOpenDBRequest.onupgradeneeded (en-US).

          Устаревшие интерфейсы

          Ранняя версия спецификации также определяла эти теперь удалённые интерфейсы. Они все ещё задокументированы на тот случай, если вам понадобится обновить ранее написанный код:

          Представляет запрос на изменение версии базы данных. С тех пор способ изменения версии базы данных изменился (путём вызова IDBFactory.open (en-US) без вызова IDBDatabase.setVersion ), а интерфейс IDBOpenDBRequest (en-US) теперь имеет функциональность удалённого IDBVersionChangeRequest .

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *