Membaca dan Menulis Data di platform Apple

(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:

  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 Local Emulator Suite.

Mendapatkan FIRDatabaseReference

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

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

Menulis data

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

Data Firebase dituliskan ke referensi Database dan diambil dengan menambahkan pemroses asinkron ke referensi tersebut. Pemroses dipicu satu kali untuk status awal data, dan dipicu lagi setiap kali data berubah.

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:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Misalnya, Anda dapat menambahkan pengguna dengan setValue seperti berikut:

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

Penggunaan setValue 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 profil mereka, Anda dapat memperbarui nama pengguna seperti berikut:

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

Membaca data

Membaca data dengan mendeteksi peristiwa nilai

Untuk membaca data di jalur dan mendeteksi perubahan, gunakan observeEventType:withBlock dari FIRDatabaseReference untuk mengamati peristiwa FIRDataEventTypeValue.

Jenis peristiwa Penggunaan standar
FIRDataEventTypeValue Membaca dan memproses perubahan di seluruh konten jalur.

Anda dapat menggunakan peristiwa FIRDataEventTypeValue untuk membaca data di jalur tertentu, sesuai kondisi pada saat peristiwa terjadi. Metode ini dipicu satu kali saat pemroses ditambahkan dan dipicu lagi setiap kali terjadi perubahan pada data, termasuk pada setiap turunannya. Callback peristiwa diberikan snapshot yang berisi semua data di jalur tersebut, termasuk data turunan. Jika tidak ada data, snapshot akan menampilkan false ketika Anda memanggil exists() dan nil ketika Anda membaca properti value miliknya.

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

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

Pemroses menerima FIRDataSnapshot yang memuat data di lokasi yang ditentukan dalam database saat terjadi peristiwa di properti value miliknya. Anda dapat menetapkan nilai ke jenis bawaan yang sesuai, seperti NSDictionary. Jika tidak ada data di lokasi tersebut, value adalah nil.

Membaca data sekali

Membaca sekali menggunakan getData()

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 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 getData() untuk mendapatkan snapshot data dari database. Jika karena alasan apa pun getData() 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:

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid];
[[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) {
  if (error) {
    NSLog(@"Received an error %@", error);
    return;
  }
  NSString *userName = snapshot.value;
}];

Penggunaan getData() 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 observer

Dalam beberapa kasus, Anda mungkin menginginkan nilai dari cache lokal segera ditampilkan, daripada memeriksa nilai yang diperbarui di server. Dalam kasus tersebut, Anda dapat menggunakan observeSingleEventOfType 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:

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
let userID = Auth.auth().currentUser?.uid
ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in
  // Get user value
  let value = snapshot.value as? NSDictionary
  let username = value?["username"] as? String ?? ""
  let user = User(username: username)

  // ...
}) { error in
  print(error.localizedDescription)
}

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
NSString *userID = [FIRAuth auth].currentUser.uid;
[[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  // Get user value
  User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]];

  // ...
} withCancelBlock:^(NSError * _Nonnull error) {
  NSLog(@"%@", error.localizedDescription);
}];

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 updateChildValues.

Saat memanggil updateChildValues, 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:

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
guard let key = ref.child("posts").childByAutoId().key else { return }
let post = ["uid": userID,
            "author": username,
            "title": title,
            "body": body]
let childUpdates = ["/posts/\(key)": post,
                    "/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
NSString *key = [[_ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"uid": userID,
                       @"author": username,
                       @"title": title,
                       @"body": body};
NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post,
                               [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post};
[_ref updateChildValues:childUpdates];

Contoh ini menggunakan childByAutoId guna membuat postingan dalam node yang berisi postingan untuk 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 updateChildValues, 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 Blok Penyelesaian

Jika Anda ingin mengetahui kapan data di-commit, Anda bisa menambahkan blok penyelesaian. setValue dan updateChildValues memerlukan blok penyelesaian opsional yang dipanggil ketika operasi tulis telah di-commit ke database. Pemroses ini dapat berguna untuk memantau data mana yang sudah disimpan dan data mana yang masih disinkronkan. Jika panggilan tidak berhasil, pemroses akan diberi objek error yang menunjukkan penyebab kegagalan tersebut.

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
do {
  try await ref.child("users").child(user.uid).setValue(["username": username])
  print("Data saved successfully!")
} catch {
  print("Data could not be saved: \(error).")
}

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
  if (error) {
    NSLog(@"Data could not be saved: %@", error);
  } else {
    NSLog(@"Data saved successfully.");
  }
}];

Menghapus data

Cara termudah untuk menghapus data adalah dengan memanggil removeValue pada referensi ke lokasi data tersebut.

