Membaca dan Menulis Data

(Opsional) Membuat prototipe dan melakukan pengujian dengan Firebase Emulator Suite

Sebelum membahas cara aplikasi Anda membaca dari dan menulis ke Realtime Database, kami akan memperkenalkan serangkaian alat yang dapat digunakan untuk membuat prototipe dan menguji fungsionalitas Realtime Database: Firebase Emulator Suite. Jika Anda sedang mencoba berbagai model data, mengoptimalkan aturan keamanan, atau berupaya menemukan cara yang paling hemat untuk berinteraksi dengan backend, kemampuan untuk bekerja secara lokal tanpa men-deploy layanan langsung dapat sangat bermanfaat.

Emulator Realtime Database adalah bagian dari Emulator Suite yang memungkinkan aplikasi berinteraksi dengan konfigurasi dan konten database yang diemulasi, serta secara opsional dengan resource project yang diemulasi (fungsi, database lain, dan aturan keamanan).emulator_suite_short

Anda hanya perlu beberapa langkah untuk menggunakan emulator Realtime Database:

  1. Menambahkan satu baris kode ke konfigurasi pengujian aplikasi untuk terhubung ke emulator.
  2. Menjalankan firebase emulators:start dari root direktori project lokal Anda.
  3. Melakukan panggilan dari kode prototipe aplikasi Anda menggunakan SDK platform Realtime Database seperti biasa, atau menggunakan Realtime Database REST API.

Panduan mendetail yang mencakup Realtime Database dan Cloud Functions telah tersedia. Sebaiknya baca juga Pengantar Emulator Suite.

Mendapatkan DatabaseReference

Untuk membaca atau menulis data dari database, Anda memerlukan instance DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref();

Menulis data

Dokumen ini membahas dasar-dasar membaca dan menulis data Firebase.

Data Firebase dituliskan ke DatabaseReference dan diambil dengan menunggu atau memproses peristiwa yang dikeluarkan oleh referensi. Peristiwa akan dikeluarkan satu kali untuk status awal data, dan dikeluarkan lagi setiap kali data berubah.

Operasi tulis dasar

Untuk operasi tulis dasar, Anda dapat menggunakan set() untuk menyimpan data ke referensi yang ditentukan, sehingga menggantikan data yang ada di jalur tersebut. Anda dapat menetapkan referensi ke jenis berikut: String, boolean, int, double, Map, List.

Misalnya, Anda dapat menambahkan pengguna dengan set() seperti berikut:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

Penggunaan set() seperti ini akan menimpa data di lokasi yang ditentukan, termasuk semua node turunan. Namun, Anda masih dapat memperbarui turunan tanpa menulis ulang seluruh objek. Jika ingin mengizinkan pengguna memperbarui profilnya, Anda dapat memperbarui nama pengguna seperti berikut:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the name, leave the age and address!
await ref.update({
  "age": 19,
});

Metode update() menerima sub-jalur ke node, sehingga Anda dapat mengupdate beberapa node di database sekaligus:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

Membaca data

Membaca data dengan memproses peristiwa nilai

Untuk membaca data di suatu jalur dan memproses perubahan, gunakan properti onValue dari DatabaseReference untuk memproses DatabaseEvent.

Anda dapat menggunakan DatabaseEvent untuk membaca data di jalur tertentu, sesuai kondisi pada saat peristiwa terjadi. Peristiwa ini dipicu satu kali saat pemroses ditambahkan dan dipicu lagi setiap kali terjadi perubahan pada data, termasuk pada setiap turunannya. Peristiwa tersebut memiliki properti snapshot yang berisi semua data di lokasi tersebut, termasuk data turunan. Jika tidak ada data, properti exists snapshot akan berupa false dan properti value-nya akan berupa null.

Contoh berikut menampilkan aplikasi blogging sosial yang mengambil detail suatu postingan dari database:

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

Pemroses menerima DataSnapshot yang memuat data di lokasi yang ditentukan dalam database saat terjadi peristiwa di properti value miliknya.

Membaca data sekali

Membaca sekali menggunakan get()

SDK didesain untuk mengelola interaksi dengan server database baik saat aplikasi Anda online maupun offline.

Biasanya, Anda harus menggunakan teknik peristiwa nilai yang dijelaskan di atas untuk membaca data agar mendapatkan notifikasi terkait pembaruan data dari backend. Teknik tersebut dapat mengurangi penggunaan dan penagihan Anda, serta dioptimalkan untuk memberikan pengalaman terbaik kepada pengguna saat mereka sedang online dan offline.

