所以我可能会存储一个列表节点和一个列表索引节点,两者之间有一些重复的数据,至少是列表名称。
我正在使用ES6,并在我的Javascript应用中承诺处理异步流,主要是在第一次数据推送后从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);
});
如何确保数据在所有地方都保持同步,知道我的应用程序仅在客户端上运行吗?
为了进行健全性检查,我在promise中设置了setTimeout并在解决之前关闭了浏览器,实际上我的数据库不是更长的一致性,但保存了没有相应列表的额外索引。
有任何建议吗?
#1 楼
好问题。我知道三种方法,下面将列出。我将为此提供一个稍有不同的示例,主要是因为它允许我在解释中使用更具体的术语。 />
假设我们有一个聊天应用程序,其中存储两个实体:消息和用户。在显示消息的屏幕中,我们还显示用户名。因此,为了最大程度地减少读取次数,我们还会在每条聊天消息中存储用户名。
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 */);
精巧的读者会注意到我在处理邮件时作弊。首先作弊的是,我从不为监听器调用
off
,但是我也不使用事务。如果我们想从客户端安全地执行这种类型的操作,则需要:确保两个地方的名称都匹配的安全规则。
但是规则需要允许足够的灵活性,以便它们在我们更改名称时暂时有所不同。因此,这变成了一个非常痛苦的两阶段提交方案。
将
username
的消息的所有so:209103
字段更改为null
(有些神奇的值)更改用户
name
的so:209103
更改为“ puf” ,将每条消息中的
username
通过so:209103
更改为null
。该查询需要两个条件的
puf
,而Firebase查询不支持此条件。因此,我们将获得一个可查询的额外属性and
(值uid_plus_name
)。客户端代码可事务性地处理所有这些转换。
这种方法使我的头部受伤。通常,这意味着我做错了事。但是,即使这是正确的方法,而且头部受伤,我更有可能犯编码错误。因此,我宁愿寻找一个更简单的解决方案。
最终一致性
更新(20150925):Firebase发布了一项功能,允许对多个路径进行原子写入。这与下面的方法类似,但是只有一个命令。请参阅上面的更新部分,以了解其工作原理。
第二种方法取决于将用户操作(“我想将我的名字更改为'puf'”)从该操作的含义(“我们需要在配置文件中更新名称:so 103103和每条包含
so:209103_puf
的消息。我将在服务器上运行的脚本中处理重命名。主要方法是像这样:
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 });
})
});
}
我在这里再次采取了一些捷径,例如使用
user = so:209103
(通常对于使用Firebase以获得最佳性能来说是个坏主意)。但是总体而言,该方法更简单,但代价是无法同时完全更新所有数据。但是最终,消息将全部更新以匹配新值。不关心
第三种方法是最简单的:在许多情况下,您实际上并没有完全更新重复的数据。在我们这里使用的示例中,您可以说每条消息都记录了我当时使用的名称。直到现在我都没有更改过我的名字,因此有意义的是,较旧的消息会显示我当时使用的名称。这在二级数据本质上是事务性的许多情况下适用。当然,它并非在所有地方都适用,但是在所有情况下,“不关心”是最简单的方法。
摘要
以上只是对如何您可以解决此问题,而且它们肯定还不完整,我发现每次需要散出重复的数据时,它都会回到这些基本方法之一。
评论
谢谢弗兰克的出色回答!确实,我采用了“不关心”方法。我确实切换了写操作,因此该项目在索引之前,所以我不冒险在列表中有一个项目链接到没有数据的地方(但是现在我可能会得到一个孤立的列表项目,这不是很大)。
–collardeau
15年6月10日在8:31
很好的答案。您概述的建议非常有用,不仅适用于Firebase,而且适用于许多情况。
– Scott Coates
15年7月14日在3:54
请务必确保最终一致地延迟服务器任务,并检查名称是否仍未更改。
– AJcodez
15年8月2日在17:04
#2 楼
为了给Franks很大的答复,我通过一套Firebase Cloud Functions实现了最终的一致性方法。每当更改主值(例如,用户名)时,函数便会触发,然后将更改传播到非规范化字段。它不像事务处理那样快,但是在很多情况下,它确实可以不需要。
评论
非常棒的乌菲。只要您没有严格的实时或离线要求,云功能就可以很好地实现这一目标。
–弗兰克·范·普菲伦
18年1月13日在3:23
评论
我会在下面写一个正确的答案。第一句话让我高兴。你可以在生日聚会上重复吗?特别是当开发者朋友在场时。 :-)这似乎也很重要,.. stackoverflow.com/questions/47334382