Memakai Indexed Database API Di HTML5


Pada web konvensional, satu-satunya metode formal untuk menyimpan data di browser adalah dengan menggunakan cookie. JavaScript dapat mengakses isi cookie melalui document.cookie. HTML5 menawarkan banyak cara baru untuk menyimpan data di sisi client. Saya bisa menyimpan data dengan mengasosiasikannya pada sebuah elemen HTML melalui attribute HTLM5 Custom Data Attributes (data-*). Selain itu, HTML5 juga menawarkan penyimpanan data melalui window.localStorage atau window.sessionStorage. Berbeda dengan penyimpanan melalui cookie, penggunaan HTML5 Web Storage sepenuhnya terjadi di sisi client dan tidak perlu di-inisialisasi melalui HTTP header dari server. Untuk penyimpanan data yang lebih banyak lagi, browser HTML5 memiliki database internal yang dapat diakses melalui Indexed Database API (http://www.w3.org/TR/IndexedDB/).

Pada artikel ini, saya akan mencoba memakai IndexedDB API dengan JavaScript. Sebagai latihan, saya akan membuat sebuah HTML sederhana dimana pengguna dapat mengisi data, menampilkannya pada tabel, dan menyimpannya pada database lokal di browser melalui IndexedDB API. Saya akan mulai dengan membuat HTML seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
    <meta charset='utf-8'/>
    <title>Latihan IndexedDb</title>
    <style type='text/css'>     
        table {
            font: 17px/30px Verdana, Arial, Helvetica, sans-serif;
            border-collapse: collapse; width: 520px;
        }
        table th {
            padding: 0 0.5em; text-align: left; background-color: #FFE45C;
        }
        table tr {
            border-top: 1px solid #fb7a31; border-bottom: 1px solid #fb7a31;
            background: #ffc;
        }
        table td {
            border-bottom: 1px solid #ccc; padding: 0 0.5em;
        }
        form {
            margin-top: 30px;
        }
        ul {
            list-style-type: none; margin: 0px; padding: 0px;
        }
        li {
            padding: 12px; border-bottom: 1px solid #eee;
        }
        li:first-child, li:last-child {
            border-top: 1px solid #777;
        }
        label {
            width: 150px; float: left; margin-top: 3px; 
            display: inline-block;
        }
        #pesan {
            border: 1px dashed #eee; padding: 10px;
            color: #808080; font-family: monospace;
        }
    </style>
</head>
<body>    
    <table>
        <thead>
            <tr>
                <th>NIM</th>
                <th>Nama</th>
                <th>Tanggal Lahir</th>
                <th>IPK</th>
                <th></th>
            </tr>         
        </thead>
        <tbody id="tabel">
        </tbody>
    </table> 
    <form id='frmUtama'>
        <ul>
            <li>
                <label for="nim">NIM:</label>
                <input id="nim" name="nim" type="text" maxlength="20" required/>
            </li>     
            <li>
                <label for="nama">Nama:</label>
                <input id="nama" name="nama" type="text" maxlength="50" required/>
            </li>
            <li>
                <label for="tanggalLahir">Tanggal Lahir:</label>
                <input id="tanggalLahir" name="tanggalLahir" type="text" maxlength="20" pattern="[0-9]{2}/[0-9]{2}/[0-9]{4}" required/>
            </li>     
            <li>
                <label for="ipk">IPK:</label>
                <input id="ipk" name="ipk" type="number" maxlength="3" step="0.1" required />
            </li>
            <li>
                <input id='btnTambah' type="submit" value="Tambah" />
            </li>
        </ul>
    </form>
    <div id='pesan'>    
    </div>
    <script type="text/javascript">
        var tabel = document.getElementById('tabel'),
            nim = document.getElementById('nim'),
            nama = document.getElementById('nama'),
            tanggalLahir = document.getElementById('tanggalLahir'),
            btnTambah = document.getElementById('btnTambah'),
            form = document.getElementById('frmUtama'),
            pesan = document.getElementById('pesan'),
            ipk = document.getElementById('ipk'),
            db;         

        function tambahBaris(e) {
            // Periksa apakah NIM sudah ada
            if (tabel.rows.namedItem(nim.value)) {
                pesan.textContent = 'Error: Nim sudah terdaftar!';
                e.preventDefault();
                return;
            }

            // Membuat baris baru
            var baris = tabel.insertRow();
            baris.id = nim.value;
            baris.insertCell().appendChild(document.createTextNode(nim.value));
            baris.insertCell().appendChild(document.createTextNode(nama.value));
            baris.insertCell().appendChild(document.createTextNode(tanggalLahir.value));
            baris.insertCell().appendChild(document.createTextNode(ipk.value));

            // Membuat tombol hapus untuk setiap baris
            var btnHapus = document.createElement('input');
            btnHapus.type = 'button';
            btnHapus.value = 'Hapus';
            btnHapus.id = nim.value;            
            baris.insertCell().appendChild(btnHapus);

            e.preventDefault();
        }               

        function hapusBaris(e) {
            if (e.target.type=='button') {                
                tabel.deleteRow(tabel.rows.namedItem(e.target.id).sectionRowIndex);
            }
        }

        form.addEventListener('submit', tambahBaris, false);                  
        tabel.addEventListener('click', hapusBaris, true);            
    </script>

