2015-06-07 26 views
24

我已阅读Stucturing Data上的Firebase文档。数据存储便宜,但用户的时间不是。我们应该优化获取操作,并在多个地方写入。如何在Firebase中编写非规范化数据

于是我可能会存储列表节点和列表索引节点,两者之间存在一些重复的数据,在最起码的列表名称。

我在使用ES6和承诺在我的JavaScript应用程序来处理异步流程,主要是在第一次数据推送后从Firebase中获取ref key。

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和关闭我的浏览器,它解决之前,确实我的数据库已经不再一致,保存没有相应的列表的额外的索引。

有什么建议吗?

+3

*我会在下面写出正确的答案。*第一句让我开心。你可以在生日派对上重复吗?特别是当开发者朋友在场时。 :-) –

+0

这似乎有关,也.. https://stackoverflow.com/questions/47334382 – Fattie

回答

47

伟大的问题。我知道三种方法,我将在下面列出。

我会稍微改变一下这个例子,主要是因为它允许我在解释中使用更具体的术语。

假设我们有一个聊天应用程序,我们在这里存储两个实体:消息和用户。在我们显示消息的屏幕中,我们还显示用户的名字。因此,为了尽量减少读取次数,我们还将每个聊天消息的用户名称都存储起来。

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等:3648524),以便我们可以查找用户。但我们将用户的名称存储在消息中,以便我们不必为每个用户查看此信息,当我们要显示消息列表。

因此,现在当我进入聊天服务的配置文件页面并将我的名字从“Frank van Puffelen”更改为“puf”时会发生什么。

事务更新

执行事务更新是可能啪啪的大多数开发商心中最初的一个。我们总是希望邮件中的username与相应配置文件中的name匹配。

使用多写(上加20150925)

由于火力地堡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); 
    }); 
} 

这将发送一个update命令火力地堡,在他们的个人资料,并在每个消息中更新用户的名字。

上一页原子的方法

因此,当用户改变在他们的个人资料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,但我也不使用交易。

如果我们想要安全地做这种类型从客户端的运行,我们需要:

  1. 安全规则,以确保在这两个地方的名称一致。但是规则需要有足够的灵活性让他们在我们改变名字时暂时有所不同。所以这变成了一个相当痛苦的两阶段提交方案。
    1. 改变所有username字段的消息通过so:209103null(一些魔术值)
    2. 变化的用户so:209103name到每个消息中“PUF”
    3. 变化username通过so:209103nullpuf
    4. 该查询需要两个条件的and,这两个条件是Firebase查询不支持的。所以我们最终会得到一个额外的属性uid_plus_name(价值so:209103_puf),我们可以查询。
  2. 以事务方式处理所有这些转换的客户端代码。

这种类型的方法让我头疼。通常这意味着我做错了什么。但即使这是正确的方法,头部伤害我更容易犯错编码。所以我更喜欢寻找更简单的解决方案。

最终一致性

更新(20150925):火力地堡释放的特征,以允许原子写入多个路径。这与下面的方法类似,但使用单个命令。请参阅上面的更新部分,了解其工作原理。

第二种方法取决于将用户操作(“我想将我的名字更改为'puf'”)从该操作的含义(“我们需要更新配置文件中的名称: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的最佳性能来说通常是一个坏主意),但总体而言,这种方法更简单,没有在同一时间完全更新所有数据的成本。但最终消息将全部更新以匹配新值。

不关心

第三种方法是最简单的的:在许多情况下,你真的没有在所有更新复制数据。在我们这里使用的例子中,您可以说每条消息都记录了当时我使用的名称。直到现在我还没有改变自己的名字,所以旧信息显示我当时使用的名字是有道理的。这适用于二手数据本质上是交易性的许多情况。它当然不适用于任何地方,但它适用于“不关心”是最简单的方法。

摘要

虽然上面是你如何能解决这个问题只是宽泛的描述,他们是绝对不完整的,我发现,每次我要扇出重复数据它回来这些基本的一个方法。

+1

谢谢弗兰克伟大的答案!事实上,我采用了“不关心”的方法。我没有切换写操作,因此该项目在索引之前出现,所以我不会冒险将列表中的项目链接到没有数据的地方(但是现在,我可能会得到一个孤立的列表项目,而不是一大笔交易)。 – legolandbridge

+0

这么好的答案。您所提出的建议非常有用,适用于许多场景,而不仅仅是Firebase。 –

+1

小心确保延迟服务器任务的*最终一致性*并检查名称是否还未更改。 – AJcodez

2

要添加到法兰克斯伟大的答复,我实施了一套最终一致性方法Firebase Cloud Functions。只要主值(例如用户名)发生变化,函数就会被触发,然后将更改传播到非规格化字段。

它不像交易一样快,但在很多情况下并不需要。

+1

伟大的加法Uffe。只要您没有严格的实时或离线要求,云功能就非常适用。 –