Tài liệu này trình bày 4 phương thức để ghi dữ liệu vào Firebase Realtime Database: set, update, push và hỗ trợ giao dịch.
Các cách tiết kiệm dữ liệu
thiết lập | Ghi hoặc thay thế dữ liệu bằng một đường dẫn đã xác định, chẳng hạn như messages/users/<username> |
cập nhật | Cập nhật một số khoá cho một đường dẫn đã xác định mà không cần thay thế tất cả dữ liệu |
đẩy | Thêm vào danh sách dữ liệu trong cơ sở dữ liệu. Mỗi khi bạn đẩy một nút mới vào danh sách, cơ sở dữ liệu sẽ tạo một khoá duy nhất, chẳng hạn như messages/users/<unique-user-id>/<username> |
giao dịch | Sử dụng giao dịch khi làm việc với dữ liệu phức tạp có thể bị hỏng do các bản cập nhật đồng thời |
Lưu dữ liệu
Thao tác ghi cơ sở dữ liệu cơ bản là một tập hợp lưu dữ liệu mới vào tham chiếu cơ sở dữ liệu đã chỉ định, thay thế mọi dữ liệu hiện có tại đường dẫn đó. Để hiểu về tập hợp, chúng ta sẽ tạo một ứng dụng viết blog đơn giản. Dữ liệu cho ứng dụng của bạn được lưu trữ tại tham chiếu cơ sở dữ liệu này:
Java
final FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("server/saving-data/fireblog");
Node.js
// Import Admin SDK const { getDatabase } = require('firebase-admin/database'); // Get a database reference to our blog const db = getDatabase(); const ref = db.ref('server/saving-data/fireblog');
Python
# Import database module. from firebase_admin import db # Get a database reference to our blog. ref = db.reference('server/saving-data/fireblog')
Tìm
// Create a database client from App. client, err := app.Database(ctx) if err != nil { log.Fatalln("Error initializing database client:", err) } // Get a database reference to our blog. ref := client.NewRef("server/saving-data/fireblog")
Hãy bắt đầu bằng cách lưu một số dữ liệu người dùng. Chúng tôi sẽ lưu trữ từng người dùng bằng một tên người dùng riêng biệt, đồng thời lưu trữ họ tên và ngày sinh của họ. Vì mỗi người dùng sẽ có một tên người dùng riêng biệt, nên bạn nên sử dụng phương thức set ở đây thay vì phương thức push vì bạn đã có khoá và không cần tạo khoá.
Trước tiên, hãy tạo một tham chiếu cơ sở dữ liệu đến dữ liệu người dùng của bạn. Sau đó, sử dụng set()
/ setValue()
để lưu một đối tượng người dùng vào cơ sở dữ liệu cùng với tên người dùng, họ tên và ngày sinh của người dùng. Bạn có thể truyền một chuỗi, số, boolean, null
, mảng hoặc bất kỳ đối tượng JSON nào. Truyền null
sẽ xoá dữ liệu tại vị trí được chỉ định. Trong trường hợp này, bạn sẽ truyền cho nó một đối tượng:
Java
public static class User { public String date_of_birth; public String full_name; public String nickname; public User(String dateOfBirth, String fullName) { // ... } public User(String dateOfBirth, String fullName, String nickname) { // ... } } DatabaseReference usersRef = ref.child("users"); Map<String, User> users = new HashMap<>(); users.put("alanisawesome", new User("June 23, 1912", "Alan Turing")); users.put("gracehop", new User("December 9, 1906", "Grace Hopper")); usersRef.setValueAsync(users);
Node.js
const usersRef = ref.child('users'); usersRef.set({ alanisawesome: { date_of_birth: 'June 23, 1912', full_name: 'Alan Turing' }, gracehop: { date_of_birth: 'December 9, 1906', full_name: 'Grace Hopper' } });
Python
users_ref = ref.child('users') users_ref.set({ 'alanisawesome': { 'date_of_birth': 'June 23, 1912', 'full_name': 'Alan Turing' }, 'gracehop': { 'date_of_birth': 'December 9, 1906', 'full_name': 'Grace Hopper' } })
Tìm
// User is a json-serializable type. type User struct { DateOfBirth string `json:"date_of_birth,omitempty"` FullName string `json:"full_name,omitempty"` Nickname string `json:"nickname,omitempty"` } usersRef := ref.Child("users") err := usersRef.Set(ctx, map[string]*User{ "alanisawesome": { DateOfBirth: "June 23, 1912", FullName: "Alan Turing", }, "gracehop": { DateOfBirth: "December 9, 1906", FullName: "Grace Hopper", }, }) if err != nil { log.Fatalln("Error setting value:", err) }
Khi một đối tượng JSON được lưu vào cơ sở dữ liệu, các thuộc tính của đối tượng sẽ tự động được liên kết với các vị trí con của cơ sở dữ liệu theo kiểu lồng nhau. Giờ đây, nếu chuyển đến URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name, bạn sẽ thấy giá trị "Alan Turing". Bạn cũng có thể lưu dữ liệu trực tiếp vào một vị trí con:
Java
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing")); usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
Node.js
const usersRef = ref.child('users'); usersRef.child('alanisawesome').set({ date_of_birth: 'June 23, 1912', full_name: 'Alan Turing' }); usersRef.child('gracehop').set({ date_of_birth: 'December 9, 1906', full_name: 'Grace Hopper' });
Python
users_ref.child('alanisawesome').set({ 'date_of_birth': 'June 23, 1912', 'full_name': 'Alan Turing' }) users_ref.child('gracehop').set({ 'date_of_birth': 'December 9, 1906', 'full_name': 'Grace Hopper' })
Tìm
if err := usersRef.Child("alanisawesome").Set(ctx, &User{ DateOfBirth: "June 23, 1912", FullName: "Alan Turing", }); err != nil { log.Fatalln("Error setting value:", err) } if err := usersRef.Child("gracehop").Set(ctx, &User{ DateOfBirth: "December 9, 1906", FullName: "Grace Hopper", }); err != nil { log.Fatalln("Error setting value:", err) }
Hai ví dụ trên (ghi cả hai giá trị cùng lúc dưới dạng một đối tượng và ghi riêng các giá trị đó vào các vị trí con) sẽ dẫn đến việc cùng một dữ liệu được lưu vào cơ sở dữ liệu của bạn:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing" }, "gracehop": { "date_of_birth": "December 9, 1906", "full_name": "Grace Hopper" } } }
Ví dụ đầu tiên sẽ chỉ kích hoạt một sự kiện trên những ứng dụng đang theo dõi dữ liệu, trong khi ví dụ thứ hai sẽ kích hoạt hai sự kiện. Điều quan trọng cần lưu ý là nếu dữ liệu đã tồn tại ở usersRef
, thì phương pháp đầu tiên sẽ ghi đè dữ liệu đó, nhưng phương pháp thứ hai sẽ chỉ sửa đổi giá trị của từng nút con riêng biệt trong khi vẫn giữ nguyên các nút con khác của usersRef
.
Cập nhật dữ liệu đã lưu
Nếu muốn ghi vào nhiều thành phần con của một vị trí trong cơ sở dữ liệu cùng một lúc mà không ghi đè các nút con khác, bạn có thể dùng phương thức cập nhật như minh hoạ dưới đây:
Java
DatabaseReference hopperRef = usersRef.child("gracehop"); Map<String, Object> hopperUpdates = new HashMap<>(); hopperUpdates.put("nickname", "Amazing Grace"); hopperRef.updateChildrenAsync(hopperUpdates);
Node.js
const usersRef = ref.child('users'); const hopperRef = usersRef.child('gracehop'); hopperRef.update({ 'nickname': 'Amazing Grace' });
Python
hopper_ref = users_ref.child('gracehop') hopper_ref.update({ 'nickname': 'Amazing Grace' })
Tìm
hopperRef := usersRef.Child("gracehop") if err := hopperRef.Update(ctx, map[string]interface{}{ "nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating child:", err) }
Thao tác này sẽ cập nhật dữ liệu của Grace để thêm biệt hiệu của cô ấy. Nếu bạn đã sử dụng set thay vì update, thì thao tác này sẽ xoá cả full_name
và date_of_birth
khỏi hopperRef
.
Firebase Realtime Database cũng hỗ trợ các bản cập nhật nhiều đường dẫn. Điều này có nghĩa là giờ đây, update có thể cập nhật các giá trị ở nhiều vị trí trong cơ sở dữ liệu cùng một lúc. Đây là một tính năng mạnh mẽ giúp bạn chuẩn hoá dữ liệu. Khi sử dụng tính năng cập nhật nhiều đường dẫn, bạn có thể thêm biệt hiệu cho cả Grace và Alan cùng một lúc:
Java
Map<String, Object> userUpdates = new HashMap<>(); userUpdates.put("alanisawesome/nickname", "Alan The Machine"); userUpdates.put("gracehop/nickname", "Amazing Grace"); usersRef.updateChildrenAsync(userUpdates);
Node.js
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' });
Python
users_ref.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' })
Tìm
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisawesome/nickname": "Alan The Machine", "gracehop/nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating children:", err) }
Sau bản cập nhật này, cả Alan và Grace đều đã được thêm biệt hiệu:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing", "nickname": "Alan The Machine" }, "gracehop": { "date_of_birth": "December 9, 1906", "full_name": "Grace Hopper", "nickname": "Amazing Grace" } } }
Xin lưu ý rằng việc cố gắng cập nhật các đối tượng bằng cách ghi các đối tượng có đường dẫn được đưa vào sẽ dẫn đến hành vi khác. Hãy xem điều gì sẽ xảy ra nếu bạn thử cập nhật Grace và Alan theo cách này:
Java
Map<String, Object> userNicknameUpdates = new HashMap<>(); userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine")); userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace")); usersRef.updateChildrenAsync(userNicknameUpdates);
Node.js
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } });
Python
users_ref.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } })
Tìm
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisawesome": &User{Nickname: "Alan The Machine"}, "gracehop": &User{Nickname: "Amazing Grace"}, }); err != nil { log.Fatalln("Error updating children:", err) }
Điều này dẫn đến hành vi khác, cụ thể là ghi đè toàn bộ nút /users
:
{ "users": { "alanisawesome": { "nickname": "Alan The Machine" }, "gracehop": { "nickname": "Amazing Grace" } } }
Thêm lệnh gọi lại khi hoàn tất
Trong Node.js và Java Admin SDK, 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 một lệnh gọi lại hoàn tất. Cả phương thức đặt và cập nhật trong các SDK này đều có một lệnh gọi lại hoàn tất không bắt buộc được gọi khi thao tác ghi đã được xác nhận vào cơ sở dữ liệu. Nếu lệnh gọi không thành công vì một lý do nào đó, lệnh gọi lại sẽ được truyền một đối tượng lỗi cho biết lý do xảy ra lỗi. Trong Python và Go Admin SDK, tất cả các phương thức ghi đều đang chặn. Tức là các phương thức ghi sẽ không trả về cho đến khi các thao tác ghi được cam kết với cơ sở dữ liệu.
Java
DatabaseReference dataRef = ref.child("data"); dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { if (databaseError != null) { System.out.println("Data could not be saved " + databaseError.getMessage()); } else { System.out.println("Data saved successfully."); } } });
Node.js
dataRef.set('I\'m writing data', (error) => { if (error) { console.log('Data could not be saved.' + error); } else { console.log('Data saved successfully.'); } });
Lưu danh sách dữ liệu
Khi tạo danh sách dữ liệu, bạn cần lưu ý đến bản chất đa người dùng của hầu hết các ứng dụng và điều chỉnh cấu trúc danh sách cho phù hợp. Mở rộng ví dụ trên, hãy thêm bài đăng trên blog vào ứng dụng của bạn. Phản ứng đầu tiên của bạn có thể là sử dụng set để lưu trữ các phần tử con bằng chỉ mục số nguyên tự động tăng, chẳng hạn như sau:
// NOT RECOMMENDED - use push() instead! { "posts": { "0": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "1": { "author": "alanisawesome", "title": "The Turing Machine" } } }
Nếu người dùng thêm một bài đăng mới, bài đăng đó sẽ được lưu trữ dưới dạng /posts/2
. Cách này sẽ hiệu quả nếu chỉ có một tác giả thêm bài đăng, nhưng trong ứng dụng blog cộng tác của bạn, nhiều người dùng có thể thêm bài đăng cùng lúc. Nếu hai tác giả cùng ghi vào /posts/2
, thì một trong các bài đăng sẽ bị tác giả còn lại xoá.
Để giải quyết vấn đề này, các ứng dụng Firebase cung cấp một hàm push()
tạo ra một khoá duy nhất cho mỗi thành phần con mới. Bằng cách sử dụng các khoá con riêng biệt, một số ứng dụng có thể thêm các phần tử con vào cùng một vị trí cùng một lúc mà không phải lo lắng về xung đột ghi.
Java
public static class Post { public String author; public String title; public Post(String author, String title) { // ... } } DatabaseReference postsRef = ref.child("posts"); DatabaseReference newPostRef = postsRef.push(); newPostRef.setValueAsync(new Post("gracehop", "Announcing COBOL, a New Programming Language")); // We can also chain the two calls together postsRef.push().setValueAsync(new Post("alanisawesome", "The Turing Machine"));
Node.js
const newPostRef = postsRef.push(); newPostRef.set({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' }); // we can also chain the two calls together postsRef.push().set({ author: 'alanisawesome', title: 'The Turing Machine' });
Python
posts_ref = ref.child('posts') new_post_ref = posts_ref.push() new_post_ref.set({ 'author': 'gracehop', 'title': 'Announcing COBOL, a New Programming Language' }) # We can also chain the two calls together posts_ref.push().set({ 'author': 'alanisawesome', 'title': 'The Turing Machine' })
Tìm
// Post is a json-serializable type. type Post struct { Author string `json:"author,omitempty"` Title string `json:"title,omitempty"` } postsRef := ref.Child("posts") newPostRef, err := postsRef.Push(ctx, nil) if err != nil { log.Fatalln("Error pushing child node:", err) } if err := newPostRef.Set(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error setting value:", err) } // We can also chain the two calls together if _, err := postsRef.Push(ctx, &Post{ Author: "alanisawesome", Title: "The Turing Machine", }); err != nil { log.Fatalln("Error pushing child node:", err) }
Khoá duy nhất dựa trên dấu thời gian, vì vậy, các mục trong danh sách sẽ tự động được sắp xếp theo thứ tự thời gian. Vì Firebase tạo một khoá duy nhất cho mỗi bài đăng trên blog, nên sẽ không xảy ra xung đột ghi nếu nhiều người dùng thêm bài đăng cùng lúc. Dữ liệu trong cơ sở dữ liệu của bạn hiện có dạng như sau:
{ "posts": { "-JRHTHaIs-jNPLXOQivY": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "-JRHTHaKuITFIhnj02kE": { "author": "alanisawesome", "title": "The Turing Machine" } } }
Trong JavaScript, Python và Go, mẫu gọi push()
rồi gọi ngay set()
phổ biến đến mức SDK Firebase cho phép bạn kết hợp các mẫu này bằng cách truyền dữ liệu cần đặt trực tiếp đến push()
như sau:
Java
// No Java equivalent
Node.js
// This is equivalent to the calls to push().set(...) above postsRef.push({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' });;
Python
# This is equivalent to the calls to push().set(...) above posts_ref.push({ 'author': 'gracehop', 'title': 'Announcing COBOL, a New Programming Language' })
Tìm
if _, err := postsRef.Push(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error pushing child node:", err) }
Lấy khoá riêng biệt do push() tạo
Việc gọi push()
sẽ trả về một tham chiếu đến đường dẫn dữ liệu mới. Bạn có thể dùng tham chiếu này để lấy khoá hoặc đặt dữ liệu cho đường dẫn đó. Đoạn mã sau đây sẽ tạo ra dữ liệu giống như ví dụ trên, nhưng giờ đây chúng ta sẽ có quyền truy cập vào khoá duy nhất đã được tạo:
Java
// Generate a reference to a new location and add some data using push() DatabaseReference pushedPostRef = postsRef.push(); // Get the unique ID generated by a push() String postId = pushedPostRef.getKey();
Node.js
// Generate a reference to a new location and add some data using push() const newPostRef = postsRef.push(); // Get the unique key generated by push() const postId = newPostRef.key;
Python
# Generate a reference to a new location and add some data using push() new_post_ref = posts_ref.push() # Get the unique key generated by push() post_id = new_post_ref.key
Tìm
// Generate a reference to a new location and add some data using Push() newPostRef, err := postsRef.Push(ctx, nil) if err != nil { log.Fatalln("Error pushing child node:", err) } // Get the unique key generated by Push() postID := newPostRef.Key
Như bạn thấy, bạn có thể lấy giá trị của khoá duy nhất từ thông tin tham chiếu push()
.
Trong phần tiếp theo về Truy xuất dữ liệu, chúng ta sẽ tìm hiểu cách đọc dữ liệu này từ cơ sở dữ liệu Firebase.
Lưu dữ liệu giao dịch
Khi xử lý dữ liệu phức tạp có thể bị hỏng do các hoạt động sửa đổi đồng thời, chẳng hạn như bộ đếm gia tăng, SDK sẽ cung cấp một thao tác giao dịch.
Trong Java và Node.js, bạn cung cấp cho thao tác giao dịch 2 lệnh gọi lại: một hàm cập nhật và một lệnh gọi lại hoàn tất không bắt buộc. Trong Python và Go, thao tác giao dịch đang chặn và do đó, thao tác này chỉ chấp nhận hàm cập nhật.
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à phải trả về trạng thái mong muốn mới mà bạn muốn ghi. Ví dụ: nếu muốn tăng số lượt ủng hộ cho một bài đăng cụ thể trên blog, bạn sẽ viết một giao dịch như sau:
Java
DatabaseReference upvotesRef = ref.child("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes"); upvotesRef.runTransaction(new Transaction.Handler() { @Override public Transaction.Result doTransaction(MutableData mutableData) { Integer currentValue = mutableData.getValue(Integer.class); if (currentValue == null) { mutableData.setValue(1); } else { mutableData.setValue(currentValue + 1); } return Transaction.success(mutableData); } @Override public void onComplete( DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) { System.out.println("Transaction completed"); } });
Node.js
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes'); upvotesRef.transaction((current_value) => { return (current_value || 0) + 1; });
Python
def increment_votes(current_value): return current_value + 1 if current_value else 1 upvotes_ref = db.reference('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes') try: new_vote_count = upvotes_ref.transaction(increment_votes) print('Transaction completed') except db.TransactionAbortedError: print('Transaction failed to commit')
Tìm
fn := func(t db.TransactionNode) (interface{}, error) { var currentValue int if err := t.Unmarshal(¤tValue); err != nil { return nil, err } return currentValue + 1, nil } ref := client.NewRef("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes") if err := ref.Transaction(ctx, fn); err != nil { log.Fatalln("Transaction failed to commit:", err) }
Ví dụ trên kiểm tra xem bộ đếm có phải là null
hay chưa được tăng, vì các giao dịch có thể được gọi bằng null
nếu không có giá trị mặc định nào được ghi.
Nếu mã trên được chạy mà không có hàm giao dịch và 2 ứng dụng cố gắng tăng giá trị này cùng lúc, thì cả hai ứng dụng sẽ ghi 1
làm giá trị mới, dẫn đến một lần tăng thay vì hai lần.
Kết nối mạng và ghi dữ liệu khi không có mạng
Các ứng dụng Firebase Node.js và Java duy trì phiên bản nội bộ riêng của mọi dữ liệu đang hoạt động. Khi dữ liệu được ghi, trước tiên, dữ liệu sẽ được ghi vào phiên bản cục bộ này. Sau đó, ứng dụng sẽ đồng bộ hoá dữ liệu đó với cơ sở dữ liệu và với các ứng dụng khác trên cơ sở "cố gắng hết sức".
Do đó, mọi thao tác ghi vào cơ sở dữ liệu sẽ kích hoạt ngay các sự kiện cục bộ, trước khi bất kỳ dữ liệu nào được ghi vào cơ sở dữ liệu. Điều này có nghĩa là khi bạn viết một ứng dụng bằng Firebase, ứng dụng của bạn sẽ vẫn phản hồi bất kể độ trễ mạng hoặc khả năng kết nối Internet.
Sau khi kết nối được thiết lập lại, chúng tôi sẽ nhận được bộ sự kiện thích hợp để máy khách "bắt kịp" trạng thái máy chủ hiện tại mà không cần phải viết bất kỳ mã tuỳ chỉnh nào.
Bảo mật dữ liệu của bạn
Firebase Realtime Database có một ngôn ngữ bảo mật cho phép bạn xác định những người dùng có quyền đọc và ghi vào các nút khác nhau trong dữ liệu của bạn. Bạn có thể đọc thêm về vấn đề này trong phần Bảo mật dữ liệu của bạn.