Книга: JavaScript. Подробное руководство, 6-е издание

22.8. Базы данных на стороне клиента

22.8. Базы данных на стороне клиента

Архитектура веб-приложений традиционно была основана на поддержке HTML, CSS и JavaScript на стороне клиента и базы данных на стороне сервера. Поэтому одним из самых удивительных прикладных интерфейсов, определяемых спецификацией HTML5, является поддержка баз данных на стороне клиента. Это не прикладные интерфейсы доступа к базам данных на стороне сервера, а действительно интерфейсы доступа к базам данных, хранящимся на компьютере клиента и доступным непосредственно из программного кода на языке JavaScript, выполняемого броузером.

Прикладной интерфейс веб-хранилища, определяемый спецификацией «Web Storage» и описанный в разделе 20.1, можно расценивать как простейшую разновидность базы данных, хранящую пары ключ/значение. Но помимо него имеются еще два прикладных интерфейса доступа к клиентским базам данных, которые являются «настоящими» базами данных. Один из них известен как «Web SQL Database» - простая реляционная база данных, поддерживающая простейшие SQL-запросы. Этот прикладной интерфейс реализован в броузерах Chrome, Safari и Opera. Он не реализован в Firefox и IE и, скорее всего, никогда не будет реализован в них. Работа над официальной спецификацией этого прикладного интерфейса была остановлена, и поддержка полноценной базы данных SQL, вероятно, никогда не приобретет статус официального стандарта или неофициальной, но широко поддерживаемой особенности веб-платформы.

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

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

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

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

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

Концепция прикладного интерфейса IndexedDB чрезвычайно проста. Чтобы прочитать или изменить данные, сначала необходимо открыть требуемую базу данных (указав ее имя). Затем создать объект транзакции и с помощью этого объекта отыскать требуемое хранилище объектов в базе данных, также по имени. Наконец, отыскать объект вызовом метода get() хранилища объектов или сохранить новый объект вызовом метода put(). (Или вызвать метод add(), если необходимо избежать затирания существующих объектов.) Если потребуется отыскать объекты по диапазону ключей, нужно создать объект IDBRange и передать его методу openCursor() хранилища объектов. Или, если потребуется выполнить запрос по вторичному ключу, отыскать именованный индекс в хранилище объектов и затем вызвать метод get() или openCursor() объекта-индекса.

Однако эта концептуальная простота осложняется тем фактом, что прикладной интерфейс должен быть асинхронным, чтобы веб-приложения могли пользоваться им, не блокируя основной поток выполнения броузера, управляющий пользовательским интерфейсом. (Спецификация IndexedDB определяет синхронную версию прикладного интерфейса для использования в фоновых потоках выполнения, но на момент написания этих строк ни один броузер еще не реализовал эту версию, поэтому она не рассматривается здесь.) Создание транзакции, а также поиск хранилища объектов и индексов являются простыми синхронными операциями. Но открытие базы данных, обновление хранилища объектов с помощью метода put() и получение хранилища или индекса с помощью метода get() или openCursor() являются асинхронными операциями. Все эти асинхронные методы немедленно возвращают объект запроса. В случае успешного или неудачного выполнения запроса броузер генерирует событие «success» или «error» в объекте запроса, которые можно обработать, определив обработчики событий с помощью свойств onsuccess и onerror. В обработчике onsuccess результат операции доступен в виде свойства result объекта запроса.

Одно из удобств этого асинхронного прикладного интерфейса заключается в простоте управления транзакциями. При типичном использовании прикладного интерфейса IndexedDB сначала открывается база данных. Это асинхронная операция, поэтому по ее выполнении вызывается обработчик onsuccess. В этом обработчике создается объект транзакции, и затем этот объект используется для поиска хранилища или хранилищ объектов, которые предполагается использовать. После этого производится серия вызовов методов get() и put() хранилищ объектов. Они также действуют асинхронно, поэтому непосредственно при их вызове ничего не происходит, но запросы, сгенерированные этими методами get() и put(), автоматически будут связаны с объектом транзакции. При необходимости можно отменить все операции в транзакции, ожидающие выполнения, и откатить любые уже выполненные операции вызовом метода abort() объекта транзакции. Во многих других прикладных интерфейсах к базам данных объект транзакции обычно имеет метод commit(), подтверждающий транзакцию. Однако в IndexedDB транзакция подтверждается после выхода из обработчика onsuccess, создавшего транзакцию, когда броузер вернется в цикл обработки событий, и после выполнения всех операций, запрошенных в транзакции (без запуска новых операций в их функциях обратного вызова). Такая схема, на первый взгляд, кажется слишком сложной, но в практическом применении она очень проста. При использовании прикладного интерфейса IndexedDB программист вынужден создавать объекты транзакций, чтобы получить доступ к хранилищам объектов, но в обычных ситуациях ему даже не приходится задумываться о транзакциях.

