我已在Stucturing Data上阅读了Firebase文档。数据存储很便宜,但用户的时间不是。我们应该优化get操作,并在多个地方写。
那么我可能存储列表节点和 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,并在解决之前关闭了我的浏览器,确实我的数据库不再一致,保存了额外的索引而没有相应的列表。
有什么建议吗?
答案 0 :(得分:56)
好问题。我知道有三种方法,我将在下面列出。
我将采用略有不同的例子,主要是因为它允许我在解释中使用更具体的术语。
假设我们有一个聊天应用程序,我们存储两个实体:消息和用户。在我们显示消息的屏幕中,我们还显示用户的名称。因此,为了最大限度地减少读取次数,我们也会将每个聊天消息的用户名称存储起来。
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
(所以:209103等等: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
的{{1}}更改为' puf' so:209103
username
的每个邮件中的so:209103
更改为null
。puf
两个条件,Firebase查询不支持这两个条件。因此,我们最终会得到一个我们可以查询的额外属性and
(值uid_plus_name
)。这种做法让我头疼。通常这意味着我做错了什么。但即使这是正确的方法,头部受到伤害,我更有可能犯编码错误。所以我更愿意寻找一个更简单的解决方案。
更新(20150925):Firebase发布了一项功能,允许对多个路径进行原子写入。这与下面的方法类似,但只有一个命令。请参阅上面的更新部分,了解其工作原理。
第二种方法取决于拆分用户操作("我想将我的名字更改为' puf'")来自该操作的含义("我们需要更新配置文件中的名称,以便:209103和每个包含so:209103_puf
的邮件。
我在我们在服务器上运行的脚本中处理重命名。主要方法是这样的:
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 });
})
});
}
(这对于Firebase的最佳性能通常是一个坏主意)。但总的来说,这种方法更简单,代价是不能同时完全更新所有数据。但最终消息将全部更新以匹配新值。
第三种方法最简单:在许多情况下,您根本不必更新重复数据。在我们这里使用的示例中,您可以说每条消息都记录了我当时使用的名称。直到现在我才改变我的名字,所以有意义的是旧的消息显示我当时使用的名字。这适用于次要数据本质上是事务性的许多情况。当然,它并不适用于所有地方,但它适用于哪里?#34;不关心"是最简单的方法。
虽然上面只是对如何解决这个问题的广泛描述而且它们肯定是不完整的,但我发现每次我需要扇出重复数据时,它都会回到这些基本方法之一。
答案 1 :(得分:4)
为了向Franks添加很好的回复,我使用一组Firebase Cloud Functions实现了最终一致性方法。只要主值(例如用户名)发生更改,就会触发函数,然后将更改传播到非规范化字段。
它不像交易那么快,但在许多情况下它不需要。