</body>
</html>

Pada HTML di atas, saya dapat menambah dan menghapus baris dari tabel seperti yang terlihat pada gambar berikut ini:

Tampilan awal HTML

Tampilan awal HTML

Pada gambar di atas, terlihat bahwa pengguna tidak boleh menambahkan mahasiswa dengan NIM yang sama karena saya akan memakai NIM sebagai key atau primary key yang bersifat unik.

Sekarang, saya akan mencoba menyimpan setiap mahasiswa yang ditambahkan oleh pengguna ke dalam database lokal. Tapi sebelumnya, langkah paling awal yang perlu saya lakukan adalah membuat database baru. Untuk itu, saya menambahkan kode program JavaScript seperti berikut ini:

function kesalahanHandler(e) {
    pesan.innerHTML += 'Kesalahan Database: ' + e.target.errorCode + '<br>';      
}

function buatDatabase() {
    var request = window.indexedDB.open('latihan', 1);
    request.onerror = kesalahanHandler;
    request.onupgradeneeded = function(e) {             
        var db = e.target.result;
        db.onerror = kesalahanHandler;                          
        var objectstore = db.createObjectStore('mahasiswa', { keyPath: 'nim' });
        pesan.innerHTML += 'Object store mahasiswa berhasil dibuat.<br>';
    }
    request.onsuccess = function(e) {           
        db = e.target.result;
        db.onerror = kesalahanHandler;                          
        pesan.innerHTML += 'Berhasil melakukan koneksi ke database!<br>';               
    }
}

buatDatabase();

Pemanggilan IndexedDB API selalu bersifat asynchronous sehingga saya harus memakai event handler. Sebagai contoh window.indexedDB.open() akan melakukan koneksi ke database. Tapi ia tidak melakukannya saat itu juga. Ia hanya mengembalikan sebuah IDBRequest. Lalu kapan saya tahu bahwa ‘request ‘ saya sudah selesai dikerjakan? Event success untuk IDBRequest menandakan bahwa ‘request’ tersebut sudah selesai dikerjakan.

Selain itu, bila ini adalah pertama kali database dibuat atau database memiliki versi yang lebih rendah dari yang hendak dipakai, maka event onupgradeneeded untuk IDBRequest akan terjadi. Ini adalah saat yang tepat untuk membuat object store baru. Perlu diperhatikan bahwa IndexedDB API tidak menyimpan data dalam bentuk tabel seperti pada database relasional. Data (tepatnya object) disimpan langsung ke object store berdasarkan sebuah key. Object store adalah sesuatu yang menyerupai tabel di database relasional.

Pada Firefox, data yang dimanipulasi melalui IndexedDB API akan disimpan melalui database SQLite (database relasional). Firefox juga menggunakan SQLite untuk menyimpan nilai penting lain miliknya. Browser yang berbeda bisa saja menggunakan back-end yang berbeda. Misalnya, pada Google Chrome, back-end untuk IndexedDB API adalah LevelDB (database NoSQL) buatan Google.

Pada Firefox terbaru, saya dapat menemukan file database SQLite yang menjadi back-end IndexedDB API di folder C:\Users[namauser]\AppData\Roaming\Mozilla\Firefox\Profiles[profile]\storage\persistent[namadomain]\idb. Untuk melihat isinya, bila sudah men-download SQLite client di http://www.sqlite.org/download.html, saya dapat menggunakan perintah berikut ini:

C:> sqlite3 C:\Users\[namauser]\AppData\Roaming\Mozzila\Firefox\...\namafile.sqlite