Наконец, существует один особый вид транзакций, обеспечивающий возможность работы очень важной части прикладного интерфейса IndexedDB. Создать новую базу данных с использованием интерфейса IndexedDB API очень просто: достаточно выбрать имя и запросить открытие этой базы данных. Но новая база данных создается абсолютно пустой, и она совершенно бесполезна, пока в нее не будет добавлено одно или более хранилищ объектов (и, возможно, нескольких индексов). Создавать хранилища объектов и индексы можно только внутри обработчика события onsuccess объекта запроса, возвращаемого методом setVersion() объекта базы данных. Метод setVersion() позволяет указать номер версии базы данных - в обычной ситуации номер версии должен изменяться при каждом изменении структуры базы данных. Однако более важно, что метод setVersion() неявно запускает специальную транзакцию, позволяющую вызвать метод сreateObjectStore() объекта базы данных и метод createlndex() хранилища объектов.

Теперь, получив представление о прикладном интерфейсе IndexedDB, вы сможете самостоятельно разобраться в примере 22.15. Этот пример использует IndexedDB для создания базы данных, отображающей почтовые индексы США в названия городов, и выполнения запросов к ней. Он демонстрирует многие, хотя и не все, основные особенности IndexedDB. На момент написания этих строк пример действовал в Firefox 4 и Chrome 11, но из-за того, что спецификация все еще продолжала меняться и реализации находились на предварительной стадии разработки, велика вероятность, что он не будет работать именно так, как описывается здесь, когда вы будете читать эти строки. Однако общая структура примера должна сохранить свою полезность для вас. Пример 22.15 получился достаточно длинным, но в нем имеется большое количество комментариев, которые облегчат его изучение.

Пример 22.15. База данных IndexedDB с почтовыми индексами США

