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.

Iklan

Day 8: Object Java dan Object Oracle Database

Learning In HomeOriginal Date: 20 Januari 2009

Untuk memakai tipe data object di Oracle Database, selain menggunakan STRUCT, aku juga bisa membuat sebuah class Java yang bersesuaian. Sebagai latihan, hari ini aku akan membuat class yang merepresentasikan object Oracle dimana object Oracle tersebut merupakan turunan dari object lain, yaitu TYPE INDIVIDUAL_CIF UNDER CIF.

Pertama, aku akan membuat class Java yang merepresentasikan object parent CIF:


import java.sql.Connection;
import java.sql.SQLException;

import oracle.sql.*;

public class CIF implements ORAData, 
  ORADataFactory {

  private final static CIF cif = 
    new CIF();
	
  public CHAR idNo;
  public CHAR accNo;	
	
  public CIF () {}
	
  public CIF (CHAR idNo, CHAR accNo) {
    this.idNo = idNo;
    this.accNo = accNo;
  }
	
  public static CIF getInstance() {
    return cif;
  }
	

  public Datum toDatum(Connection cn) 
      throws SQLException {
    StructDescriptor sd = StructDescriptor.
      createDescriptor("CIF", cn);
    Object[] attr = {idNo,accNo};
    return new STRUCT(sd, cn, attr);
  }


  public ORAData create(Datum datum, 
   int sqlType) throws SQLException {
    if (datum==null) return null;
      Object[] attr = ((STRUCT)datum).
         getOracleAttributes();
      return new CIF((CHAR) attr[0], 
         (CHAR) attr[1]);
  }
	
  public String toString() {
    return "CIF: IDNO = " + this.idNo + 
      "; ACCNO = " + this.accNo;
  }

}

Class di atas harus meng-implementasi-kan ORAData dan ORADataFactory, jika akan menggunakan fitur ekstensi JDBC dari Oracle. Jika portabilitas merupakan prioritas, class standar dari JDBC, SQLData.

Langkah berikutnya, aku membuat class turunan dari class sebelumnnya:


import java.sql.Connection;
import java.sql.SQLException;

import oracle.sql.*;

public class IndividualCIF 
  extends CIF {
	
  public CHAR firstName;
  public CHAR lastName;
  public DATE birthDate;
	
  public final static IndividualCIF 
    individualCIF = new IndividualCIF();
	
  public IndividualCIF() {}
	
  public IndividualCIF(CHAR idNo, CHAR accNo, 
    CHAR firstName, CHAR lastName, 
    DATE birthDate) {
   super(idNo, accNo);
   this.firstName = firstName;
   this.lastName = lastName;
   this.birthDate = birthDate;
  }
	
  public static IndividualCIF 
    getInstance() {
   return individualCIF;
  }

  public Datum toDatum(Connection cn) 
    throws SQLException {
   StructDescriptor sd = 
     StructDescriptor.createDescriptor(
       "INDIVIDUAL_CIF", cn);
   Object[] attr = { super.idNo, super.accNo, 
      firstName, lastName, birthDate };
   return new STRUCT (sd, cn, attr);	
  }
	
  public ORAData create(Datum d, int sqlType) 
    throws SQLException {
     if (d==null) return null;
     Object[] attr = ((STRUCT)d).
       getOracleAttributes();
     return new IndividualCIF((CHAR)attr[0], 
       (CHAR)attr[1],(CHAR)attr[2], 
       (CHAR)attr[3], (DATE)attr[4]);		
  }
	

  public String toString() {
   return "CIFIndividual: ACC NO = " + 
       super.accNo + "; " +
       "ID NO = " + super.idNo + "; " +
       "FirstName = " + this.firstName + "; " +
       "LastName = " + this.lastName + "; " +
       "BirthDate = " + this.birthDate.dateValue();
  }
	
}

Selanjutnya, aku membuat sebuah class factory yang akan dipakai untuk menghasilkan salah satu dari kedua class di atas sesuai dengan tipe data ORACLE-nya:

import oracle.sql.*;

public class CIFFactory implements 
  ORADataFactory {

 public static final CIFFactory 
  cifFactory = new CIFFactory();

 public static CIFFactory 
   getInstance() {
     return cifFactory;
 }
	

 public ORAData create(Datum d, 
  int sqlType) throws SQLException {
   STRUCT s = (STRUCT) d;
   if (s.getSQLTypeName().equals(
     "SCOTT.CIF")) {
    return CIF.getInstance().
     create(d, sqlType);
   } else if (s.getSQLTypeName().equals(
     "SCOTT.INDIVIDUAL_CIF")) {
      return IndividualCIF.getInstance().
      create(d, sqlType);
   } else {
    return null;
   }
 }

}

Dan ini adalah potongan kode program utama yang mengambil data field dari tabel dalam bentuk salah satu dari class di atas:


Object cif  = rs.getORAData(1, 
  CIFFactory.getInstance());
System.out.println(cif);

Object Type: Menyimpan Object Ke Dalam Table

Object yang sudah kita buat juga dapat kita simpan ke dalam sebuah table. Sebelumnya, kita harus membuat table untuk menampung object tersebut, dengan perintah seperti berikut:


CREATE TABLE TBL_INDIVIDUAL_CIF OF
INDIVIDUAL_CIF;

Setelah itu, kita bisa memasukkan data seperti biasanya:


INSERT INTO TBL_INDIVIDUAL_CIF VALUES (
'ID-1', 'ACC-1', 'Solid', 'Snake',
CURRENT_DATE
);

Kode berikut akan mengambil nilai object tersebut melalui PL/SQL:

DECLARE
  cif INDIVIDUAL_CIF;
BEGIN
  SELECT VALUE(P) INTO cif FROM
     tbl_individual_cif p;
  cif.print_account_info();
END;
/