IndexedDB – стандарт хранения больших объемов структурированных данных на клиенте – был ожидаем также как и WebSocket (ну может самую малость меньше). В свете выхода FireFox 4 я нашёл время и силы всё-таки разобраться, как им пользоваться, и попытаться написать что-то больше, чем пример с адресной книгой, гуляющий по интернетам (в процессе поиска информации у меня сложилось впечатление, что это был единственный пример).

Несколько вводных слов


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

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

На чём будем тренироваться


Так случилось, что я в последнее время занимаюсь созданием чатиков. Вот и будем строить чатик, со следующими требованиями:
  • все сообщения чата хранятся локально, от сервера принимаются не более 100 последних сообщений;
  • также локально хранятся пользовательские настройки, в нашем случае — имя пользователя.

Поехали


Первым делом необходимо проверить, есть ли поддержка IndexedDB в браузере. Делается это так:
if ("webkitIndexedDB" in window){
 var idb=window.webkitIndexedDB;
} else if ("mozIndexedDB" in window) {
 var idb=window.mozIndexedDB;
} else {
 //тут объясняем, что этот конь здесь не ходит или делаем что-то альтернативное и умное
};


Лирическое отступление:
Не могу понять, зачем разработчики браузеров используют модификаторы движков для поддерживаемых функций. Даже если наполнение отличается друг от друга, определить, что за браузер ��ришёл (про параноиков речь не идёт), труда не составляет, а так приходится писать, в общем-то, лишний код.


Далее необходимо создать объект типа IDBRequest, который будет предоставлять асинхронный доступ к объектам базы данных. Синхронный доступ к IndexedDB в драфте спецификации присутствует, но пока не реализован.
var idbRequest=idb.open(dbName,dbDescription);
//dbName – имя базы данных, dbDescription – её описание (опционально)
//И навесим на него обработчики
idbRequest.onsuccess=function (e) {…};
idbRequest.onerror=function (e) {…};


Обработчки onerror:
Если верить спецификации, то в качестве аргумента должен прилететь объект типа IDBErrorEvent, имеющий два свойства – code и message. На практике, прилетает просто событие, к счастью имеющее в свойствах объект IDBRequest, из которого можно вытащить код ошибки и, в случае вебкита, собственно сообщение. Итоговый обработчик имеет следующую структуру:
function idbRequestError(err){
 idbRequest=err.target;
 //код ошибки idbRequest.errorCode
 //если webkit, описание ошибки idbRequest.webkitErrorMessage;
}


Вызвать эту ошибку довольно просто:
  • запретить сохранение локальных данных в настройках браузера;
  • долго думать над сообщением FireFox «Этот сайт пытается записать данные локально. Разрешить?»

Если подключение прошло успешно, можно продолжить и проверить, есть ли нужная база данных у пользователя и той ли она версии. Структура обработчика успешного подключения:
function idbRequestSuccess(e){
  var db=e.target.result;
  if (db.version===’’){
    //базы данных нет
  }else if (db.version!=’3.14’){
    //база данных не той версии
  } else {
    //всё хорошо
  };
}


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

Трансакции в IndexedDB


Драфт W3C определяет четыре типа трансакций: READ_ONLY, READ_WRITE, SNAPSHOT_READ и VERSION_CHANGED.

Лирическое отступление
Честно говоря, я не понял отличий READ_ONLY от SNAPSHOT_READ, видимо, разработчики браузеров тоже этот момент не осознали потому, она не реализована.


READ_ONLY – служит, как следует из названия, для чтения. Блокирует трансакции других типов.
READ_WRITE – служит для изменения данных, дожидается завершения всех конкурирующих трансакций над выбранным объектом, блокирует все прочие трансакции и выполняется.
VERSION_CHANGE – трансакция, которая дожидается завершения всех прочих трансакций, блокирует доступ к объектам данных для всех и выполняется. Только в этой трансакции можно создавать, удалять или изменять объекты данных.

Лирическое отступление
Все трансакции имеют числовые коды. По спецификации W3C READ_WRITE=0, READ_ONLY=1, SNAPSHOT_READ=2, VERSION_CHANGE=3. Писать конструкции типа “webkitIDBTransaction.READ_ONLY” мне было, конечно же, лень и я задавал трансакции кодами. То, что VERSION_CHANGE трансакция имеет код 2, я выяснил довольно быстро. Но выяснение того, что в FireFox READ_ONLY=0, а READ_WRITE=1 слоило мне многих закоротивших нервных клеток.


Создание объектов данных


Как уже было сказано, совершать манипуляции с объектами данных можно только из трансакции VERSION_CHANGE. Подключиться к ней мы можем из обработчика успешной смены версии.
var setVersion=db.setVersion('3.14');
setVersion.onsuccess=function (e) { 
  var db=e.target.transaction.db;
  //действия над объектами данных
};


Что можно сделать с объектами данных:
Создать – createObjectStore()
Удалить – deleteObjectStore()
Назначить трансакцию – transaction()

Разберемся с удалением


В качестве аргумента метод удаления принимает имя объекта данных, который следует удалить. Метод выполняется асинхронно, и по идее, должен возвращать объект типа IDBRequest, к которому можно прицепить обработчик onsuccess. Но ни Webkit, ни Mozilla не считают нужным что-либо вернуть. Работает метод, тем не менее, асинхронно и блокирует доступ конкурирующим методам. Потому использование конструкции
for (var i=0; i<db.objectStoreNames.length; i++){
 db.deleteObjectStore(db.objectStoreNames.length[i]);
};