<!DOCTYPE html>
<html>
<head>
<title>Zipcode Database</title>
<script>
// Реализации IndexedDB все еще используют префиксы в именах
var indexedDB = window.indexedDB || // Использовать стандартный API БД
window.mozIndexedDB || // Или раннюю версию в Firefox
window.webkitlndexedDB; // Или раннюю версию в Chrome
// В Firefox не используются префиксы для следующих двух объектов:
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
// Эта функция будет использоваться для вывода сообщений об ошибках
function logerr(e) {
  console.log("Ошибка IndexedDB " + e.code + ": " + e.message);
}
// Эта функция асинхронно получает объект базы данных (при необходимости
// создает и инициализирует базу данных) и передает его функции f().
function withDB(f) {
  var request = indexedDB.open("zipcodes"); // Открыть базу данных zipcode
  request.onerгог = logerr; // Выводить сообщения об ошибках
  request.onsuccess = function() { // Или вызвать эту функцию по завершении
    var db = request.resu.lt; // Результатом запроса является база данных
    // Базу данных можно открыть всегда, даже если она не существует.
    // Мы проверяем версию, чтобы узнать, была ли БД создана и инициализирована.
    // Если нет - это необходимо сделать. Но если БД уже настроена,
    // остается просто передать ее функции f().
    if (db.version === "1") f(db); // Если БД существует, передать ее f()
    else initdb(db,f); // Иначе сначала инициализировать ее
  }
}
// Принимает почтовый индекс, отыскивает город, которому он принадлежит,
// и асинхронно передает название города указанной функции,
function lookupCity(zip, callback) {
  withDB(function(db) {
    // Создать объект транзакции для этого запроса
    var transaction = db.transaction(["zipcodes"],  // Требуемое хранилище
                    IDBTransaction.READ_0NLY,       // He обновлять
                    0);                             // Время ожидания не ограничено
    // Получить хранилище объектов из транзакции
    var objects = transaction.objectStoreC'zipcodes");
    // Запросить объект, соответствующий указанному индексу.
    // Строки выше выполнялись синхронно, но эта выполняется асинхронно
    var request = objects.get(zip);
    request.onerror = logerr; // Выводить сообщения об ошибках
    request.onsuccess = function() { // Передать результаты этой функции
                    // Искомый объект сейчас в свойстве request.result
      var object = request.result;
      if (object) // Если cootb. найдено, передать город и штат функции
        callback(object.city + ", " + object.state);
      else // Иначе сообщить о неудаче
        callback("Неизвестный индекс");
    }
  });
}
// По указанному названию города отыскивает все почтовые индексы для всех
// городов (в любом штате) с этим названием (с учетом регистра символов).
// Асинхронно передает результаты по одному указанной функции
function lookupZipcodes(city, callback) {
  withDB(function(db) {
    // Как и выше, создаем транзакцию и получаем хранилище объектов
    var transaction = db.transaction(["zipcodes"], IDBTransaction.READ_0NLY, 0);
    var store = transaction.objectStoreC'zipcodes");
    // На этот раз нам требуется получить индекс по названиям городов
    var index = store.index(”cities");
    // Этот запрос может вернуть несколько результатов, поэтому, чтобы
    // получить их все, следует использовать объект курсора. Чтобы создать
    // курсор, нужно создать объект диапазона, представляющий диапазон ключей
    var range = new IDBKeyRange.only(city); // Диапазон с одним ключом
    // Все, что выше, выполняется синхронно.
    // Теперь нужно запросить курсор, который возвращается асинхронно,
    var request = index.openCursor(range); // Запросить курсор
    request.onerror = logerr;              // Сообщать об ошибках
    request.onsuccess = function() { // Передать курсор этой функции
      // Этот обработчик событий будет вызван несколько раз, по одному
      // для каждой записи, соответствующей запросу, и еще один раз
      // с пустым курсором, указывающим на окончание результатов,
      var cursor = request.result         // Курсор в свойстве
      request.result if (!cursor) return; // Нет курсора = нет результатов
      var object = cursor.value // Получить совпавшую запись
      callback(object);         // Передать ее указанной функции
      cursor.continue();        // Запросить следующую запись
    };
  });
}
// Эта функция используется обработчиком onchange в документе ниже.
// Она выполняет запрос к БД и отображает результаты
function displayCity(zip) {
  lookupCity(zip, function(s) { document.getElementById('city').value=s; });
}
// Это другая функция, используемая обработчиком onchange в документе ниже.
// Она выполняет запрос к БД и отображает результаты
function displayZipcodes(city) {
  var output = document.getElementById("zipcodes");
  output.innerHTML = "Найденные индексы:";
  lookupZipcodes(city, function(o) {
    var div = document.createElement("div");
    var text = o.zipcode + ": + o.city + ", " + o.state;
    div.appendChild(document.createTextNode(text));
    output.appendChild(div);
  });
}
// Настраивает структуру базы данных и заполняет ее данными, затем передает базу данных
// функции f(). Эта функция вызывается функцией withDB(), если база данных еще не была
// инициализирована. Это самая хитрая часть программы, поэтому мы оставили ее напоследок,
function initdb(db, f) {
  // Загрузка информации о почтовых индексах и сохранение ее в базе данных может
  // потребовать некоторого дополнительного времени при первом запуске этого
  // приложения. Поэтому необходимо вывести сообщение, извещающее о выполнении операции,
  var statusline = document.createElement("div");
  statusline.style.cssText =
    "position:fixed; left:0px; top:0px; width:100%;" +
    "color:white; background-color: black; font: bold 18pt sans-serif;" +
    "padding: 10px; ";
  document.body.appendChild(statusline);
  function status(msg) { statusline.innerHTML = msg.toString(); };
  status("Инициализация базы данных почтовых индексов");
  // Единственное место, где можно определить или изменить структуру
  // базы данных IndexedDB - обработчик onsucess запроса setVersion.
  var request = db.setVersion("1"); // Попробовать изменить версию БД
  request.onerror = status; // Вывести сообщение в случае ошибки
  request.onsuccess = function() { // Иначе вызвать эту функцию
    // База данных почтовых индексов включает единственное хранилище.
    // Оно хранит объекты следующего вида: {
    // zipcode: "02134", // Отправьте на телепередачу Zoom![60]:-)
    // city: "Allston",
    // state: "MA",
    // latitude: "42.355147",
    // longitude: '-71.13164”
    // }
    //
    // Свойство "zipcode" используется в качестве ключа базы данных
    // Кроме того, создается индекс по названию города
    // Создать хранилище объектов, указав имя хранилища и объект с параметрами,
    // включающий "путь к ключу", определяющий имя свойства-ключа для этого
    // хранилища. (Если опустить путь к ключу, IndexedDB определит свой
    // собственный уникальный целочисленный ключ.)
    var store = db.createObjectStore("zipcodes", // имя хранилища
                                 { keyPath: "zipcode" });
    // Создать в хранилище объектов индекс по названию города.
    // Строка пути к ключу передается этому методу непосредственно,
    // как обязательный аргумент, а не как свойство объекта с параметрами,
    store.createlndex("cities", "city");
    // Теперь необходимо загрузить информацию о почтовых индексах, преобразовать
    // ее в объекты и сохранить эти объекты в созданном выше хранилище.
    //
    // Файл с исходными данными содержит строки следующего вида:
    //
    // 02130,Jamaica Plain,MA.42.309998,-71.11171
    // 02131,Roslindale,MA,42.284678,-71.13052
    // 02132,West Roxbury.MA,42.279432,-71.1598
    // 02133,Boston,MA,42.338947,-70.919635
    // 02134,Allston,MA, 42.355147,-71.13164
    //
    // Как ни странно, но почтовая служба США не обеспечивает свободный доступ
    // к этой информации, поэтому мы будет использовать устаревшие данные переписи
    // с сайта: http://mappinghacks.com/2008/04/28/civicspace-zip-code-database/
    // Для загрузки данных используется объект XMLHttpRequest.
    // Но для обработки данных по мере поступления будут использованы
    // новые события onload и onprogress, определяемые спецификацией XHR2
    var xhr = new XMLHttpRequest();  // Объект XHR для загрузки данных
    xhr.open("GET", "zipcodes.csv"); // HTTP-запрос типа GET для этого URL
    xhr.send();                      // Запустить немедленно
    xhr.oneг гог = status;           // Отображать сообщения об ошибках
    var lastChar = 0, numlines = 0;  // Уже обработанный объем
    // Обрабатывает файл базы данных блоками, по мере загрузки
    xhr.onprogress = xhr.onload = function(e) { // Сразу два обработчика!
      // Обработать блок между lastChar и последним принятым символом
      // перевода строки. (Нам требуется отыскать последний символ
      // перевода строки, чтобы не обработать неполную запись)
      var lastNewline = xhr.responseText.lastIndexOf("n");
      if (lastNewline > lastChar) {
        var chunk = xhr.responseText.substring(lastChar, lastNewline)
        lastChar = lastNewline + 1; // Откуда начинать в следующий раз
        // Разбить новый фрагмент на строки
        var lines = chunk.split("n”);
        numlines += lines.length;
        // Чтобы вставить информацию о почтовом индексе в базу данных, необходимо
        // получить объект транзакции. Все операции добавления объектов
        // в базу данных, выполняемые с использованием этого объекта,
        // будут автоматически подтверждаться после выхода из этой функции,
        // когда броузер вернется в цикл обработки событий.
        // Чтобы создать объект транзакции, следует определить,
        // какие хранилища объектов будут использоваться (у нас имеется всего
        // одно хранилище). Кроме того, требуется сообщить, что будет
        // выполняться не только чтение, но и запись в базу данных:
        var transaction = db.transaction(["zipcodes’'], // хранилища
                                     IDBTransaction.READ_WRITE);
        // Получить ссылку на хранилище из объекта транзакции
        var store = transaction.objectStore("zipcodes");
        // Теперь обойти в цикле строки в файле с почтовыми индексами,
        // создать на их основе объекты и добавить их в хранилище.
        for(var і = 0; і < lines.length; i++) {
          var fields = lines[i].split(",");// Значения через запятую
          var record = { // Сохраняемый объект
            zipcode: fields[0], // Все свойства - строки
            city: fields[1],
            state: fields[2],
            latitude: fields[3],
            longitude: fields[4]
          };
          // Вся прелесть IndexedDB API в том, что хранилище
          // объектов *по-настоящему* просто использовать.
          // Следующая строка добавляет запись:
          store.put(record); // Или add(), чтобы избежать затирания
        }
        status("Инициализация базы данных, загружено записей:
               + numlines + ".");
      }
      if (e.type == "load") {
        // Если это было последнее событие load, значит, мы отправили в базу
        // данных все сведения о почтовых индексах. Но, так как мы только
        // что обработали порядка 40000 записей, они все еще могут записываться
        // в хранилище. Поэтому мы выполним простой запрос. Когда он будет
        // успешно выполнен, это послужит сигналом, что база данных готова
        // к работе, и наконец можно удалить строку сообщения и вызвать
        // функцию f(), которая так давно была передана функции withDB()
        lookupCity("02134",
          function(s) { // Allston, MA
             document.body.removeChild(statusline); withDB(f);
        });
      }
    }
  }
}
</script>
</head>
<body>
<р>Введите почтовый индекс, чтобы отыскать город:</р>
Индекс: <input></input>
Город: <output></output>
</div>
<div>
  <р>Введите название города (с учетом регистра символов, без названия штата),
   чтобы отыскать все города с этим названием и их почтовые индексы:</р>
   Город: <input></input>
  <div></div>
</div>
<р><і>Зтот пример работает только в Firefox 4 и Chrome 11.</i></p>
<р><і>Выполнение первого запроса может занять длительное время.</і></р>
<р><і>Вам может потребоваться запустить Chrome с
ключом --unlimited-quota-for-indexeddb</i></p>
</body>
</html>

Оглавление книги


Генерация: 0.498. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз