Tài liệu này trình bày các kiến thức cơ bản về cách đọc và ghi dữ liệu Firebase.
Dữ liệu Firebase được ghi vào tệp tham chiếu FirebaseDatabase
và truy xuất bằng
đính kèm một trình nghe không đồng bộ vào tham chiếu. Trình nghe này được kích hoạt
một lần cho trạng thái ban đầu của dữ liệu và lặp lại một lần nữa bất cứ khi nào dữ liệu thay đổi.
(Không bắt buộc) Tạo nguyên mẫu và thử nghiệm bằng Firebase Local Emulator Suite
Trước khi nói về cách ứng dụng đọc và ghi vào Realtime Database, hãy giới thiệu một bộ công cụ mà bạn có thể dùng để tạo nguyên mẫu và kiểm thử Realtime Database chức năng: Firebase Local Emulator Suite. Nếu bạn đang thử dùng dữ liệu khác mô hình, tối ưu hoá quy tắc bảo mật hoặc nỗ lực tìm ra cách tiết kiệm chi phí để tương tác với hệ thống phụ trợ, nhờ đó có thể làm việc tại địa phương mà không triển khai dịch vụ trực tiếp có thể là ý tưởng hay.
Trình mô phỏng Realtime Database là một phần của Local Emulator Suite, cho phép ứng dụng của bạn tương tác với cấu hình và nội dung cơ sở dữ liệu được mô phỏng, như cũng như các tài nguyên dự án được mô phỏng (không bắt buộc) (các hàm, cơ sở dữ liệu khác, và quy tắc bảo mật).
Việc sử dụng trình mô phỏng Realtime Database chỉ bao gồm vài bước:
- Thêm một dòng mã vào cấu hình kiểm thử của ứng dụng để kết nối với trình mô phỏng.
- Trên gốc của thư mục dự án cục bộ, chạy
firebase emulators:start
. - Gọi điện từ mã nguyên mẫu của ứng dụng bằng nền tảng Realtime Database SDK như thường lệ hoặc sử dụng API REST Realtime Database.
Có sẵn hướng dẫn chi tiết liên quan đến Realtime Database và Cloud Functions. Bạn cũng nên xem giới thiệu về Local Emulator Suite.
Nhận một DatabaseReference
Để đọc hoặc ghi dữ liệu từ cơ sở dữ liệu, bạn cần có một thực thể của
DatabaseReference
:
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Ghi dữ liệu
Thao tác ghi cơ bản
Đối với các thao tác ghi cơ bản, bạn có thể sử dụng setValue()
để lưu dữ liệu vào một thuộc tính cụ thể
thay thế mọi dữ liệu hiện có tại đường dẫn đó. Bạn có thể sử dụng phương pháp này để:
- Sau đây là các loại thẻ và vé tương ứng với các loại JSON hiện có:
String
Long
Double
Boolean
Map<String, Object>
List<Object>
- Truyền một đối tượng Java tuỳ chỉnh, nếu lớp xác định đối tượng đó có giá trị mặc định hàm khởi tạo không nhận đối số và có các phương thức getter công khai cho các thuộc tính được chỉ định.
Nếu bạn sử dụng một đối tượng Java, nội dung của đối tượng đó sẽ tự động được ánh xạ
vào các vị trí con theo cách lồng ghép. Việc sử dụng đối tượng Java cũng thường giúp
mã dễ đọc và dễ bảo trì hơn. Ví dụ: nếu bạn có một
ứng dụng có hồ sơ người dùng cơ bản, đối tượng User
của bạn có thể có dạng như sau:
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; } }
Bạn có thể thêm người dùng có setValue()
như sau:
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); }
Việc sử dụng setValue()
theo cách này sẽ ghi đè dữ liệu tại vị trí được chỉ định,
bao gồm mọi nút con. Tuy nhiên, bạn vẫn có thể cập nhật tài khoản của trẻ mà không cần
viết lại toàn bộ đối tượng. Nếu bạn muốn cho phép người dùng cập nhật hồ sơ của họ
bạn có thể cập nhật tên người dùng như sau:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Đọc dữ liệu
Đọc dữ liệu bằng trình nghe liên tục
Để đọc dữ liệu tại một đường dẫn và theo dõi các thay đổi, hãy sử dụng addValueEventListener()
để thêm ValueEventListener
vào DatabaseReference
.
gửi biểu mẫu | Lệnh gọi lại sự kiện | Mức sử dụng thông thường |
---|---|---|
ValueEventListener |
onDataChange() |
Đọc và theo dõi các thay đổi đối với toàn bộ nội dung của đường dẫn. |
Bạn có thể sử dụng phương thức onDataChange()
để đọc ảnh chụp nhanh tĩnh của
nội dung tại một lộ trình nhất định, như đã tồn tại tại thời điểm diễn ra sự kiện. Phương thức này
được kích hoạt một lần khi trình nghe được đính kèm và lặp lại mỗi lần dữ liệu,
bao gồm cả trẻ em, các thay đổi. Lệnh gọi lại sự kiện được truyền một ảnh chụp nhanh chứa
tất cả dữ liệu ở vị trí đó, bao gồm cả dữ liệu của trẻ. Nếu không có dữ liệu,
ảnh chụp nhanh sẽ trả về false
khi bạn gọi exists()
và null
khi bạn gọi
getValue()
trên đó.
Ví dụ sau minh hoạ một ứng dụng viết blog trên mạng xã hội truy xuất thông tin chi tiết về một bài đăng trong cơ sở dữ liệu:
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);
Trình nghe sẽ nhận được một DataSnapshot
chứa dữ liệu theo phương thức đã chỉ định
vị trí trong cơ sở dữ liệu tại thời điểm diễn ra sự kiện. Đang gọi getValue()
trên
Tổng quan nhanh sẽ trả về bản trình bày đối tượng Java của dữ liệu. Nếu không có dữ liệu
tại vị trí đó, lệnh gọi getValue()
sẽ trả về null
.
Trong ví dụ này, ValueEventListener
cũng xác định phương thức onCancelled()
sẽ được gọi nếu quá trình đọc bị huỷ. Ví dụ: một lượt đọc có thể bị huỷ nếu
ứng dụng không có quyền đọc từ vị trí cơ sở dữ liệu Firebase. Chiến dịch này
sẽ được truyền một đối tượng DatabaseError
cho biết lý do xảy ra lỗi.
Đọc dữ liệu một lần
Đọc một lần bằng get()
SDK này được thiết kế để quản lý hoạt động tương tác với máy chủ cơ sở dữ liệu cho dù đang trực tuyến hoặc ngoại tuyến.
Thông thường, bạn nên sử dụng các kỹ thuật ValueEventListener
được mô tả ở trên
để đọc dữ liệu và nhận thông báo về nội dung cập nhật đối với dữ liệu từ phần phụ trợ. Chiến lược phát hành đĩa đơn
giảm mức sử dụng và thanh toán, đồng thời được tối ưu hoá để
cung cấp cho người dùng trải nghiệm tốt nhất khi họ trực tuyến và ngoại tuyến.
Nếu chỉ cần dữ liệu một lần, bạn có thể sử dụng get()
để có ảnh chụp nhanh
khỏi cơ sở dữ liệu. Nếu vì bất kỳ lý do gì, get()
không thể trả về máy chủ
máy khách sẽ thăm dò bộ nhớ đệm của bộ nhớ cục bộ và trả về lỗi nếu giá trị
vẫn không tìm thấy giá trị.
Việc sử dụng get()
không cần thiết có thể làm tăng mức sử dụng băng thông và dẫn đến mất
Bạn có thể ngăn chặn điều này bằng cách sử dụng trình nghe theo thời gian thực như minh hoạ ở trên.
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()));
}
}
});
Đọc một lần bằng trình nghe
Trong một số trường hợp, bạn có thể muốn trả về giá trị từ bộ nhớ đệm cục bộ
ngay lập tức, thay vì kiểm tra giá trị đã cập nhật trên máy chủ. Trong các
Bạn có thể sử dụng addListenerForSingleValueEvent
để lấy dữ liệu từ
bộ nhớ đệm của ổ đĩa cục bộ ngay lập tức.
Điều này hữu ích đối với dữ liệu chỉ cần tải một lần và dự kiến sẽ không thay đổi thường xuyên hoặc cần chủ động lắng nghe. Ví dụ: ứng dụng viết blog trong các ví dụ trước sẽ sử dụng phương thức này để tải hồ sơ của người dùng khi họ bắt đầu viết một bài đăng mới.
Cập nhật hoặc xoá dữ liệu
Cập nhật các trường cụ thể
Để ghi đồng thời vào các phần tử con cụ thể của một nút mà không ghi đè lên các nút khác
nút con, hãy sử dụng phương thức updateChildren()
.
Khi gọi updateChildren()
, bạn có thể cập nhật các giá trị con ở cấp thấp hơn bằng cách
chỉ định đường dẫn cho khoá. Nếu dữ liệu được lưu trữ ở nhiều vị trí để mở rộng quy mô
tốt hơn, bạn có thể cập nhật tất cả các phiên bản của dữ liệu đó bằng cách sử dụng
ngừng sử dụng dữ liệu. Ví dụ: một
ứng dụng viết blog qua mạng xã hội có thể có lớp Post
như sau:
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; } }
Để tạo một bài đăng đồng thời cập nhật bài đăng đó thành hoạt động gần đây và nguồn cấp dữ liệu hoạt động của người dùng đăng bài, ứng dụng viết blog sẽ sử dụng mã như sau:
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); }
Ví dụ này sử dụng push()
để tạo bài đăng trong nút chứa bài đăng cho
tất cả người dùng tại /posts/$postid
và truy xuất khoá đồng thời bằng
getKey()
. Sau đó, khoá này có thể được dùng để tạo mục nhập thứ hai trong tài khoản
bài đăng tại /user-posts/$userid/$postid
.
Khi sử dụng các đường dẫn này, bạn có thể cập nhật đồng thời nhiều vị trí trong
cây JSON với một lệnh gọi đến updateChildren()
, chẳng hạn như trong ví dụ này
tạo bài đăng mới ở cả hai vị trí. Cập nhật đồng thời theo cách này
rất quan trọng: tất cả cập nhật thành công hoặc tất cả cập nhật đều không thành công.
Thêm một lệnh gọi lại hoàn thành
Nếu muốn biết thời điểm dữ liệu của bạn đã được xác nhận, bạn có thể thêm
trình nghe hoàn thành. Cả setValue()
và updateChildren()
đều không bắt buộc
trình nghe hoàn thành được gọi khi quá trình ghi thành công
cam kết với cơ sở dữ liệu. Nếu cuộc gọi không thành công, trình nghe sẽ
đã truyền một đối tượng lỗi cho biết lý do xảy ra lỗi.
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 // ... } });
Xóa dữ liệu
Cách đơn giản nhất để xoá dữ liệu là gọi removeValue()
trên một tham chiếu đến
vị trí của dữ liệu đó.
Bạn cũng có thể xoá bằng cách chỉ định null
làm giá trị cho lần ghi khác
chẳng hạn như setValue()
hoặc updateChildren()
. Bạn có thể dùng kỹ thuật này
với updateChildren()
để xoá nhiều phần tử con trong một lệnh gọi API.
Tách trình nghe
Các lệnh gọi lại sẽ bị xoá bằng cách gọi phương thức removeEventListener()
trên
Tài liệu tham khảo về cơ sở dữ liệu Firebase.
Nếu bạn thêm một trình nghe nhiều lần vào một vị trí dữ liệu, được gọi nhiều lần cho mỗi sự kiện và bạn phải tách sự kiện đó cùng một số lần lần để xóa hoàn toàn dữ liệu đó.
Việc gọi removeEventListener()
trên trình nghe cha mẹ sẽ không
tự động xoá trình nghe đã đăng ký trên các nút con;
removeEventListener()
cũng phải được gọi trên mọi trình nghe con
để loại bỏ lệnh gọi lại.
Lưu dữ liệu dưới dạng giao dịch
Khi làm việc với dữ liệu có thể bị hỏng do đồng thời sửa đổi, chẳng hạn như bộ đếm tăng dần, bạn có thể sử dụng hoạt động giao dịch. Bạn cung cấp cho thao tác này hai đối số: một hàm cập nhật và một đối số không bắt buộc lệnh gọi lại hoàn thành. Hàm cập nhật lấy trạng thái hiện tại của dữ liệu làm một đối số và trả về trạng thái mong muốn mới mà bạn muốn viết. Nếu một khách hàng khác ghi vào vị trí đó trước khi giá trị mới của bạn được áp dụng thành công thì hàm cập nhật của bạn sẽ được gọi lại bằng giá trị hiện tại mới và quá trình ghi được thử lại.
Ví dụ: trong ứng dụng viết blog xã hội ví dụ, bạn có thể cho phép người dùng gắn dấu sao và bỏ gắn dấu sao bài đăng, đồng thời theo dõi số sao mà một bài đăng đã nhận được như sau:
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); } }); }
Việc sử dụng giao dịch sẽ giúp tránh việc tính số sao không chính xác nếu có nhiều người dùng gắn dấu sao cùng một bài đăng cùng một lúc hoặc ứng dụng khách có dữ liệu cũ. Nếu giao dịch bị từ chối, máy chủ trả về giá trị hiện tại cho máy khách, Thao tác này sẽ chạy lại giao dịch với giá trị được cập nhật. Thao tác này lặp lại cho đến khi giao dịch được chấp nhận hoặc bạn đã thử quá nhiều lần.
Số gia ở phía máy chủ nguyên tử
Trong trường hợp sử dụng trên, chúng ta viết hai giá trị vào cơ sở dữ liệu: mã nhận dạng của người dùng gắn dấu sao/bỏ gắn dấu sao bài đăng và số sao tăng lên. Nếu chúng tôi đã biết rằng người dùng đang gắn dấu sao bài đăng, chúng ta có thể sử dụng gia số nguyên tử thay vì một giao dịch.
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); }
Mã này không sử dụng hoạt động giao dịch nên không tự động nhận được chạy lại nếu có bản cập nhật xung đột. Tuy nhiên, vì phép toán tăng xảy ra trực tiếp trên máy chủ cơ sở dữ liệu, nên không có khả năng xảy ra xung đột.
Nếu bạn muốn phát hiện và từ chối xung đột dành riêng cho ứng dụng, chẳng hạn như một người dùng gắn dấu sao bài đăng mà họ đã gắn dấu sao trước đó, bạn nên viết quy tắc bảo mật cho trường hợp sử dụng đó.
Làm việc với dữ liệu khi không có mạng
Nếu máy khách bị mất kết nối mạng, ứng dụng của bạn sẽ tiếp tục hoạt động chính xác.
Mỗi ứng dụng được kết nối với cơ sở dữ liệu Firebase đều duy trì phiên bản nội bộ riêng về mọi dữ liệu mà trình nghe đang được sử dụng hoặc dữ liệu được gắn cờ để được giữ lại đồng bộ hoá với máy chủ. Khi dữ liệu được đọc hoặc ghi, phiên bản cục bộ này của dữ liệu được sử dụng đầu tiên. Sau đó, ứng dụng Firebase sẽ đồng bộ hoá dữ liệu đó với máy chủ cơ sở dữ liệu từ xa và với các ứng dụng khách khác nhờ "nỗ lực tối đa" cơ sở.
Do đó, tất cả hoạt động ghi vào cơ sở dữ liệu sẽ kích hoạt các sự kiện cục bộ ngay lập tức, trước khi bất kỳ tương tác nào với máy chủ. Điều này có nghĩa là ứng dụng của bạn vẫn thích ứng bất kể độ trễ hoặc khả năng kết nối mạng.
Sau khi kết nối được thiết lập lại, ứng dụng của bạn sẽ nhận được nhóm các sự kiện để máy khách đồng bộ hóa với trạng thái máy chủ hiện tại mà không phải viết bất kỳ mã tuỳ chỉnh nào.
Chúng ta sẽ nói thêm về hành vi ngoại tuyến trong Tìm hiểu thêm về các chức năng trực tuyến và ngoại tuyến.