Pull to refresh

Готовим IndexedDB

Reading time7 min
Views77K


На Хабре уже рассказывали про 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);


Метод open возвращает объект IDBOpenDBRequest, в котором нас интересует три обработчика событий:
  • 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).

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

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

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

objectStore.createIndex("name", "name", { unique: false });


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


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

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

db.transaction(["myObjectStore"], "readonly");


Теперь, когда у нас есть открытая транзакция, мы можем получить наш ObjectStore у которого есть следующие методы для работы с записями:
  • 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.
Tags:
Hubs:
Total votes 28: ↑27 and ↓1+26
Comments8

Articles