Пользователь
0,0
рейтинг
14 апреля 2011 в 15:11

Разработка → IndexedDB: пробуем готовить

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
Alexander Kouznetsov @unconnected
карма
171,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (31)

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

    мб потому что это пока рабочий драфт, а не окончательный стандарт?
    • 0
      всё равно, выше моего понимания :)
      • +5
        Пока драфт сложно ожидать одинакового поведения от свойства в разных браузерах (тот же ваш пример с webkitIDBTransaction.READ_ONLY), а значит использование «стандартного» свойства будет потенциальным источником ошибок не в одном, так в другом браузере. А так будет стандарт, производители его реализуют, а их «эксперименты» (реализации каких-то версий черновиков с префиксом) останутся совместимыми с уже написанным кодом. Может так они думают :)
      • +1
        Там лишнего кода-то с гулькин нос. Те if'ы, которые у Вас, можно заменить на:
        var idb = window.mozIndexedDB || window.webkitIndexedDB || null;

        // То, что ниже, нужно все равно, даже если бы IndexedDB называлась одинаково
        // во всех браузерах
        if (idb) {
        // IndexedDB есть
        } else {
        // IndexedDB нет
        }
        • 0
          зачем or'ить еще и null?
          • +2
            Технически незачем, вопрос чистой эстетики: idb вроде как определяется, поэтому undefined мне нравится чуть меньше, чем явный null.
        • 0
          я говорил в целом, а не про данный конкретный случай: все вместе в боевом приложении уже набегает
        • 0
          «С гулькин нос» означает «мало».
          • 0
            А что, много что ли? Разница будет лишь в одной строке (первой), а остальное ровно то же, что и в случае единого именования indexedDB в разных браузерах.
      • 0
        Тут про вендорные префиксы в CSS, но думаб это поможет прояснить ситуацию: web-standards.ru/articles/prefix-or-posthack/
  • +1
    Занятная статья. Сам около 2 месяцев назад разбирался с IndexedDB, правда только в Google Chrome, ярких плюсов не заметил. Почему не взяли как БД WebDatabase? Вроде как она уже у всех есть и проблем с версиями нет. Есть конечно минус с тем, что развивать ее не будут, но работать в ближайших версиях должна же у всех.
    • 0
      это эксперимент, первым решил проверить indexedDB именно потому, что WebDatabase будет умирать
      • +2
        Вряд ли будет. Недавно я спрашивал одного из разработчиков Chrome про этот момент, он ответил, что умирать он не будет точно. Вопрос сейчас стоит скорее в развитии, потому что невозможно развивать что-либо без альтернатив.

        WebDatabase мне лично немного привычнее был из-за использования SQL, хоть уже год на нем почти ничего не делал. Логику IndexedDB до сих пор до конца не понятна.
        • 0
          мне тоже, как адепту реляционных баз данных и любителю SQL, башню слегка сносило, но в общем-то логика вполне прозрачная
          правда не рабочая :)
        • 0
          WebDatabase умирать будет, про это было несколько хороших статей. Основной аргумент — браузер не должен зависеть от развития SQL в SQLite и версии SQLite, встроенной в браузер, а должен иметь свой язык, зависящий от стандарта.
  • +1
    Видимо, пока что толку от нее еще меньше, чем от WebSQL.
    Но я так и не понял, в чем преимущества этой спецификации перед WebSQL (кроме идеологических, которые крайне сомнительны, т.к. IndexedDB был предложен Oracle).
    • 0
      имхо, только идеология
      изящества типа хотелось :)
      я перевод делал как раз на эту тему habrahabr.ru/blogs/firefox/115393/
      • +1
        Да, я читал ваш перевод. Но ни из него, ни из оригинала так и не смог проникнуться «красотой» IndexedDB:) Мне кажется, что это какие-то политичеcкие игры.

        NoSQL — это здорово, конечно, но ведь это не «no SQL at all», это «not only SQL». Одно key-value хранилище у веб-разработчиков уже есть; вполне возможно, мне кажется, написать над ним обертку, эмулирующую IndexedDB.

        Тем не менее, большое вам спасибо за этот пост — я начал понимать, как с ней работать.
  • +7
    Все хорошо. Кроме транСакций. Transaction, но транЗакция. Прошу прощения, но сильно режет глаз такое написание.
    • 0
      оба написания допустимы, в моих учебниках была «трансакция» — дело исключительно привычки
      • +1
        Применительно к базам данных, в русском языке прочно укоренилось написание «транзакция». В википедии (не знаю, правда, на сколько вы ей доверяете), на странице Транзакция явно присутствует предупреждение «Не следует путать с трансакция». Вас, конечно, здесь все поняли. Это просто замечание.
        А если не секрет, в каких ваших учебниках употреблялось «трансакция»?
        • –1
          разработка приложений баз данных в Delphi 3.0 :)
          хотя, сейчас дошло, почему так написал, только что осилил «Игры, в которые играют люди» — там именно трансакционный анализ
          если верить вики: «В банковской и экономической литературе обычно (но не всегда), используется написание ТранСакция, а в информатике преимущественно ТранЗакция.»
          • 0
            Да уже и в банковской сфере давно все говорят и пишут «транЗакция»
            • 0
              рад за банковскую сферу :)
  • 0
    if (db.version!=’3.14’){
    //база данных не той версии
    }


    Число Пи?

    Это случайность или какая то пасхалка?

    • 0
      там еще 42 есть :)
  • 0
    Проверьте, работает ли оно в IE10 (+ прототип IndexedDB): html5labs.interoperabilitybridges.com/html5labs/prototypes/indexeddb/indexeddb/info/
  • 0
    Пост устарел.

    developer.mozilla.org/en-US/docs/IndexedDB/Using_IndexedDB
    This tutorial is based on an old version of the specification and does not work on up-to-date browsers — it still uses the removed setVersion() method.
    • 0
      а на дату посмотреть не судьба?
      • 0
        И что? Например посты про XSLT не устаревают никогда :)
  • +1
    Освежил информацию об IndexedDB на Хабре.

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