Jika hanya memerlukan data satu kali, Anda dapat menggunakan get() untuk mendapatkan snapshot data dari database. Jika karena alasan apa pun get() tidak dapat menampilkan nilai server, klien akan memeriksa cache penyimpanan lokal dan menampilkan error jika nilainya masih tidak ditemukan.

Contoh berikut menunjukkan pengambilan nama pengguna yang dapat dilihat publik satu kali dari database:

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

Penggunaan get() yang tidak perlu dapat meningkatkan penggunaan bandwidth dan menyebabkan penurunan performa. Ini dapat dicegah menggunakan pemroses realtime seperti yang ditunjukkan di atas.

Membaca data sekali dengan once()

Dalam beberapa kasus, Anda mungkin menginginkan nilai dari cache lokal segera ditampilkan, bukan memeriksa nilai yang diupdate di server. Dalam kasus tersebut, Anda dapat menggunakan once() untuk langsung mendapatkan data dari cache disk lokal.

Cara ini berguna untuk data yang hanya perlu dimuat sekali, dan tidak diharapkan sering berubah atau memerlukan pemroses aktif. Misalnya, aplikasi blogging pada contoh sebelumnya menggunakan metode ini untuk memuat profil pengguna ketika mulai membuat postingan baru:

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

Memperbarui atau menghapus data

Memperbarui kolom tertentu

Untuk menulis secara simultan ke turunan tertentu dari sebuah node tanpa menimpa node turunan yang lain, gunakan metode update().

Saat memanggil update(), Anda dapat memperbarui nilai turunan di level yang lebih rendah dengan menetapkan jalur untuk kunci tersebut. Jika data disimpan dalam beberapa lokasi agar dapat melakukan penskalaan yang lebih baik, Anda dapat memperbarui semua instance data tersebut menggunakan fan-out data. Misalnya, sebuah aplikasi blogging sosial mungkin ingin membuat postingan sekaligus memperbaruinya ke feed aktivitas terbaru dan feed aktivitas pembuat postingan. Untuk melakukannya, aplikasi blogging tersebut menggunakan kode seperti ini:

void writeNewPost(String uid, String username, String picture, String title,
        String body) async {
    // A post entry.
    final postData = {
        'author': username,
        'uid': uid,
        'body': body,
        'title': title,
        'starCount': 0,
        'authorPic': picture,
    };

    // Get a key for a new Post.
    final newPostKey =
        FirebaseDatabase.instance.ref().child('posts').push().key;

    // Write the new post's data simultaneously in the posts list and the
    // user's post list.
    final Map<String, Map> updates = {};
    updates['/posts/$newPostKey'] = postData;
    updates['/user-posts/$uid/$newPostKey'] = postData;

    return FirebaseDatabase.instance.ref().update(updates);
}

Contoh ini menggunakan push() untuk membuat postingan dalam node yang berisi postingan bagi semua pengguna di /posts/$postid, sekaligus mengambil kunci dengan key. Selanjutnya, kunci tersebut dapat digunakan untuk membuat entri kedua di postingan pengguna pada /user-posts/$userid/$postid.

Dengan menggunakan jalur tersebut, Anda dapat menjalankan pembaruan simultan ke beberapa lokasi di hierarki JSON dengan satu panggilan ke update(), seperti yang digunakan pada contoh ini untuk membuat postingan baru di kedua lokasi. Pembaruan simultan yang dilakukan dengan cara ini bersifat atomik: semuanya akan berhasil atau semuanya akan gagal.

Menambahkan callback penyelesaian

Jika ingin tahu kapan data di-commit, Anda dapat mendaftarkan callback penyelesaian. set() dan update() akan menampilkan Future, sehingga Anda dapat menambahkan callback berhasil dan error yang dipanggil jika operasi tulis telah di-commit ke database dan jika panggilan tidak berhasil.

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

Menghapus data

Cara termudah untuk menghapus data adalah dengan memanggil remove() pada referensi ke lokasi data tersebut.

Penghapusan juga dapat dilakukan dengan menentukan null sebagai nilai untuk operasi tulis lainnya, seperti set() atau update(). Teknik ini dapat digunakan dengan update() untuk menghapus beberapa turunan dengan satu panggilan API.

Menyimpan data sebagai transaksi

Ketika menangani data yang bisa rusak karena perubahan serentak, seperti penghitung pertambahan inkremental, Anda dapat menggunakan transaksi dengan meneruskan pengendali transaksi ke runTransaction(). Pengendali transaksi mengambil status data saat ini sebagai argumen dan menampilkan status baru yang ingin Anda tulis. Jika ada klien lain yang melakukan penulisan ke lokasi sebelum nilai yang baru berhasil ditulis, fungsi update Anda akan dipanggil lagi dengan nilai baru saat ini, dan proses tulis akan dicoba ulang.