Penghapusan juga dapat dilakukan dengan menentukan nil sebagai nilai untuk operasi tulis lainnya, seperti setValue atau updateChildValues. Teknik ini dapat digunakan dengan updateChildValues untuk menghapus beberapa turunan dengan satu panggilan API.

Melepas pemroses

Observer tidak akan otomatis menghentikan sinkronisasi data saat Anda meninggalkan ViewController. Jika tidak dihapus dengan benar, observer akan terus menyinkronkan data ke memori lokal. Jika tidak lagi diperlukan, hapus observer dengan meneruskan FIRDatabaseHandle terkait ke metode removeObserverWithHandle.

Saat pemblokiran callback ditambahkan ke referensi, FIRDatabaseHandle akan ditampilkan. Handle ini dapat digunakan untuk menghapus pemblokiran callback.

Jika beberapa pemroses ditambahkan ke referensi database, setiap pemroses akan dipanggil ketika terjadi peristiwa. Untuk menghentikan sinkronisasi data di lokasi tersebut, Anda harus menghapus semua observer di sebuah lokasi dengan memanggil metode removeAllObservers.

Memanggil removeObserverWithHandle atau removeAllObservers pada pemroses tidak akan otomatis menghapus pemroses yang terdaftar pada node turunannya. Anda juga harus tetap melacak referensi atau handle tersebut untuk menghapusnya.

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 kondisi data saat ini sebagai argumen dan menampilkan kondisi baru yang ingin Anda tuliskan.

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

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
  if var post = currentData.value as? [String: AnyObject],
    let uid = Auth.auth().currentUser?.uid {
    var stars: [String: Bool]
    stars = post["stars"] as? [String: Bool] ?? [:]
    var starCount = post["starCount"] as? Int ?? 0
    if let _ = stars[uid] {
      // Unstar the post and remove self from stars
      starCount -= 1
      stars.removeValue(forKey: uid)
    } else {
      // Star the post and add self to stars
      starCount += 1
      stars[uid] = true
    }
    post["starCount"] = starCount as AnyObject?
    post["stars"] = stars as AnyObject?

    // Set value and report transaction success
    currentData.value = post

    return TransactionResult.success(withValue: currentData)
  }
  return TransactionResult.success(withValue: currentData)
}) { error, committed, snapshot in
  if let error = error {
    print(error.localizedDescription)
  }
}

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) {
  NSMutableDictionary *post = currentData.value;
  if (!post || [post isEqual:[NSNull null]]) {
    return [FIRTransactionResult successWithValue:currentData];
  }

  NSMutableDictionary *stars = post[@"stars"];
  if (!stars) {
    stars = [[NSMutableDictionary alloc] initWithCapacity:1];
  }
  NSString *uid = [FIRAuth auth].currentUser.uid;
  int starCount = [post[@"starCount"] intValue];
  if (stars[uid]) {
    // Unstar the post and remove self from stars
    starCount--;
    [stars removeObjectForKey:uid];
  } else {
    // Star the post and add self to stars
    starCount++;
    stars[uid] = @YES;
  }
  post[@"stars"] = stars;
  post[@"starCount"] = @(starCount);

  // Set value and report transaction success
  currentData.value = post;
  return [FIRTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError * _Nullable error,
                       BOOL committed,
                       FIRDataSnapshot * _Nullable snapshot) {
  // Transaction completed
  if (error) {
    NSLog(@"%@", error.localizedDescription);
  }
}];

Penggunaan transaksi dapat mencegah kesalahan penghitungan jumlah bintang jika beberapa pengguna memberi bintang pada postingan yang sama secara bersamaan, atau jika klien memiliki data yang sudah usang. Nilai yang dimuat pada class FIRMutableData pada awalnya adalah nilai terakhir klien yang diketahui untuk jalur tersebut, atau nil jika nilainya tidak ada. Server membandingkan nilai awal tersebut dengan nilai saat ini dan akan menerima transaksi jika nilainya cocok, atau menolaknya. Jika transaksi ditolak, server akan menampilkan nilai saat ini ke klien, yang akan mengulangi transaksi tersebut dengan nilai yang telah diperbarui. Proses ini berulang hingga transaksi diterima atau ada terlalu banyak percobaan yang telah dilakukan.

Pertambahan inkremental sisi server atomik

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.

Swift

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
let updates = [
  "posts/\(postID)/stars/\(userID)": true,
  "posts/\(postID)/starCount": ServerValue.increment(1),
  "user-posts/\(postID)/stars/\(userID)": true,
  "user-posts/\(postID)/starCount": ServerValue.increment(1)
] as [String : Any]
Database.database().reference().updateChildValues(updates)

Objective-C

Catatan: Produk Firebase ini tidak tersedia di target App Clip.
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1],
                        [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]};
[[[FIRDatabase database] reference] updateChildValues: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, sehingga klien dapat menyinkronkannya 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