Join us for Firebase Summit on November 10, 2021. Tune in to learn how Firebase can help you accelerate app development, release with confidence, and scale with ease. Register

保存數據

本文檔涵蓋了將數據寫入 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 ,我們將看到值“阿蘭·圖靈”。您還可以將數據直接保存到子位置:

爪哇
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 的數據以包括她的暱稱。如果您曾使用此設定的,而不是更新,它會刪除兩個full_namedate_of_birthhopperRef

Firebase 實時數據庫還支持多路徑更新。這意味著,更新現在可以同時在數據庫中的多個位置更新價值觀,強大的功能,它允許幫助你非規範化數據。使用多路徑更新,您可以同時為 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同時,則該職位的人會被其他被刪除。

為了解決這個問題,火力地堡客戶提供push()生成每個新的子獨特的按鍵功能。通過使用唯一的子鍵,多個客戶端可以同時將子鍵添加到同一位置,而無需擔心寫入衝突。

爪哇
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和圍棋,調用的模式push()然後立即調用set()是如此普遍,在火力地堡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()引用。

在下一節中檢索數據,我們將學習如何從火力地堡數據庫讀取這些數據。

保存交易數據

當與可能由並發修改被損壞複雜的數據,如增量計數器工作時,SDK提供一個事務操作

在 Java 和 Node.js 中,您為事務操作提供了兩個回調:一個更新函數和一個可選的完成回調。在 Python 和 Go 中,事務操作是阻塞的,因此它只接受更新函數。

update 函數將數據的當前狀態作為參數,並返回您想要寫入的新的所需狀態。例如,如果您想增加特定博客文章的點贊數,您可以編寫如下所示的交易:

爪哇
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 實時數據庫具有一種安全語言,可讓您定義哪些用戶對數據的不同節點具有讀寫訪問權限。您可以閱讀更多關於它的保護您的數據