sqlite> .tables
database            index_data          object_store        unique_index_data
file                object_data         object_store_index
sqlite> .header on
sqlite> select * from database;
name        version
----------  ----------
latihan     1
sqlite> select * from object_store;
id          auto_increment  name        key_path
----------  --------------  ----------  ----------
1           0               mahasiswa   nim
sqlite> _

Programmer JavaScript yang memakai IndexedDB API tidak perlu (dan tidak seharusnya) mengetahui back-end database karena tujuan dari dari IndexedDB API adalah sebuah abstraksi yang mempermudah developer. Saya membuka isi database back-end hanya untuk menunjukkan bahwa object store yang dibuat dengan IndexedDB API berhasil disimpan oleh Firefox dalam bentuk sebuah database SQLite.

Berikutnya, saya akan menambahkan data pada object store bila pengguna membuat sebuah baris baru di tabel. Untuk itu, saya membuat JavaScript berikut ini:

...
function cetakPesanHandler(msg) {
    return function(e) {
        pesan.innerHTML += msg + '<br>';
    }
}

function buatTransaksi() {
    var transaction = db.transaction(['mahasiswa'], 'readwrite');
    transaction.onerror = kesalahanHandler;
    transaction.oncomplete = cetakPesanHandler('Transaksi baru saja diselesaikan.');                  
    return transaction;
}

function tambahKeDatabase(mahasiswa) {      
    var objectstore = buatTransaksi().objectStore('mahasiswa');
    var request = objectstore.add(mahasiswa);
    request.onerror = kesalahanHandler;
    request.onsuccess = cetakPesanHandler('Mahasiswa [' + mahasiswa.nim + '] telah ditambahkan ke database lokal.');            
}

...

function tambahBaris(e) {
    ...

    // Tambah ke database
    tambahKeDatabase({
        nim: nim.value,
        nama: nama.value,
        tanggalLahir: tanggalLahir.value,
        ipk: ipk.value
    });

    ...
}   

Pada kode program di atas, setiap kali menambah baris baru ke tabel, saya akan memanggil tambahKeDatabase() yang akan membuat sebuah IDBTransaction baru. Untuk menambah data ke object store, saya perlu memanggil method IDBTransaction.objectStore() yang akan mengambalikan object store dalam bentuk IDBObjectStore. Setelah itu, saya bisa memanipulasi object dalam object store dengan memanggil method IDBObjectStore seperti add(), clear(), delete(), put() dan sebagainya. Pada contoh di atas, karena saya ingin menambahkan data baru, maka saya memanggil IDBObjectStore.add().

Konsep transaction pada IndexedDB API cukup berbeda dibandingkan dengan database pada umumnya. Transaction disini bersifat asynchronous dan akan di-commit secara otomatis oleh browser. Developer hanya bisa membatalkannya dengan memanggil method abort(). Lalu kapan transaction di-commit? Selama browser melihat ada request yang diberikan pada transaction, maka transaction akan tetap aktif. Setelah tidak ada request untuk transaction tersebut, browser akan men-commit transaction tersebut secara otomatis. Perilaku ini disebut sebagai auto-commit. Bila user menutup browser sebelum browser sempat melakukan auto-commit, maka perubahan pada transaction tersebut tidak akan disimpan.

Sampai disini, setiap baris yang saya tambahkan ke tabel akan ikut tersimpan ke database seperti yang terlihat pada gambar berikut ini:

Menyimpan object melalui IndexedDB API

Menyimpan object melalui IndexedDB API

Setelah berhasil menyimpan data, saya akan kembali melakukan perubahan pada JavaScript agar membaca isi object store pada saat halaman pertama kali dibuka. Untuk itu, saya menambahkan kode program JavaScript berikut ini:

...
function buatDatabase() {
    ...
    request.onsuccess = function(e) {           
        ...
        bacaDariDatabase();                                                             
    }
}

function bacaDariDatabase() {
    var objectstore = buatTransaksi().objectStore('mahasiswa');
    objectstore.openCursor().onsuccess = function(e) {
        var result = e.target.result;
        if (result) {
            pesan.innerHTML += 'Membaca mahasiswa [' + result.value.nim + '] dari database.<br>';
            var baris = tabel.insertRow();                  
            baris.id = result.value.nim;
            baris.insertCell().appendChild(document.createTextNode(result.value.nim));
            baris.insertCell().appendChild(document.createTextNode(result.value.nama));
            baris.insertCell().appendChild(document.createTextNode(result.value.tanggalLahir));
            baris.insertCell().appendChild(document.createTextNode(result.value.ipk));
            var btnHapus = document.createElement('input');
            btnHapus.type = 'button';
            btnHapus.value = 'Hapus';
            btnHapus.id = result.value.nim;         
            baris.insertCell().appendChild(btnHapus);
            result.continue();
        }
    }   
}
...

