如何在 Firebase 中写入非规范化数据
How to write denormalized data in Firebase
我在 Stucturing Data 上阅读了 Firebase 文档。数据存储很便宜,但用户的时间却不便宜。我们应该针对get操作进行优化,多处写入。
那么我可能会存储一个 list 节点和一个 list-index 节点,两者之间有一些重复的数据,非常至少列表名称。
我在我的 javascript 应用程序中使用 ES6 和 promises 来处理异步流程,主要是在第一次数据推送后从 firebase 获取 ref 键。
let addIndexPromise = new Promise( (resolve, reject) => {
let newRef = ref.child('list-index').push(newItem);
resolve( newRef.key()); // ignore reject() for brevity
});
addIndexPromise.then( key => {
ref.child('list').child(key).set(newItem);
});
如何确保数据在所有地方保持同步,知道我的应用程序只在客户端上运行?
为了完整性检查,我在我的承诺中设置了一个 setTimeout 并在它解决之前关闭了我的浏览器,实际上我的数据库不再一致,保存了一个没有相应列表的额外索引.
有什么建议吗?
好问题。我知道这三种方法,我将在下面列出。
我将举一个稍微不同的例子,主要是因为它允许我在解释中使用更具体的术语。
假设我们有一个聊天应用程序,我们在其中存储了两个实体:消息和用户。在显示消息的屏幕中,我们还显示了用户的姓名。因此,为了最大限度地减少阅读次数,我们也将用户的姓名存储在每条聊天消息中。
users
so:209103
name: "Frank van Puffelen"
location: "San Francisco, CA"
questionCount: 12
so:3648524
name: "legolandbridge"
location: "London, Prague, Barcelona"
questionCount: 4
messages
-Jabhsay3487
message: "How to write denormalized data in Firebase"
user: so:3648524
username: "legolandbridge"
-Jabhsay3591
message: "Great question."
user: so:209103
username: "Frank van Puffelen"
-Jabhsay3595
message: "I know of three approaches, which I'll list below."
user: so:209103
username: "Frank van Puffelen"
所以我们将用户配置文件的主要副本存储在 users
节点中。在消息中,我们存储 uid
(so:209103 和 so:3648524)以便我们可以查找用户。但是我们也将用户名存储在消息中,这样当我们想显示消息列表时就不必为每个用户查找它。
现在,当我转到聊天服务的个人资料页面并将我的名字从 "Frank van Puffelen" 更改为 "puf" 时会发生什么。
交易更新
大多数开发人员最初可能会想到执行事务性更新。我们总是希望消息中的 username
与相应配置文件中的 name
相匹配。
使用多路径写入(添加于 20150925)
从 Firebase 2.3(针对 JavaScript)和 2.4(针对 Android 和 iOS)开始,您可以通过使用单个多路径更新非常轻松地实现原子更新:
function renameUser(ref, uid, name) {
var updates = {}; // all paths to be updated and their new values
updates['users/'+uid+'/name'] = name;
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
updates['messages/'+messageSnapshot.key()+'/username'] = name;
})
ref.update(updates);
});
}
这将向 Firebase 发送一个更新命令,更新其个人资料和每条消息中的用户名。
先前的原子方法
因此,当用户更改其个人资料中的 name
时:
var ref = new Firebase('https://mychat.firebaseio.com/');
var uid = "so:209103";
var nameInProfileRef = ref.child('users').child(uid).child('name');
nameInProfileRef.transaction(function(currentName) {
return "puf";
}, function(error, committed, snapshot) {
if (error) {
console.log('Transaction failed abnormally!', error);
} else if (!committed) {
console.log('Transaction aborted by our code.');
} else {
console.log('Name updated in profile, now update it in the messages');
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.on('child_added', function(messageSnapshot) {
messageSnapshot.ref().update({ username: "puf" });
});
}
console.log("Wilma's data: ", snapshot.val());
}, false /* don't apply the change locally */);
相当参与和精明 reader 会注意到我在处理消息时作弊。第一个作弊是我从不为监听器调用 off
,但我也不使用事务。
如果我们想从客户端安全地执行此类操作,我们需要:
- 确保两个地方的名称匹配的安全规则。但是规则需要允许足够的灵活性,以便在我们更改名称时它们可以暂时不同。所以这变成了一个非常痛苦的两阶段提交方案。
- 将
so:209103
消息的所有 username
字段更改为 null
(一些神奇的值)
- 将用户
so:209103
的 name
更改为 'puf'
- 将每条消息中的
username
由so:209103
即null
更改为puf
。
- 该查询需要
and
两个条件,Firebase 查询不支持。因此,我们最终会得到一个额外的 属性 uid_plus_name
(值为 so:209103_puf
),我们可以对其进行查询。
- 以事务方式处理所有这些转换的客户端代码。
这种方法让我很头疼。通常这意味着我做错了什么。但即使这是正确的方法,如果我的脑袋很痛,我也更有可能犯编码错误。所以我更愿意寻找一个更简单的解决方案。
最终一致性
更新 (20150925):Firebase 发布了一项允许对多个路径进行原子写入的功能。这与下面的方法类似,但使用单个命令。请参阅上面的更新部分以了解其工作原理。
第二种方法依赖于将用户操作 ("I want to change my name to 'puf'") 与该操作的含义分开(“我们需要更新配置文件 so:209103 中的名称以及具有 user = so:209103
).
我会在我们 运行 服务器上的脚本中处理重命名。主要方法是这样的:
function renameUser(ref, uid, name) {
ref.child('users').child(uid).update({ name: name });
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
messageSnapshot.update({ username: name });
})
});
}
我在这里再次采取一些捷径,例如使用 once('value'
(这通常不是 Firebase 最佳性能的好主意)。但总的来说,这种方法更简单,代价是不会同时完全更新所有数据。但最终消息将全部更新以匹配新值。
不关心
第三种方法是所有方法中最简单的:在许多情况下,您实际上根本不需要更新重复数据。在我们在这里使用的示例中,您可以说每条消息都记录了我当时使用的名称。直到刚才我才更改我的名字,所以旧消息显示我当时使用的名字是有道理的。这适用于许多次级数据本质上是事务性数据的情况。当然,它并不适用于所有地方,但它适用的地方 "not caring" 是最简单的方法。
总结
虽然以上只是对如何解决这个问题的粗略描述,而且它们肯定不完整,但我发现每次我需要扇出重复数据时,它都会回到这些基本方法之一。
为了补充 Frank 的精彩回复,我使用一组 Firebase Cloud Functions 实现了最终一致性方法。每当主要值(例如用户名)发生更改时都会触发这些函数,然后将更改传播到非规范化字段。
它不如交易快,但在很多情况下不需要如此。
我在 Stucturing Data 上阅读了 Firebase 文档。数据存储很便宜,但用户的时间却不便宜。我们应该针对get操作进行优化,多处写入。
那么我可能会存储一个 list 节点和一个 list-index 节点,两者之间有一些重复的数据,非常至少列表名称。
我在我的 javascript 应用程序中使用 ES6 和 promises 来处理异步流程,主要是在第一次数据推送后从 firebase 获取 ref 键。
let addIndexPromise = new Promise( (resolve, reject) => {
let newRef = ref.child('list-index').push(newItem);
resolve( newRef.key()); // ignore reject() for brevity
});
addIndexPromise.then( key => {
ref.child('list').child(key).set(newItem);
});
如何确保数据在所有地方保持同步,知道我的应用程序只在客户端上运行?
为了完整性检查,我在我的承诺中设置了一个 setTimeout 并在它解决之前关闭了我的浏览器,实际上我的数据库不再一致,保存了一个没有相应列表的额外索引.
有什么建议吗?
好问题。我知道这三种方法,我将在下面列出。
我将举一个稍微不同的例子,主要是因为它允许我在解释中使用更具体的术语。
假设我们有一个聊天应用程序,我们在其中存储了两个实体:消息和用户。在显示消息的屏幕中,我们还显示了用户的姓名。因此,为了最大限度地减少阅读次数,我们也将用户的姓名存储在每条聊天消息中。
users
so:209103
name: "Frank van Puffelen"
location: "San Francisco, CA"
questionCount: 12
so:3648524
name: "legolandbridge"
location: "London, Prague, Barcelona"
questionCount: 4
messages
-Jabhsay3487
message: "How to write denormalized data in Firebase"
user: so:3648524
username: "legolandbridge"
-Jabhsay3591
message: "Great question."
user: so:209103
username: "Frank van Puffelen"
-Jabhsay3595
message: "I know of three approaches, which I'll list below."
user: so:209103
username: "Frank van Puffelen"
所以我们将用户配置文件的主要副本存储在 users
节点中。在消息中,我们存储 uid
(so:209103 和 so:3648524)以便我们可以查找用户。但是我们也将用户名存储在消息中,这样当我们想显示消息列表时就不必为每个用户查找它。
现在,当我转到聊天服务的个人资料页面并将我的名字从 "Frank van Puffelen" 更改为 "puf" 时会发生什么。
交易更新
大多数开发人员最初可能会想到执行事务性更新。我们总是希望消息中的 username
与相应配置文件中的 name
相匹配。
使用多路径写入(添加于 20150925)
从 Firebase 2.3(针对 JavaScript)和 2.4(针对 Android 和 iOS)开始,您可以通过使用单个多路径更新非常轻松地实现原子更新:
function renameUser(ref, uid, name) {
var updates = {}; // all paths to be updated and their new values
updates['users/'+uid+'/name'] = name;
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
updates['messages/'+messageSnapshot.key()+'/username'] = name;
})
ref.update(updates);
});
}
这将向 Firebase 发送一个更新命令,更新其个人资料和每条消息中的用户名。
先前的原子方法
因此,当用户更改其个人资料中的 name
时:
var ref = new Firebase('https://mychat.firebaseio.com/');
var uid = "so:209103";
var nameInProfileRef = ref.child('users').child(uid).child('name');
nameInProfileRef.transaction(function(currentName) {
return "puf";
}, function(error, committed, snapshot) {
if (error) {
console.log('Transaction failed abnormally!', error);
} else if (!committed) {
console.log('Transaction aborted by our code.');
} else {
console.log('Name updated in profile, now update it in the messages');
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.on('child_added', function(messageSnapshot) {
messageSnapshot.ref().update({ username: "puf" });
});
}
console.log("Wilma's data: ", snapshot.val());
}, false /* don't apply the change locally */);
相当参与和精明 reader 会注意到我在处理消息时作弊。第一个作弊是我从不为监听器调用 off
,但我也不使用事务。
如果我们想从客户端安全地执行此类操作,我们需要:
- 确保两个地方的名称匹配的安全规则。但是规则需要允许足够的灵活性,以便在我们更改名称时它们可以暂时不同。所以这变成了一个非常痛苦的两阶段提交方案。
- 将
so:209103
消息的所有username
字段更改为null
(一些神奇的值) - 将用户
so:209103
的name
更改为 'puf' - 将每条消息中的
username
由so:209103
即null
更改为puf
。 - 该查询需要
and
两个条件,Firebase 查询不支持。因此,我们最终会得到一个额外的 属性uid_plus_name
(值为so:209103_puf
),我们可以对其进行查询。
- 将
- 以事务方式处理所有这些转换的客户端代码。
这种方法让我很头疼。通常这意味着我做错了什么。但即使这是正确的方法,如果我的脑袋很痛,我也更有可能犯编码错误。所以我更愿意寻找一个更简单的解决方案。
最终一致性
更新 (20150925):Firebase 发布了一项允许对多个路径进行原子写入的功能。这与下面的方法类似,但使用单个命令。请参阅上面的更新部分以了解其工作原理。
第二种方法依赖于将用户操作 ("I want to change my name to 'puf'") 与该操作的含义分开(“我们需要更新配置文件 so:209103 中的名称以及具有 user = so:209103
).
我会在我们 运行 服务器上的脚本中处理重命名。主要方法是这样的:
function renameUser(ref, uid, name) {
ref.child('users').child(uid).update({ name: name });
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
messageSnapshot.update({ username: name });
})
});
}
我在这里再次采取一些捷径,例如使用 once('value'
(这通常不是 Firebase 最佳性能的好主意)。但总的来说,这种方法更简单,代价是不会同时完全更新所有数据。但最终消息将全部更新以匹配新值。
不关心
第三种方法是所有方法中最简单的:在许多情况下,您实际上根本不需要更新重复数据。在我们在这里使用的示例中,您可以说每条消息都记录了我当时使用的名称。直到刚才我才更改我的名字,所以旧消息显示我当时使用的名字是有道理的。这适用于许多次级数据本质上是事务性数据的情况。当然,它并不适用于所有地方,但它适用的地方 "not caring" 是最简单的方法。
总结
虽然以上只是对如何解决这个问题的粗略描述,而且它们肯定不完整,但我发现每次我需要扇出重复数据时,它都会回到这些基本方法之一。
为了补充 Frank 的精彩回复,我使用一组 Firebase Cloud Functions 实现了最终一致性方法。每当主要值(例如用户名)发生更改时都会触发这些函数,然后将更改传播到非规范化字段。
它不如交易快,但在很多情况下不需要如此。