Scott Tiger Tech Blog

Blog technologiczny firmy Scott Tiger S.A.

IndexedDB

Autor: Piotr Karpiuk o wtorek 3. Lipiec 2012

W zakresie trwałego przechowywania danych po stronie klienta aplikacji webowych mamy obecnie sporo mechanizmów do wyboru: Web Storage, File API, jak również bazy danych. Co do baz danych, istnieją dwie alternatywy:

  • WebSQL, czyli relacyjna baza danych (w praktyce SQLite) i dostęp za pomocą SQLa – obecna we wszystkich liczących się przeglądarkach z wyjątkiem MSIE i FF, ale pod koniec 2010 roku wycofana ze standardu HTML5,
  • IndexedDB, czyli noSQLowa, obiektowa baza danych, dla odmiany ujęta w standardzie HTML5.

Niniejszy artykuł pozwoli sobie wyrobić zdanie na temat IndexedDB, technologii obecnej w przeglądarkach FF 4+, Chrome 11+ i MSIE 10+. Najpierw kilka akapitów ogólnego opisu, a na końcu przykład.

Twórcy IndexedDB postawili za cel stworzenie API do przechowywania (w szczególności offline) po stronie klienta dużych ilości danych dla celów wydajnego przeszukiwania z wykorzystaniem indeksów. Jedna aplikacja webowa (identyfikowana przez trójkę: protokół, domena, port – patrz zasada same origin policy) może tworzyć wiele baz danych różniących się nazwami. W obrębie bazy mamy magazyny (ang. object stores) – odpowiedniki tabel w relacyjnych bazach danych – gdzie trzymamy pary klucz/wartość. Kluczem może być łańcuch, data lub liczba, a wartością dowolny JSON (z dodatkowo dostępnymi typami date, regexp, undefined, a w FF nawet file i blob). W rzeczywistości klucz w parze klucz/wartość nie musi być osobnym ręcznie generowanym bytem (ang. out-of-line key), lecz może być generowany automatycznie (odpowiednik sekwencji w relacyjnych bazach danych), jak również może być częścią wartości (ang. in-line key), np. dla obiektu { pesel: '77061512654', imie: 'Jan', nazwisko: 'Kowalski' } możemy podczas tworzenia magazynu zdefiniować ścieżkę klucza (ang. key path) w postaci identyfikatora pesel, co spowoduje że kluczem dla wartości staje się jej pole pesel.

Obiekty w magazynach są posortowane rosnąco wg unikalnego klucza. Na dowolnej własności obiektów magazynu można założyć indeks w celu szybkiego dostępu. Formalnie jest to specjalizowany trwały magazyn przeznaczony do wyszukiwania rekordów w innym magazynie. Jak każdy magazyn, przechowuje pary klucz-wartość, gdzie wartość jest kluczem w docelowym magazynie. Indeks jest aktualizowany automatycznie wraz z aktualizacją magazynu docelowego. Indeksy mogą wymuszać proste więzy (np. unikatowość).

Jedna baza może obsługiwać jednocześnie wiele połączeń (np. kilka zakładek w przeglądarce może otworzyć tę samą aplikację dobierającą się do tej samej bazy). W obrębie połączenia może być wiele transakcji. Dostęp do bazy danych odbywa się wyłącznie poprzez transakcje, które mogą być jednego z trzech typów: read/write, read only albo version change – ta ostatnia służy do modyfikowania struktury bazy i zakładania indeksów. Problem współbieżności rozwiązano w ten sposób, że przy otwieraniu transakcji czytającej lub piszącej trzeba określić których magazynów będzie dotyczyć (zasięg, ang. scope). Transakcja zatwierdza się automatycznie, ale można ją ręcznie anulować (metoda abort()).

Po utworzeniu, baza ma wersję 1. Jedynym sposobem na zmianę wersji jest otworzyć ją z numerem wersji wyższym niż obecny. To utworzy nową transakcję i wygeneruje zdarzenie upgradeneeded. Jedynym miejscem, w którym możemy modyfikować schemat bazy, jest obsługa tego zdarzenia.

Jeśli ktoś szuka w IndexedDB jakiegoś języka zapytań będącego odpowiednikiem SQL, to zawiedzie się srodze. Jeśli coś takiego jest mu potrzebne, musi… napisać go we własnym zakresie. W zakresie odczytu danych API oferuje jedynie odczyt obiektu o wskazanym kluczu oraz iterację (kursorem) po wartościach z podanego zakresu klucza lub indeksu.

