获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

保存数据

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

本文档介绍了将数据写入 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 实时数据库具有一种安全语言,可让您定义哪些用户对数据的不同节点具有读写权限。您可以在保护您的数据中了解更多信息。