Membaca dan Menulis Data di iOS

(Opsional) Membuat prototipe dan melakukan pengujian dengan Firebase Local 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 fungsi Realtime Database: Firebase Local 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 Local Emulator Suite yang memungkinkan aplikasi berinteraksi dengan konfigurasi dan konten database teremulasi, serta secara opsional dengan resource project teremulasi (fungsi, database lain, dan aturan keamanan).

Anda hanya perlu beberapa langkah untuk menggunakan emulator Realtime Database:

  1. Menambahkan 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 REST API Realtime Database.

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

var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

@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 FIRDatabase dan diambil dengan menambahkan pemroses asinkron ke referensi tersebut. Pemroses dipicu satu kali untuk mengetahui 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

self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

[[[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

self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

[[[[_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

refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

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

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

self.ref.child("users/\(user.uid)/username").getData { (error, snapshot) in
    if let error = error {
        print("Error getting data \(error)")
    }
    else if snapshot.exists() {
        print("Got data \(snapshot.value!)")
    }
    else {
        print("No data available")
    }
}

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 segera 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

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

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 ke turunan tertentu dari sebuah node secara simultan 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 dan sekaligus memperbaruinya ke feed aktivitas terbaru dan feed aktivitas pembuat postingan. Untuk melakukannya, aplikasi blogging tersebut menggunakan kode seperti ini:

Swift

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

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, dan 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 bersamaan ke beberapa lokasi di pohon JSON dengan sekali 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: entah semua pembaruan berhasil atau semuanya gagal.

Menambahkan Blok Penyelesaian

Jika Anda ingin mengetahui kapan data di-commit, Anda bisa menambahkan blok penyelesaian. setValue dan updateChildValues membawa 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

ref.child("users").child(user.uid).setValue(["username": username]) {
  (error:Error?, ref:DatabaseReference) in
  if let error = error {
    print("Data could not be saved: \(error).")
  } else {
    print("Data saved successfully!")
  }
}

Objective-C

[[[_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 sebuah 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 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

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

[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

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

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, seperti pengguna yang memberi bintang pada postingan yang telah dia beri bintang sebelumnya, 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 dari setiap data aktif. Ketika ditulis, data akan dituliskan ke versi lokal ini 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 data yang dituliskan ke server. Ini berarti 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, 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