本文件說明將資料寫入 Firebase 即時資料庫的四種方法:設定、更新、推送和交易支援。
儲存資料的方式
set | 將資料寫入或取代至定義的路徑,例如 messages/users/<username> |
update | 更新已定義路徑的部分鍵,而不替換所有資料 |
推送 | 新增至資料庫中的資料清單。每當您將新節點推送至清單時,資料庫都會產生專屬索引鍵,例如 messages/users/<unique-user-id>/<username> |
交易 | 處理並行更新可能損毀的複雜資料時使用交易功能 |
正在儲存資料
基本資料庫寫入作業是一組,可將新資料儲存到指定的資料庫參照,取代該路徑中的任何現有資料。為了瞭解設定內容,我們會建構一個簡單的網誌應用程式。您的應用程式資料會儲存在以下資料庫參照中:
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')
Go
// 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")
我們先儲存一些使用者資料。我們使用不重複的使用者名稱來儲存每位使用者,並且會儲存他們的全名和出生日期。由於每個使用者都會有不重複的使用者名稱,因此建議您使用這裡的設定方法,不要使用推送方法,因為您已擁有金鑰,不必建立金鑰。
首先,請建立使用者資料的資料庫參照。然後使用 set()
/ setValue()
,透過使用者名稱、全名和生日將使用者物件儲存至資料庫。您可以傳遞設定字串、數字、布林值、null
、陣列或任何 JSON 物件。如果傳遞 null
,則會移除指定位置的資料。在此情況下,您可以向函式傳送物件:
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' } })
Go
// 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) }
將 JSON 物件儲存至資料庫後,系統會以巢狀方式將物件屬性自動對應至資料庫的子項位置。現在如果您前往 https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name 這個網址,就會看到「Alan Turing」。您也可以直接將資料儲存到孩子位置:
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' })
Go
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) }
上述兩個範例 (同時將兩個值與物件同時寫入,並分別寫入子項位置),將相同的資料儲存到資料庫:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing" }, "gracehop": { "date_of_birth": "December 9, 1906", "full_name": "Grace Hopper" } } }
第一個範例只會在正在觀看資料的用戶端上觸發一個事件,第二個範例則觸發兩個事件。請注意,如果 usersRef
中已有資料,第一種方法會覆寫這類資料,但第二個方法只會修改每個獨立子節點的值,同時讓 usersRef
的其他子項保持不變。
更新已儲存的資料
如要同時寫入資料庫位置的多個子項,而不覆寫其他子節點,您可以使用更新方法,如下所示:
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' })
Go
hopperRef := usersRef.Child("gracehop") if err := hopperRef.Update(ctx, map[string]interface{}{ "nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating child:", err) }
這會更新小蕾的資料,加入她的暱稱。如果在這裡設定而非更新,系統會刪除 hopperRef
中的 full_name
和 date_of_birth
。
Firebase 即時資料庫也支援多路徑更新。這代表更新作業可以同時更新資料庫中多個位置的值,而這項強大功能可協助您將資料去標準化。您可以透過多路徑更新功能,同時為 Grace 和 Alan 新增暱稱:
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' })
Go
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) }
本次更新後,小艾和小蕾也新增了暱稱:
{ "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" } } }
請注意,若嘗試透過包含的路徑編寫物件來更新物件,則會造成不同的行為。如果改為透過這種方式更新小蕾和阿俊,這將會發生以下情況:
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' } })
Go
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) }
這會產生不同的行為,也就是覆寫整個 /users
節點:
{ "users": { "alanisawesome": { "nickname": "Alan The Machine" }, "gracehop": { "nickname": "Amazing Grace" } } }
新增完成回呼
在 Node.js 和 Java Admin SDK 中,如果您想瞭解資料何時已修訂,您可以新增完成回呼。這些 SDK 中的設定和更新方法都使用選用的完成回呼,系統會在將寫入作業提交至資料庫時呼叫該回呼。如果呼叫因為某些原因而失敗,系統會傳送回呼錯誤物件,指出失敗的原因。在 Python 和 Go Admin SDK 中,所有寫入方法都會遭到封鎖。也就是說,寫入方法要等到寫入作業提交至資料庫後,才會傳回。
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.'); } });
正在儲存資料清單
建立資料清單時,請留意大多數應用程式屬於多使用者性質,並據此調整清單結構。從上述範例展開,讓我們在您的應用程式中加入網誌文章。首先,您或許可以使用「set」來儲存採自動遞增整數索引的子項,如下所示:
// NOT RECOMMENDED - use push() instead! { "posts": { "0": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "1": { "author": "alanisawesome", "title": "The Turing Machine" } } }
使用者新增貼文會儲存為 /posts/2
。雖然只有一位作者會新增文章,但在您協作網誌應用程式中,多位使用者可以同時新增文章,這種做法才可行。如果兩位作者同時寫入 /posts/2
,則其中一則訊息會由另一位作者刪除。
為解決這個問題,Firebase 用戶端提供 push()
函式,可為每個新的子項產生一個專屬金鑰。透過使用不重複的子項金鑰,多個用戶端可以同時將子項新增至相同的位置,不必擔心寫入衝突。
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' })
Go
// 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) }
不重複鍵是以時間戳記為基礎,因此清單項目會自動按照時間排序。由於 Firebase 會為每篇網誌文章產生專屬金鑰,因此即便多位使用者同時新增文章,也不會發生寫入衝突。資料庫資料現在看起來會像這樣:
{ "posts": { "-JRHTHaIs-jNPLXOQivY": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "-JRHTHaKuITFIhnj02kE": { "author": "alanisawesome", "title": "The Turing Machine" } } }
在 JavaScript、Python 和 Go 中,呼叫 push()
然後立即呼叫 set()
的模式十分常見,因此 Firebase SDK 可讓您藉由傳送要直接設為 push()
的資料進行合併,方法如下:
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' })
Go
if _, err := postsRef.Push(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error pushing child node:", err) }
取得從 Push() 產生的專屬金鑰
呼叫 push()
會傳回新資料路徑的參照,您可以使用該路徑取得鍵或設定資料路徑。下列程式碼產生的資料與上述範例相同,但現在我們可以存取所產生的專屬金鑰:
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
Go
// 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
如您所見,您可以從 push()
參照取得不重複鍵的值。
在下一節的擷取資料中,我們將探討如何從 Firebase 資料庫中讀取這些資料。
正在儲存交易資料
處理可能因為並行修改而損毀的複雜資料 (例如漸進式計數器) 時,SDK 會提供交易作業。
在 Java 和 Node.js 中,您會提供交易作業兩種回呼:更新函式和選用的完成回呼。在 Python 和 Go 中,交易作業會遭到封鎖,因此只接受更新函式。
更新函式會將資料目前的狀態做為引數,並應傳回要寫入的新所需狀態。舉例來說,如果您想增加特定網誌文章的認同票數,就必須編寫如下的交易:
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')
Go
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) }
上述範例會檢查計數器是否為 null
,或尚未遞增,因為如未寫入預設值,可以使用 null
呼叫交易。
如果上述程式碼在沒有交易函式的情況下執行,且兩個用戶端同時嘗試遞增該函式,則兩個用戶端都會將 1
寫入新值,這會產生一個遞增量,而非兩個。
網路連線和離線寫入
Firebase Node.js 和 Java 用戶端會保留自己的任何有效資料的內部版本。寫入資料時,系統會先將資料寫入這個本機版本。接著,用戶端會「盡可能」將這些資料與資料庫和其他用戶端同步處理。
因此,在任何資料寫入資料庫之前,所有寫入資料庫的動作都會立即觸發本機事件。也就是說,當您使用 Firebase 編寫應用程式時,無論是在網路延遲或正常連線的情況下,應用程式都會持續回應。
重新建立連線後,我們會收到一組適當的事件,讓用戶端能夠根據目前的伺服器狀態「擴充」,而無須編寫任何自訂程式碼。
保護您的資料
Firebase 即時資料庫採用安全性語言,可用於定義哪些使用者俱備資料不同節點的讀取和寫入權限。詳情請參閱保護您的資料。