приводит к непрогнозируемым результатам. Что-то удалиться, что-то нет. Узнать нормальным способом не получается. Структура костыля, в принципе, понятна, но я решил этим не заморачиваться, т.к. удалаять объекты нужно, по большому счету, только в случае изменения структуры базы.

Создание объектов


В примере, с адресной книгой, создавался всего один объект базы данных. Но, когда я решил создать два объекта, FireFox начала обкладывать меня матом на тему NON_TRANSIENT_ERR, что примерно означает: «не то, и не в той трансакции делаешь».

Изначально конструкция была следующей, и нормально работала в Chrome:
var setVersion=db.setVersion(dbVersion);
setVersion.onsuccess=idbCreateStore;
function idbCreateStore(e){
 //получим объект базы данных, ассоциированный с VERSION_CHANGE трансакцией
 var db=e.target.transaction.db;
 if (!db.objectStoreNames.contains('chat')){
  //объект для хранения записей из чата
  soChat=db.createObjectStore('chat', ‘id’);
  soChat.createIndex('itime','time');
 };
 if (!db.objectStoreNames.contains('iam')){
  //объект для хранения настроек пользователя
  soIam=db.createObjectStore('iam');
 };
}


После нескольких часов экспериментов, был создан хак и для ОгнеЛиса, который работает и в Chrome:
var setVersion=db.setVersion('4');
setVersion.onsuccess=idbCreateStore;
function idbCreateStore(e){
 //получим объект базы данных, ассоциированный с VERSION_CHANGE трансакцией
 var db=e.target.transaction.db;
 if (!db.objectStoreNames.contains('chat')){
  //объект для хранения записей из чата
  co=db.createObjectStore('chat',’id’);
  setVersion=db.setVersion('42')
  setVersion.onsuccess=idbCreateStore;
  return;
 };
 if (!db.objectStoreNames.contains('iam')){
  //объект для хранения настроек пользователя
  co=db.createObjectStore('iam');
 };
}


Как вы наверно заметили, перед создан��ем объекта идёт проверка, нет ли уже такого объекта в базе. Сделано это для того, чтобы не возиться с удалением объектов. Если попробовать создать объект, который уже присутствует в базе, то вывалится ошибка.

Опциональные аргументы создания объектов: имя ключа и флаг автоинкриментности (игнорируется браузерами).

Также из кода исчезло создание индексов: ОгнеЛис болезненно реагировал на создание индексов в трансакции смены версии.

К этому моменту база данных сформирована. Казалось бы, можно продолжить. Но это не так. Все вызовы выполняются асинхронно, потому нельзя утверждать однозначно, что все объекты были созданы. Опять-таки, если верить спецификации, в возвращаемом методом createObjectStore() объекте IDBObjectStore должно содержаться свойство IDBRequest, которому можно навесить обработчик onsuccess. Но похожего свойства в объекте нет. Потому, перед тем как запустить основной цикл работы необходимо дождаться завершения создания объектов БД, что делается следующим не хитрым кодом:
var idbObjectsWait=true;
while (idbObjectsWait){
 idbObjectsWait=!(db.objectStoreNames.contains('chat') && db.objectStoreNames.contains('iam'));
};

Наконец-то база создана, и в неё можно начать писать и из неё же читать, что записали.

Запись


Записывать данные можно двумя способами (и только из трансакции записи): add и put. Различия следующие: если в add передать ключ, который уже присутствует в объекте, значение не будет записано; put – заменяет данные. Операции опять же асинхронные.
var t=idb.transaction(['iam'], idbConst.WRITE);
var s=t.objectStore('iam');
s.put({'name':$('#name').val()},1);


Чтение


С чтением меня ждало самое большое разочарование. По идее к базе можно строить запросы используя интерфейс IDBKeyRange, но его поддержку ни в Хроме, ни в ОгнеЛисе я не обнаружил. Т.е. вся возня с индексами множится на ноль: запрашивать нечего. Собственно чтение, осуществляется весьма тривиально:
var t=idb.transaction(['chat'],idbConst.READ);
var s=t.objectStore('chat');
var r=s.openCursor();
r.onsuccess=function (e) {
 var idbEntry=e.target.result;
 if (idbEntry){
  //делаем, что нам нужно и читаем дальше
  idbEntry.continue();
 } else {
  //данные закончились
 };
};


Управлять начальным положением указателя не получится, но можно порулить направлением чтения, передав параметр в метод continue.

Заключение


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

Что посмотреть по теме


mikewest.org/2010/12/intro-to-indexeddb — очень хорошая презентация Майка Веста с тем самым примером адресной книги, которая хоть и содержит ряд неточностей (видимо, просто время прошло), очень хороша для начала разбирательств.
developer.mozilla.org/en/IndexedDB — документ разработки от Мозиллы.
www.w3.org/TR/IndexedDB — спецификация W3C.
www.netroxsc.ru/pub/chateg — пример для Chrome, проверенный в Chrome 11 и 12, и исходники проекта

UPD 2014-04-09. Обновлённая статья по IndexDB: Готовим IndexedDB