Pada kode program di atas, untuk membaca seluruh isi object store, saya menggunakan cursor dengan memanggil openCursor(). Untuk melakukan iterasi ke seluruh object yang ada, saya harus memanggil method continue() dari IDBCursor yang dikembalikan. Pemanggilan continue() akan menyebabkan event handler untuk success kembali dipanggil. Bila masih ada object yang bisa dikembalikan, saya dapat membaca nilainya yang ditampung dalam property result. Bila sudah tidak ada object lagi, maka result akan bernilai undefined.

Sekarang, bila saya menutup browser lalu membuka browser lagi, baris yang sama seperti sebelumnya akan tetap muncul kembali, seperti yang terlihat pada gambar berikut ini:

Membaca object melalui IndexedDB API

Membaca object melalui IndexedDB API

Sebagai langkah terakhir, saya perlu menghapus nilai dari database lokal bila seandainya baris dihapus oleh pengguna. Oleh sebab itu, saya menambahkan kode program JavaScript berikut ini:

...
function hapusDariDatabase(nim) {
    var objectstore = buatTransaksi().objectStore('mahasiswa');
    var request = objectstore.delete(nim);
    request.onerror = kesalahanHandler;
    request.onsuccess = cetakPesanHandler('Mahasiswa [' + nim + '] berhasil dihapus dari database lokal.');
}
...
function hapusBaris(e) {
    if (e.target.type=='button') {                
        tabel.deleteRow(tabel.rows.namedItem(e.target.id).sectionRowIndex);
        hapusDariDatabase(e.target.id);
    }
}

Sekarang, bila saya menghapus baris dari tabel, maka object dari database akan dihapus berdasarkan nim yang diasosiasikan dengan baris tersebut, seperti yang terlihat pada gambar berikut ini:

Menghapus object melalui IndexedDB API

Menghapus object melalui IndexedDB API

Dengan IndexedDB API, saya telah membuat sebuah halaman web dimana seluruh datanya disimpan di sisi client. Tapi ada satu permasalahan yang saya jumpai disini. Walaupun ini adalah sebuah aplikasi yang hanya bekerja di sisi client, saya harus tetap terhubung secara online ke server untuk membaca halaman ini. Pada saat koneksi internet terputus atau saat saya tidak bisa menghubungi server, Firefox akan menampilkan pesan kesalahan Unable to connect: Firefox can’t establish a connection to the servert at … bila saya berusaha men-refresh halaman. Untuk mengatasi masalah ini, HTML5 memperkenalkan Application Cache (AppCache) yang dapat dipakai untuk menyimpan halaman secara offline sehingga pengguna tetap dapat membuka halaman pada saat tidak terkoneksi ke Internet.

Untuk memakai HTML5 AppCache, saya harus mendefinisikan sebuah file appcache. Sebagai contoh, saya akan membuat sebuah file bernama latihan.appcache yang isinya adalah:

CACHE MANIFEST
http://snake.domainku.com:8020/latihan_indexedb.html

Setelah itu, saya mengubah tag HTML dengan menambahkan atribut manifest seperti berikut ini:

<!DOCTYPE html>
<html manifest="latihan.appcache">
    ...
</html>

Sebagai tambahan, bila tidak memakai library seperti Offline.js, saya dapat memeriksa event online dan offline di Firefox untuk memberikan pesan informasi pada pengguna bila pengguna menjadi tidak terkoneksi atau menjadi terkoneksi ke Internet (sebagai catatan, kedua event ini tidak akurat dan hanya memeriksa status work offline di Firefox):

window.addEventListener('offline', function(e) {
    pesan.innerHTML += 'Anda sedang tidak terhubung ke Internet!<br>';
});         
window.addEventListener('online', function(e) {
    pesan.innerHTML += 'Anda sudah terhubung kembali ke Internet!<br>';
});

Sekarang, bila saya menjalankan browser dan menampilkan halaman pada kondisi online, ia akan men-download file yang terdaftar di cache manifest agar dapat dipakai secara offline. Setelah itu, saya dapat membuat halaman secara offline dan mengisi data seperti biasanya. Saya juga bisa me-refresh halaman tanpa menemukan pesan kesalahan lagi, seperti yang terlihat pada gambar berikut ini:

Web dapat direfresh walaupun tidak terhubung ke server

Web dapat direfresh walaupun tidak terhubung ke server

Halaman web tetap dapat bekerja walaupun server dimatikan atau tidak terkoneksi ke server karena Firefox menyimpan file untuk dipakai secara offline. Saya dapat melihat informasi application cache di Firefox dengan membuka menu Options, Network, Advanced. Pada bagian Offline Web Content and User Data, saya akan menemukan informasi seperti pada gambar berikut ini:

Pengaturan AppCache Di Firefox

Pengaturan AppCache Di Firefox

Saya juga dapat mengosongkan isi AppCache bila perlu dengan men-klik tombol Clear Now. Sebagai alternatif, saya juga dapat memilih salah satu situs yang terdaftar dan men-klik tombol Remove… untuk menghapus AppCache untuk situs tersebut.

Perihal Solid Snake
I'm nothing...

6 Responses to Memakai Indexed Database API Di HTML5

  1. Yogie Kurniawan mengatakan:

    Terima kasih pak tulisannya sangat bermanfaat. Saya sedang menyusun skripsi dengan menggunakan indexedDB dalam aplikasi saya. Kalau untuk dokumentasi objek dalam bentuk class diagramnya apa sama saja seperti yang lain ?

    • Solid Snake mengatakan:

      Syntax JavaScript untuk saat ini belum mendukung OOP sehingga OOP pada JavaScript diterapkan melalui disiplin. Pada ES6/JavaScript 6 (https://thesolidsnake.wordpress.com/2014/06/01/mencoba-ecmascript-6-harmony-di-firefox/) yang saat ini belum final, baru akan ada struktur bahasa untuk mendukung OOP seperti class dan inheritance. Hal ini membuat JavaScript mendukung OOP seperti halnya pada PHP, Java, C# dan sebagainya.

      Selain belum mendukung class, JavaScript juga sering dipakai secara dinamis. Hal ini membuat fungsi class diagram semakin berkurang. Walaupun demikian, terlepas dari sifat dinamis dimana setiap object bisa memiliki atribut dan operasi baru secara dinamis, class diagram tetap dapat dipakai sebagai catatan mental.

      • Yogie Kurniawan mengatakan:

        Maksud dari JavaScript diterapkan melalui disiplin dan class diagram tetap dapat dipakak sebagai catatan mental. Apa maksudnya ya pak, saya kurang mengerti ?
        Satu lagi pak. Dalam implementasi, saya menggunakan Framework AngularJS untuk Framework JavaScript yang katanya Framework ini mempunyai pola arsitektur MVC yang berkembang menjadi pola arsitektur MVVM. Dengan pendekatan menggunakan Framework yang memiliki arsitektur tersebut apakah mungkin perancangannya akan mirip seperti yang bapak posting diblog ini dengan menggunakan UML ?
        Terimakasih banyak sebelumnya atas ilmu yang sudah dibagikannya. Semoga Tuhan YME membalasnya. Amin

  2. Solid Snake mengatakan:

    Saya akan mencoba menunjukkan dengan contoh, misalnya bila kita menganggap ada class Mahasiswa seperti pada contoh berikut ini:

    function Mahasiswa(nama, nilai) {
      this.nama = nama;
      this.nilai = nilai;
      this.daftar = function() { console.log(this.nama + ' mendaftar!') };
    }
    
    var m1 = new Mahasiswa("solid snake", 50);
    var m2 = new Mahasiswa("liquid snake", 60);
    
    m1.daftar();
    m2.daftar();
    

    Kita bisa menggambarkannya sebagai 1 kotak class Mahasiswa dengan atribut nama dan nilai serta sebuah operasi daftar() di class diagram.

    Akan tetapi, karena sifatnya yang dinamis, bisa saja terdapat kode program seperti berikut ini:

    m2.marah = function() { console.log(this.nama + ' marah!'); }
    
    m2.marah();
    

    Sekarang, hanya m2 yang punya operasi marah() sementara m1 tidak! Mengerjakan m1.marah() akan menghasilkan pesan kesalahan. Bagaimana menggambarkan ini di class diagram? marah() bukan bagian dari seluruh Mahasiswa, tapi dimiliki beberapa Mahasiswa seperti m2.

    Class diagram lebih tepat dipakai untuk bahasa yang tidak dinamis seperti Java. Pada artikel ini saya tidak memakai diagram UML dan juga tidak membahasa tentang MVVM.

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: