Đọc và ghi dữ liệu trên Android

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

Tài liệu này trình bày những kiến ​​thức cơ bản về đọc và ghi dữ liệu Firebase.

Dữ liệu Firebase được ghi vào tham chiếu FirebaseDatabase và được truy xuất bằng cách đính kèm trình nghe không đồng bộ vào tham chiếu. Trình nghe được kích hoạt một lần cho trạng thái ban đầu của dữ liệu và một lần nữa bất cứ khi nào dữ liệu thay đổi.

(Tùy chọn) Nguyên mẫu và thử nghiệm với Firebase Local Emulator Suite

Trước khi nói về cách ứng dụng của bạn đọc và ghi vào Cơ sở dữ liệu thời gian thực, hãy giới thiệu một bộ công cụ bạn có thể sử dụng để tạo nguyên mẫu và kiểm tra chức năng của Cơ sở dữ liệu thời gian thực: Firebase Local Emulator Suite. Nếu bạn đang thử các mô hình dữ liệu khác nhau, tối ưu hóa các quy tắc bảo mật của mình hoặc đang tìm cách hiệu quả nhất về chi phí để tương tác với back-end, thì khả năng làm việc cục bộ mà không cần triển khai các dịch vụ trực tiếp có thể là một ý tưởng tuyệt vời.

Trình giả lập Cơ sở dữ liệu thời gian thực là một phần của Bộ giả lập cục bộ, 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, cũng như tùy chọn các tài nguyên dự án được mô phỏng của bạn (chức năng, cơ sở dữ liệu khác và quy tắc bảo mật).

Việc sử dụng trình giả lập Cơ sở dữ liệu thời gian thực chỉ bao gồm một vài bước:

  1. Thêm một dòng mã vào cấu hình thử nghiệm của ứng dụng để kết nối với trình giả lập.
  2. Từ thư mục gốc của thư mục dự án cục bộ của bạn, hãy chạy firebase emulators:start .
  3. Thực hiện lệnh gọi từ mã nguyên mẫu của ứng dụng bằng cách sử dụng SDK nền tảng Cơ sở dữ liệu thời gian thực như bình thường hoặc sử dụng API REST của Cơ sở dữ liệu thời gian thực.

Đã có hướng dẫn chi tiết liên quan đến Cơ sở dữ liệu thời gian thực và Chức năng đám mây . Bạn cũng nên xem phần giới thiệu Bộ giả lập cục bộ .

Nhận một tham chiếu cơ sở dữ liệu

Để đọc hoặc ghi dữ liệu từ cơ sở dữ liệu, bạn cần một phiên bản 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 tham chiếu đã chỉ định, 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 để:

  • Chuyển các loại tương ứng với các loại JSON có sẵn như sau:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Truyền một đối tượng Java tùy chỉnh, nếu lớp định nghĩa nó có hàm tạo mặc định không nhận đối số và có trình thu thập công khai cho các thuộc tính được gán.

Nếu bạn sử dụng một đối tượng Java, nội dung của đối tượng của bạn sẽ tự động được ánh xạ tới các vị trí con theo kiểu lồng nhau. Việc sử dụng một đối tượng Java cũng thường làm cho mã của bạn dễ đọc hơn 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, thì đối tượng User của bạn có thể trô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 bằng 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);
}

Sử dụng setValue() theo cách này sẽ ghi đè dữ liệu tại vị trí đã chỉ định, bao gồm mọi nút con. Tuy nhiên, bạn vẫn có thể cập nhật một đối tượng con 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 với người nghe liên tục

Để đọc dữ liệu tại một đường dẫn và lắng nghe các thay đổi, hãy sử dụng phương thức addValueEventListener() để thêm ValueEventListener vào DatabaseReference .

Thính giả gọi lại sự kiện sử dụng điển hình
ValueEventListener onDataChange() Đọc và lắng nghe những thay đổi đối với toàn bộ nội dung của một đườ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 đường dẫn nhất định, vì chúng đã tồn tại tại thời điểm diễn ra sự kiện. Phương pháp này được kích hoạt một lần khi người nghe được đính kèm và một lần nữa mỗi khi dữ liệu, bao gồm cả trẻ em, thay đổi. Cuộc gọi lại sự kiện được chuyển qua một ảnh chụp nhanh chứa tất cả dữ liệu tại vị trí đó, bao gồm cả dữ liệu con. Nếu không có dữ liệu, ảnh chụp nhanh sẽ trả về false khi bạn gọi exists()null khi bạn gọi getValue() trên đó.

Ví dụ sau minh họa một ứng dụng viết blog xã hội truy xuất thông tin chi tiết của bài đăng từ 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);

Người nghe nhận được một DataSnapshot chứa dữ liệu tại vị trí đã chỉ định trong cơ sở dữ liệu tại thời điểm xảy ra sự kiện. Việc gọi getValue() trên ảnh chụp nhanh sẽ trả về biểu diễn đối tượng Java của dữ liệu. Nếu không có dữ liệu nào tồn tại tại vị trí, việc gọi getValue() sẽ trả về null .

Trong ví dụ này, ValueEventListener cũng định nghĩa phương thức onCancelled() được gọi nếu quá trình đọc bị hủy. Ví dụ: một lần đọc có thể bị hủy nếu ứng dụng khách không có quyền đọc từ vị trí cơ sở dữ liệu Firebase. Phương thức này đượ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 cách sử dụng get()

SDK được thiết kế để quản lý các tương tác với máy chủ cơ sở dữ liệu cho dù ứng dụng của bạn trực tuyến hay ngoại tuyến.

Nói chung, bạn nên sử dụng các kỹ thuật ValueEventListener được mô tả ở trên để đọc dữ liệu nhằm nhận thông báo về các bản cập nhật dữ liệu từ chương trình phụ trợ. Các kỹ thuật của trình nghe giúp giảm mức sử dụng và thanh toán của bạn, đồng thời được tối ưu hóa để mang đến cho người dùng của bạn trải nghiệm tốt nhất khi họ trực tuyến và ngoại tuyến.

Nếu bạn chỉ cần dữ liệu một lần, bạn có thể sử dụng get() để lấy ảnh chụp nhanh dữ liệu từ cơ sở dữ liệu. Nếu vì bất kỳ lý do gì, get() không thể trả về giá trị máy chủ, máy khách sẽ thăm dò bộ đệm lưu trữ cục bộ và trả về lỗi nếu 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 giảm hiệu suất, điều này có thể được ngăn chặn bằng cách sử dụng trình nghe thời gian thực như minh họa ở 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 cách sử dụng một người nghe

Trong một số trường hợp, bạn có thể muốn trả về giá trị từ bộ nhớ cache 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 những trường hợp đó, bạn có thể sử dụng addListenerForSingleValueEvent để lấy dữ liệu từ bộ đệm đĩa cục bộ ngay lập tức.

Điều này hữu ích cho dữ liệu chỉ cần được tải một lần và dự kiến ​​sẽ không thay đổi thường xuyên hoặc yêu cầu lắng nghe tích cực. Chẳng hạn, ứng dụng viết blog trong các ví dụ trước sử dụng phương pháp 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 xóa dữ liệu

Cập nhật các trường cụ thể

Để ghi đồng thời cho các nút con cụ thể của một nút mà không ghi đè lên các nút con khác, 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 khóa. 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 phân xuất dữ liệu . Ví dụ: một ứng dụng viết blog xã hội có thể có một lớp Post như thế này:

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 và đồng thời cập nhật nó vào nguồn cấp dữ liệu 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ử 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 một bài đăng trong nút chứa các bài đăng cho tất cả người dùng tại /posts/$postid và đồng thời truy xuất khóa bằng getKey() . Sau đó, khóa có thể được sử dụng để tạo mục nhập thứ hai trong bài đăng của người dùng tại /user-posts/$userid/$postid .

Sử dụng các đường dẫn này, bạn có thể thực hiện cập nhật đồng thời cho nhiều vị trí trong cây JSON bằng một lệnh gọi duy nhất đến updateChildren() , chẳng hạn như cách ví dụ này tạo bài đăng mới ở cả hai vị trí. Các bản cập nhật đồng thời được thực hiện theo cách này là nguyên tử: tất cả các bản cập nhật đều thành công hoặc tất cả các bản cập nhật đều không thành công.

Thêm một cuộc gọi lại hoàn thành

Nếu bạn muốn biết khi nào dữ liệu của mình được cam kết, bạn có thể thêm trình xử lý hoàn thành. Cả setValue()updateChildren() đều có một trình lắng nghe hoàn thành tùy chọn được gọi khi quá trình ghi đã được chuyển thành công vào cơ sở dữ liệu. Nếu cuộc gọi không thành công, người nghe sẽ được chuyể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 để xóa dữ liệu là gọi removeValue() trên tham chiếu đến vị trí của dữ liệu đó.

Bạn cũng có thể xóa bằng cách chỉ định null làm giá trị cho thao tác ghi khác, chẳng hạn như setValue() hoặc updateChildren() . Bạn có thể sử dụng kỹ thuật này với updateChildren() để xóa nhiều phần tử con trong một lệnh gọi API.

Tách người nghe

Các cuộc gọi lại được loại bỏ bằng cách gọi phương thức removeEventListener() trên tham chiếu cơ sở dữ liệu Firebase của bạn.

Nếu một trình nghe đã được thêm nhiều lần vào một vị trí dữ liệu, thì trình nghe đó được gọi nhiều lần cho mỗi sự kiện và bạn phải tách trình nghe đó nhiều lần để xóa hoàn toàn.

Việc gọi removeEventListener() trên một bộ lắng nghe chính không tự động xóa các bộ nghe đã đăng ký trên các nút con của nó; removeEventListener() cũng phải được gọi trên bất kỳ người nghe con nào để xóa cuộc 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 sửa đổi đồng thời, chẳng hạn như bộ đếm gia tăng, bạn có thể sử dụng thao tác giao dịch . Bạn cung cấp cho thao tác này hai đối số: hàm cập nhật và hàm gọi lại hoàn thành tùy chọn. Hàm cập nhật lấy trạng thái hiện tại của dữ liệu làm đối số và trả về trạng thái mong muốn mới mà bạn muốn ghi. Nếu một ứng dụng khách khác ghi vào vị trí trước khi giá trị mới của bạn được ghi thành công, hàm cập nhật của bạn sẽ được gọi lại với giá trị hiện tại mới và quá trình ghi sẽ được thử lại.

Chẳng hạn, trong ứng dụng viết blog xã hội mẫu, bạn có thể cho phép người dùng gắn dấu sao và bỏ gắn dấu sao cho các bài đăng và theo dõi số lượng 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() {
        @Override
        public Transaction.Result doTransaction(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 ngăn số lượng sao không chính xác nếu nhiều người dùng gắn sao cùng một bài đăng cùng một lúc hoặc khách hàng có dữ liệu cũ. Nếu giao dịch bị từ chối, máy chủ sẽ trả về giá trị hiện tại cho máy khách, ứng dụng này sẽ chạy lại giao dịch với giá trị được cập nhật. Điều này lặp lại cho đến khi giao dịch được chấp nhận hoặc đã thực hiện quá nhiều lần.

Gia tăng phía máy chủ nguyên tử

Trong trường hợp sử dụng ở trên, chúng tôi đang ghi hai giá trị vào cơ sở dữ liệu: ID của người dùng đã gắn dấu sao/bỏ gắn dấu sao bài đăng và số dấu sao tăng dần. Nếu chúng tôi đã biết người dùng đó đang gắn dấu sao cho bài đăng, chúng tôi có thể sử dụng thao tác gia tăng nguyên tử thay vì 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 thao tác giao dịch, vì vậy mã này không tự động chạy lại nếu có bản cập nhật xung đột. Tuy nhiên, vì thao tác gia tăng diễn ra trực tiếp trên máy chủ cơ sở dữ liệu nên sẽ 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 các xung đột dành riêng cho ứng dụng, chẳng hạn như người dùng gắn dấu sao cho bài đăng mà họ đã gắn dấu sao trước đó, thì bạn nên viết các quy tắc bảo mật tùy chỉnh cho trường hợp sử dụng đó.

Làm việc với dữ liệu ngoại tuyến

Nếu khách hàng mất kết nối mạng, ứng dụng của bạn sẽ tiếp tục hoạt động bình thường.

Mọi máy khách được kết nối với cơ sở dữ liệu Firebase đều duy trì phiên bản nội bộ của bất kỳ dữ liệu nào mà trình nghe đang được sử dụng hoặc được gắn cờ để đồng bộ hóa 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 sẽ được sử dụng trước tiên. Sau đó, ứng dụng khách Firebase sẽ đồng bộ hóa dữ liệu đó với các máy chủ cơ sở dữ liệu từ xa và với các ứng dụng khách khác trên cơ sở "nỗ lực tối đa".

Do đó, tất cả các thao tác 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 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 phản hồi bất kể độ trễ mạng hoặc khả năng kết nối.

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 sự kiện thích hợp để 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ã tùy chỉnh nào.

Chúng ta sẽ nói nhiều hơn về hành vi ngoại tuyến trong Tìm hiểu thêm về các khả năng trực tuyến và ngoại tuyến .

Bước tiếp theo