Typowy scenariusz pracy z bazą IndexedDB wygląda następująco:

  1. Otwieramy bazę danych i rozpoczynamy transakcję.
  2. Tworzymy magazyn.
  3. Tworzymy zlecenie (ang. request) do wykonania operacji na bazie, np. zapis lub odczyt danych.
  4. Czekamy na zakończenie operacji nasłuchując właściwego zdarzenia DOM. Zdarzenie błędu domyślnie przerywa transakcję, chyba że zostanie anulowane (metoda preventDefault zdarzenia). Co więcej, zdarzenia błędów bąbelkują od obiektu zlecenia poprzez obiekt transakcji do obiektu bazy danych, tzn. nie trzeba pisać obsługi błędów dla każdego zlecenia, można napisać jedną zbiorczą funkcję obsługi błędów na poziomie bazy danych.
  5. Odczytujemy wynik, który znajduje się w obiekcie zdarzenia.

Uwagi

  • Istnieje ograniczenie na przestrzeń dyskową zajmowaną przez wszystkie bazy (max 50% dysku) i pojedynczej bazy (max 20% pierwszego ograniczenia), czyli np. dla dysku 100GB jedna baza nie może zajmować więcej niż 10GB.
  • Czyszczenie cache przeglądarki może wyczyścić bazy IndexedDB.
  • W trybie incognito (czyli tzw. „trybie porno”) IndexedDB jest wyłączony.
  • Oczekuje się, że transakcja będzie krótka – przeglądarka może ją wycofać gdy uzna że trwa już zbyt długo.

Przykłady

Ponieważ specyfikacja nie jest jeszcze kompletna i pomiędzy przeglądarkami są (drobne) różnice w interfejsie dostępu do bazy, dostęp do obiektu IndexedDB wygląda tak:

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;

Interfejs bazy danych może być asynchroniczny (wywołania zwrotne) lub synchroniczny. Ten drugi jest z myślą o wątkach WebWorkers, ale póki co wszystkie przeglądarki implementują wyłącznie podejście asynchroniczne.

var db;
const customerData = [  
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },  
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" }  
];  

// Otwieramy baze danych i rozpoczynamy transakcje. Jesli baza nie istnieje, zostanie utworzona.
window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;  
var request = window.indexedDB.open("MyTestDatabase");  

request.onerror = function(event) {
  alert( 'error' );
};

function upgradeDB( event ) {
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
  objectStore.createIndex("name", "name", { unique: false });
  objectStore.createIndex("email", "email", { unique: true });
  operations();
}

request.onsuccess = function(event) {
  db = request.result;
  db.onerror = function( event ) {
    alert("Database error: " + event.target.errorCode);
  };
  var req = db.setVersion( 40 );
  req.onsuccess = upgradeDB;
};

request.onupgradeneeded = upgradeDB;

function operations() {
  var transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE); // pusta tablica oznacza: wszystkie magazyny
  // Do something when all the data is added to the database.
  transaction.oncomplete = function(event) {
    alert("All done!");
  };

  transaction.onerror = function(event) {
    // Don't forget to handle errors!
  };

  var objectStore = transaction.objectStore("customers");
  for (var i in customerData) {
    var request = objectStore.add(customerData[i]); // put gdy wstawiamy lub modyfikujemy
    request.onsuccess = function(event) {
      // event.target.result == customerData[i].ssn
    };
  }
}

Usuwanie danych:

var request = db.transaction(["customers"], IDBTransaction.READ_WRITE)  
                .objectStore("customers")  
                .delete("444-44-4444");  
request.onsuccess = function(event) {  
  // It's gone!  
};

Pobieranie rekordu wg klucza:

db.transaction("customers").objectStore("customers").get("444-44-4444").onsuccess = function(event) {  
  alert("Name for SSN 444-44-4444 is " + event.target.result.name);  
};

i po indeksie:

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

index.openCursor().onsuccess = function(event) {  
  var cursor = event.target.result;  
  if (cursor) {  
    // cursor.key is a name, like "Bill", and cursor.value is the whole object.  
    alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ", email: " + cursor.value.email);  
    cursor.continue();  
  }  
};

Wyciąganie fragmentu magazynu:

// Only match "Donna"  
var singleKeyRange = IDBKeyRange.only("Donna");  
  
// Match anything past "Bill", including "Bill"  
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");  
  
// Match anything past "Bill", but don't include "Bill"  
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);  
  
// Match anything up to, but not including, "Donna"  
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);  
  
//Match anything between "Bill" and "Donna", but not including "Donna"  
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);  
  
index.openCursor(boundKeyRange).onsuccess = function(event) {  
  var cursor = event.target.result;  
  if (cursor) {  
    // Do something with the matches.  
    cursor.continue();  
  }  
};
Share and Enjoy:
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Śledzik
  • Blip
  • Blogger.com
  • Gadu-Gadu Live
  • LinkedIn
  • MySpace
  • Wykop

Zostaw komentarz

XHTML: Możesz użyć następujących tagów: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>