Misalnya, pada contoh aplikasi blogging sosial, Anda dapat mengizinkan pengguna memberi atau menghapus bintang pada postingan, serta memantau jumlah bintang yang telah diterima suatu postingan dengan cara berikut ini:

void toggleStar(String uid) async {
  DatabaseReference postRef =
      FirebaseDatabase.instance.ref("posts/foo-bar-123");

  TransactionResult result = await postRef.runTransaction((Object? post) {
    // Ensure a post at the ref exists.
    if (post == null) {
      return Transaction.abort();
    }

    Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
    if (_post["stars"] is Map && _post["stars"][uid] != null) {
      _post["starCount"] = (_post["starCount"] ?? 1) - 1;
      _post["stars"][uid] = null;
    } else {
      _post["starCount"] = (_post["starCount"] ?? 0) + 1;
      if (!_post.containsKey("stars")) {
        _post["stars"] = {};
      }
      _post["stars"][uid] = true;
    }

    // Return the new data.
    return Transaction.success(_post);
  });
}

Secara default, peristiwa dipicu setiap kali fungsi update transaksi berjalan. Dengan begitu, saat menjalankan fungsi yang telah dijalankan beberapa kali, Anda dapat melihat status pemrosesan. Anda dapat menetapkan applyLocally ke false untuk menyembunyikan status pemrosesan ini dan, sebagai gantinya, menunggu sampai transaksi selesai sebelum peristiwa dipicu:

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

Hasil transaksi ini berupa TransactionResult, yang berisi informasi tentang, misalnya, di-commit atau tidaknya transaksi, dan snapshot baru:

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");

TransactionResult result = await ref.runTransaction((Object? post) {
  // ...
});

print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

Membatalkan transaksi

Jika Anda ingin membatalkan transaksi dengan aman, panggil Transaction.abort() untuk menampilkan AbortTransactionException:

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

Pertambahan inkremental atomik sisi server

Dalam kasus penggunaan di atas, kita menulis dua nilai ke database: ID pengguna yang memberi/menghapus bintang pada postingan, dan pertambahan inkremental jumlah bintang. Jika sudah mengetahui bahwa pengguna memberi bintang pada postingan, kita dapat menggunakan operasi pertambahan inkremental atomik, bukan transaksi.

void addStar(uid, key) async {
  Map<String, Object?> updates = {};
  updates["posts/$key/stars/$uid"] = true;
  updates["posts/$key/starCount"] = ServerValue.increment(1);
  updates["user-posts/$key/stars/$uid"] = true;
  updates["user-posts/$key/starCount"] = ServerValue.increment(1);
  return FirebaseDatabase.instance.ref().update(updates);
}

Kode ini tidak menggunakan operasi transaksi, sehingga tidak otomatis dijalankan ulang jika ada pembaruan yang bertentangan. Namun, karena operasi pertambahan inkremental terjadi langsung di server database, tidak ada kemungkinan konflik.

Jika ingin mendeteksi dan menolak konflik khusus aplikasi, misalnya pengguna memberi bintang pada postingan yang sebelumnya telah dibintanginya, Anda harus menulis aturan keamanan khusus untuk kasus penggunaan tersebut.

Menangani data secara offline

Jika koneksi jaringan klien terputus, aplikasi Anda akan tetap berfungsi dengan baik.

Setiap klien yang terhubung ke database Firebase menyimpan versi internalnya sendiri untuk setiap data aktif. Ketika ditulis, data akan dituliskan ke versi lokal ini terlebih dahulu. Selanjutnya, klien Firebase menyinkronkan data tersebut dengan server remote database, dan dengan klien lain berdasarkan "upaya terbaik".

Akibatnya, semua operasi tulis ke database akan langsung memicu peristiwa lokal, sebelum ada data yang dituliskan ke server. Ini berarti aplikasi Anda akan tetap responsif, apa pun kondisi konektivitas atau latensi jaringannya.

Setelah terhubung kembali ke jaringan, aplikasi Anda akan menerima kumpulan peristiwa yang sesuai agar klien melakukan sinkronisasi dengan kondisi server saat ini, tanpa harus menulis kode khusus.

Kita akan membahas lebih lanjut perilaku offline di bagian Mempelajari lebih lanjut kemampuan online dan offline.

Langkah berikutnya