保存數據

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

本文檔介紹了將數據寫入 Firebase 實時數據庫的四種方法:設置、更新、推送和事務支持。

保存數據的方法

將數據寫入或替換到定義的路徑,例如messages/users/<username>
更新在不替換所有數據的情況下更新已定義路徑的一些鍵
添加到數據庫中的數據列表。每次將新節點推送到列表時,您的數據庫都會生成一個唯一鍵,例如messages/users/<unique-user-id>/<username>
交易在處理可能被並發更新破壞的複雜數據時使用事務

保存數據

基本的數據庫寫入操作是一組將新數據保存到指定的數據庫引用,替換該路徑上的任何現有數據。為了理解 set,我們將構建一個簡單的博客應用程序。您的應用程序的數據存儲在此數據庫引用中:

爪哇
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("server/saving-data/fireblog");
節點.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')
// 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 方法而不是 push 方法是有意義的,因為您已經擁有密鑰並且不需要創建一個。

首先,創建對用戶數據的數據庫引用。然後使用set() / setValue()將用戶對象與用戶的用戶名、全名和生日一起保存到數據庫中。您可以傳遞設置字符串、數字、布爾值、 null 、數組或任何 JSON 對象。傳遞null將刪除指定位置的數據。在這種情況下,您將向它傳遞一個對象:

爪哇
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);
節點.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'
    }
})

// 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 對象保存到數據庫時,對象屬性會自動以嵌套方式映射到數據庫子位置。現在,如果您導航到 URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name ,我們將看到值“Alan Turing”。您還可以將數據直接保存到子位置:

爪哇
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing"));
usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
節點.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'
})
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的其他子節點。

更新保存的數據

如果要同時寫入一個數據庫位置的多個子節點而不覆蓋其他子節點,可以使用如下所示的更新方法:

爪哇
DatabaseReference hopperRef = usersRef.child("gracehop");
Map<String, Object> hopperUpdates = new HashMap<>();
hopperUpdates.put("nickname", "Amazing Grace");

hopperRef.updateChildrenAsync(hopperUpdates);
節點.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'
})
hopperRef := usersRef.Child("gracehop")
if err := hopperRef.Update(ctx, map[string]interface{}{
	"nickname": "Amazing Grace",
}); err != nil {
	log.Fatalln("Error updating child:", err)
}

這將更新 Grace 的數據以包含她的暱稱。如果您在此處使用 set 而不是 update,它將從您的hopperRef中刪除full_namedate_of_birth

Firebase 實時數據庫還支持多路徑更新。這意味著 update 現在可以同時更新數據庫中多個位置的值,這是一項強大的功能,可以幫助您對數據進行非規範化。使用多路徑更新,您可以同時為 Grace 和 Alan 添加暱稱:

爪哇
Map<String, Object> userUpdates = new HashMap<>();
userUpdates.put("alanisawesome/nickname", "Alan The Machine");
userUpdates.put("gracehop/nickname", "Amazing Grace");

usersRef.updateChildrenAsync(userUpdates);
節點.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'
})
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"
    }
  }
}

請注意,嘗試通過寫入包含路徑的對象來更新對象將導致不同的行為。讓我們看看如果您嘗試以這種方式更新 Grace 和 Alan 會發生什麼:

爪哇
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);
節點.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'
    }
})
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 中的 set 和 update 方法都採用可選的完成回調,當寫入已提交到數據庫時調用該回調。如果由於某種原因調用不成功,則向回調傳遞一個錯誤對象,指示失敗發生的原因。在 Python 和 Go Admin SDK 中,所有寫入方法都是阻塞的。也就是說,在寫入提交到數據庫之前,寫入方法不會返回。

爪哇
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.");
    }
  }
});
節點.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()函數,該函數為每個新的 child 生成一個唯一的密鑰。通過使用唯一的子鍵,多個客戶端可以同時將子節點添加到同一位置,而無需擔心寫入衝突。

爪哇
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"));
節點.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'
})

// 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()來組合它們,如下所示:

爪哇
// No Java equivalent
節點.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'
})
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()將返回對新數據路徑的引用,您可以使用它來獲取密鑰或為其設置數據。以下代碼將產生與上述示例相同的數據,但現在我們可以訪問生成的唯一鍵:

爪哇
// 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();
節點.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
// 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 中,事務操作是阻塞的,因此它只接受更新函數。

更新函數將數據的當前狀態作為參數,並應返回您想要寫入的新的期望狀態。例如,如果您想增加特定博客文章的點贊數,您可以編寫如下事務:

爪哇
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");
  }
});
節點.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')
fn := func(t db.TransactionNode) (interface{}, error) {
	var currentValue int
	if err := t.Unmarshal(&currentValue); 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 編寫應用程序時,無論網絡延遲或 Internet 連接如何,您的應用程序都將保持響應。

重新建立連接後,我們將收到一組適當的事件,以便客戶端“趕上”當前服務器狀態,而無需編寫任何自定義代碼。

保護您的數據

Firebase 實時數據庫具有一種安全語言,可讓您定義哪些用戶對數據的不同節點具有讀寫權限。您可以在保護您的數據中了解更多信息。