Dokumen ini membahas dasar-dasar membaca dan menulis data Firebase.
Data Firebase dituliskan ke referensi FirebaseDatabase
dan diambil dengan menambahkan pemroses asinkron ke referensi tersebut. Pemroses dipicu satu kali untuk status awal data, dan dipicu lagi setiap kali data berubah.
(Opsional) Membuat prototipe dan melakukan pengujian dengan Firebase Local Emulator Suite
Sebelum membahas cara aplikasi Anda membaca dari dan menulis ke Realtime Database, kenali Firebase Local Emulator Suite yang merupakan serangkaian alat yang dapat Anda gunakan untuk membuat prototipe dan menguji fungsi Realtime Database. 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 Local Emulator Suite, yang memungkinkan aplikasi Anda berinteraksi dengan konten dan konfigurasi database yang diemulasi, serta, jika diinginkan, dengan resource project yang diemulasi (fungsi, database lain, dan aturan keamanan).
Hanya diperlukan beberapa langkah untuk menggunakan emulator Realtime Database:
- Menambahkan satu baris kode ke konfigurasi pengujian aplikasi untuk terhubung ke emulator.
- Menjalankan
firebase emulators:start
dari root direktori project lokal Anda. - 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 Local Emulator Suite.
Mendapatkan DatabaseReference
Untuk membaca atau menulis data dari database, Anda memerlukan instance DatabaseReference
:
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Menulis data
Operasi tulis dasar
Untuk operasi tulis dasar, Anda dapat menggunakan setValue()
untuk menyimpan data ke referensi yang ditentukan, sehingga menggantikan data yang ada di jalur tersebut. Anda bisa menggunakan metode ini untuk:
- Meneruskan jenis yang cocok dengan jenis JSON yang tersedia berikut ini:
String
Long
Double
Boolean
Map<String, Object>
List<Object>
- Meneruskan objek Java kustom, jika class yang menentukannya memiliki konstruktor default yang tidak membutuhkan argumen, dan memiliki pengambil publik untuk properti yang akan ditetapkan.
Jika Anda menggunakan objek Java, konten objek akan otomatis dipetakan ke lokasi turunan secara bertingkat. Penggunaan objek Java biasanya juga membuat kode Anda lebih mudah dibaca dan dipelihara. Misalnya, jika Anda memiliki aplikasi dengan profil pengguna dasar, objek User
mungkin akan terlihat sebagai berikut:
Kotlin+KTX
@IgnoreExtraProperties data class User(val username: String? = null, val email: String? = null) { // Null default values create a no-argument default constructor, which is needed // for deserialization from a DataSnapshot. }
Java
@IgnoreExtraProperties public class User { public String username; public String email; public User() { // Default constructor required for calls to DataSnapshot.getValue(User.class) } public User(String username, String email) { this.username = username; this.email = email; } }
Anda dapat menambahkan pengguna dengan setValue()
sebagai berikut:
Kotlin+KTX
fun writeNewUser(userId: String, name: String, email: String) { val user = User(name, email) database.child("users").child(userId).setValue(user) }
Java
public void writeNewUser(String userId, String name, String email) { User user = new User(name, email); mDatabase.child("users").child(userId).setValue(user); }
Penggunaan setValue()
seperti ini akan menimpa data di jalur 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:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Membaca data
Membaca data dengan pemroses persisten
Untuk membaca data di suatu jalur dan memproses perubahan, gunakan metode addValueEventListener()
untuk menambahkan ValueEventListener
ke DatabaseReference
.
Listener | Callback peristiwa | Penggunaan standar |
---|---|---|
ValueEventListener |
onDataChange() |
Membaca dan memproses perubahan pada seluruh konten di sebuah jalur. |
Anda dapat menggunakan metode onDataChange()
untuk membaca snapshot statis konten di jalur tertentu, sebagaimana adanya konten tersebut ketika peristiwa terjadi. Metode ini terpicu satu kali ketika pemroses ditambahkan dan terpicu lagi setiap kali terjadi perubahan pada data, termasuk pada setiap turunannya. Callback peristiwa mendapatkan snapshot yang berisi semua data di lokasi tersebut, termasuk data turunan. Jika tidak ada data, snapshot akan menampilkan false
ketika exists()
dipanggil, serta menampilkan null
ketika getValue()
dipanggil pada snapshot tersebut.
Contoh berikut menampilkan aplikasi blogging sosial yang mengambil detail suatu postingan dari database:
Kotlin+KTX
val postListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { // Get Post object and use the values to update the UI val post = dataSnapshot.getValue<Post>() // ... } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) } } postReference.addValueEventListener(postListener)
Java
ValueEventListener postListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // Get Post object and use the values to update the UI Post post = dataSnapshot.getValue(Post.class); // .. } @Override public void onCancelled(DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); } }; mPostReference.addValueEventListener(postListener);
Pemroses akan menerima DataSnapshot
yang berisi data di lokasi yang ditentukan dalam database saat peristiwa terjadi. Pemanggilan getValue()
pada snapshot akan menampilkan perwakilan objek Java data tersebut. Jika tidak ada data di lokasi, null
akan ditampilkan saat getValue()
dipanggil.
Dalam contoh ini, ValueEventListener
juga menentukan metode onCancelled()
yang dipanggil jika operasi baca dibatalkan. Misalnya, proses baca dapat dibatalkan jika klien tidak memiliki izin untuk membaca dari lokasi database Firebase. Metode ini mendapatkan objek DatabaseError
yang menunjukkan alasan terjadinya kegagalan.
Membaca data sekali
Membaca sekali menggunakan get()
SDK dirancang untuk mengelola interaksi dengan server database, baik saat aplikasi Anda online maupun offline.
Biasanya, Anda harus menggunakan teknik ValueEventListener
yang dijelaskan di atas untuk membaca data agar mendapatkan notifikasi terkait perubahan data dari backend. Teknik pemroses mengurangi penggunaan dan penagihan Anda, serta dioptimalkan untuk memberikan pengalaman terbaik kepada pengguna saat mereka 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 menyelidiki cache penyimpanan lokal dan menampilkan error jika nilainya masih belum ditemukan.
Penggunaan get()
yang tidak perlu dapat meningkatkan penggunaan bandwidth dan menyebabkan penurunan performa. Ini dapat dicegah menggunakan pemroses realtime seperti yang ditunjukkan di atas.
Kotlin+KTX
mDatabase.child("users").child(userId).get().addOnSuccessListener {
Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
Log.e("firebase", "Error getting data", it)
}
Java
mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
@Override
public void onComplete(@NonNull Task<DataSnapshot> task) {
if (!task.isSuccessful()) {
Log.e("firebase", "Error getting data", task.getException());
}
else {
Log.d("firebase", String.valueOf(task.getResult().getValue()));
}
}
});
Membaca sekali menggunakan pemroses
Dalam beberapa kasus, Anda mungkin menginginkan nilai dari cache lokal langsung ditampilkan, daripada memeriksa nilai yang diperbarui di server. Dalam kasus tersebut, Anda dapat menggunakan addListenerForSingleValueEvent
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 pengguna mulai membuat postingan baru.
Memperbarui atau menghapus data
Memperbarui kolom tertentu
Untuk menulis secara simultan ke turunan tertentu sebuah node tanpa menimpa node turunan yang lain, gunakan metode updateChildren()
.
Saat memanggil updateChildren()
, Anda dapat memperbarui nilai turunan di level yang lebih rendah dengan menentukan jalur untuk kunci. 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 memiliki class Post
seperti ini:
Kotlin+KTX
@IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", var body: String? = "", var starCount: Int = 0, var stars: MutableMap<String, Boolean> = HashMap(), ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, "body" to body, "starCount" to starCount, "stars" to stars, ) } }
Java
@IgnoreExtraProperties public class Post { public String uid; public String author; public String title; public String body; public int starCount = 0; public Map<String, Boolean> stars = new HashMap<>(); public Post() { // Default constructor required for calls to DataSnapshot.getValue(Post.class) } public Post(String uid, String author, String title, String body) { this.uid = uid; this.author = author; this.title = title; this.body = body; } @Exclude public Map<String, Object> toMap() { HashMap<String, Object> result = new HashMap<>(); result.put("uid", uid); result.put("author", author); result.put("title", title); result.put("body", body); result.put("starCount", starCount); result.put("stars", stars); return result; } }
Untuk membuat postingan dan memperbaruinya ke feed aktivitas terbaru sekaligus ke feed aktivitas pengguna yang memposting, aplikasi blogging tersebut menggunakan kode seperti ini:
Kotlin+KTX
private fun writeNewPost(userId: String, username: String, title: String, body: String) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously val key = database.child("posts").push().key if (key == null) { Log.w(TAG, "Couldn't get push key for posts") return } val post = Post(userId, username, title, body) val postValues = post.toMap() val childUpdates = hashMapOf<String, Any>( "/posts/$key" to postValues, "/user-posts/$userId/$key" to postValues, ) database.updateChildren(childUpdates) }
Java
private void writeNewPost(String userId, String username, String title, String body) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously String key = mDatabase.child("posts").push().getKey(); Post post = new Post(userId, username, title, body); Map<String, Object> postValues = post.toMap(); Map<String, Object> childUpdates = new HashMap<>(); childUpdates.put("/posts/" + key, postValues); childUpdates.put("/user-posts/" + userId + "/" + key, postValues); mDatabase.updateChildren(childUpdates); }
Contoh ini menggunakan push()
untuk membuat postingan dalam node yang berisi postingan bagi semua pengguna di /posts/$postid
, sekaligus mengambil kunci dengan getKey()
. 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 updateChildren()
, 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 telah di-commit, Anda bisa menambahkan pemroses penyelesaian. setValue()
dan updateChildren()
menerima pemroses penyelesaian opsional yang dipanggil ketika operasi tulis telah berhasil di-commit ke database. Jika panggilan tidak berhasil, pemroses akan diberi objek error yang menunjukkan penyebab kegagalannya.
Kotlin+KTX
database.child("users").child(userId).setValue(user) .addOnSuccessListener { // Write was successful! // ... } .addOnFailureListener { // Write failed // ... }
Java
mDatabase.child("users").child(userId).setValue(user) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Write was successful! // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Write failed // ... } });
Menghapus data
Cara termudah untuk menghapus data adalah dengan memanggil removeValue()
pada referensi ke lokasi data tersebut.
Penghapusan juga dapat dilakukan dengan menentukan null
sebagai nilai untuk operasi tulis lainnya, seperti setValue()
atau updateChildren()
. Teknik ini dapat digunakan dengan updateChildren()
untuk menghapus beberapa turunan dengan satu panggilan API.
Melepas pemroses
Callback dihapus dengan memanggil metode removeEventListener()
pada referensi database Firebase.
Jika telah ditambahkan beberapa kali ke lokasi data, pemroses akan dipanggil beberapa kali untuk setiap peristiwa, dan Anda harus melepasnya dalam jumlah yang sama seperti saat menambahkannya agar terhapus semuanya.
Memanggil removeEventListener()
pada pemroses induk tidak akan otomatis menghapus pemroses yang terdaftar pada node turunannya. removeEventListener()
juga harus dipanggil pada pemroses turunan mana pun untuk menghapus callback.
Menyimpan data sebagai transaksi
Ketika menangani data yang bisa rusak karena perubahan serentak, seperti penghitung pertambahan inkremental, Anda dapat menggunakan operasi transaksi. Operasi ini menggunakan dua argumen: fungsi pembaruan dan callback penyelesaian opsional. Fungsi pembaruan mengambil status data saat ini sebagai argumen, dan akan menampilkan status baru yang ingin Anda tulis. Jika klien lain melakukan operasi tulis ke lokasi ini sebelum nilai baru Anda berhasil ditulis, fungsi pembaruan Anda akan dipanggil lagi dengan nilai saat ini yang baru, dan operasi tulis akan dicoba ulang.
Misalnya, pada contoh aplikasi blogging sosial, Anda dapat mengizinkan pengguna memberi bintang atau menghapus bintang pada postingan, serta mengetahui berapa banyak bintang yang telah diterima suatu postingan dengan cara berikut ini:
Kotlin+KTX
private fun onStarClicked(postRef: DatabaseReference) { // ... postRef.runTransaction(object : Transaction.Handler { override fun doTransaction(mutableData: MutableData): Transaction.Result { val p = mutableData.getValue(Post::class.java) ?: return Transaction.success(mutableData) if (p.stars.containsKey(uid)) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1 p.stars.remove(uid) } else { // Star the post and add self to stars p.starCount = p.starCount + 1 p.stars[uid] = true } // Set value and report transaction success mutableData.value = p return Transaction.success(mutableData) } override fun onComplete( databaseError: DatabaseError?, committed: Boolean, currentData: DataSnapshot?, ) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError!!) } }) }
Java
private void onStarClicked(DatabaseReference postRef) { postRef.runTransaction(new Transaction.Handler() { @NonNull @Override public Transaction.Result doTransaction(@NonNull MutableData mutableData) { Post p = mutableData.getValue(Post.class); if (p == null) { return Transaction.success(mutableData); } if (p.stars.containsKey(getUid())) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1; p.stars.remove(getUid()); } else { // Star the post and add self to stars p.starCount = p.starCount + 1; p.stars.put(getUid(), true); } // Set value and report transaction success mutableData.setValue(p); return Transaction.success(mutableData); } @Override public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot currentData) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError); } }); }
Menggunakan transaksi akan mencegah kesalahan penghitungan jumlah bintang jika beberapa pengguna memberi bintang pada postingan yang sama secara bersamaan, atau klien memiliki data yang sudah usang. Jika transaksi ditolak, server akan menampilkan nilai saat ini ke klien yang akan menjalankan lagi transaksi tersebut dengan nilai yang diupdate. Proses ini akan berulang hingga transaksi diterima atau ada terlalu banyak percobaan yang dilakukan.
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.
Kotlin+KTX
private fun onStarClicked(uid: String, key: String) { val updates: MutableMap<String, Any> = hashMapOf( "posts/$key/stars/$uid" to true, "posts/$key/starCount" to ServerValue.increment(1), "user-posts/$uid/$key/stars/$uid" to true, "user-posts/$uid/$key/starCount" to ServerValue.increment(1), ) database.updateChildren(updates) }
Java
private void onStarClicked(String uid, String key) { Map<String, Object> updates = new HashMap<>(); updates.put("posts/"+key+"/stars/"+uid, true); updates.put("posts/"+key+"/starCount", ServerValue.increment(1)); updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true); updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1)); mDatabase.updateChildren(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 mempertahankan setiap data versi internalnya sendiri yang menggunakan pemroses atau yang ditandai agar tetap sinkron dengan server. Saat data dibaca atau ditulis, versi lokal data ini digunakan terlebih dahulu. Selanjutnya, klien Firebase menyinkronkan data tersebut dengan server database di tempat lain, dan dengan klien lain berdasarkan "upaya terbaik".
Akibatnya, semua operasi tulis ke database akan segera memicu peristiwa lokal, sebelum ada interaksi dengan server. Artinya, aplikasi Anda akan tetap responsif, apa pun kondisi latensi atau konektivitas 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 dalam artikel Mempelajari lebih lanjut kemampuan online dan offline.
Langkah berikutnya
- Menangani daftar data
- Mempelajari cara membuat struktur data
- Mempelajari lebih lanjut kemampuan online dan offline