如何在 Firebase 中写入非规范化数据

Posted

技术标签:

【中文标题】如何在 Firebase 中写入非规范化数据【英文标题】:How to write denormalized data in Firebase 【发布时间】:2015-08-22 00:18:31 【问题描述】:

我已阅读 Stucturing Data 上的 Firebase 文档。数据存储很便宜,但用户的时间却不是。我们应该针对 get 操作进行优化,并在多个地方写入。

那么我可能会存储一个 list 节点和一个 list-index 节点,两者之间有一些重复的数据,至少是列表名称。

我在我的 javascript 应用程序中使用 ES6 和 promise 来处理异步流,主要是在第一次数据推送后从 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 并在它解决之前关闭了我的浏览器,实际上我的数据库不再一致,在没有相应列表的情况下保存了一个额外的索引。 p>

有什么建议吗?

【问题讨论】:

我会在下面写一个正确的答案。第一句话让我很开心。你能在生日聚会上重复一遍吗?尤其是当开发者朋友在场时。 :-) 这似乎也很相关.. ***.com/questions/47334382 【参考方案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(适用于 androidios)开始,您可以通过使用单个多路径更新非常轻松地实现原子更新:

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(一些神奇的值) 将用户so:209103name 更改为“puf” 将每条消息中的usernameso:209103null 更改为puf。 该查询需要两个条件的and,Firebase 查询不支持这些条件。所以我们最终会得到一个额外的属性uid_plus_name(值为so:209103_puf),我们可以对其进行查询。
    以事务方式处理所有这些转换的客户端代码。

这种方法让我头疼。通常这意味着我做错了什么。但即使这是正确的方法,如果我的脑袋很痛,我也更有可能犯编码错误。所以我更喜欢寻找更简单的解决方案。

最终一致性

更新 (20150925):Firebase 发布了一项功能,允许对多个路径进行原子写入。这与下面的方法类似,但使用单个命令。请参阅上面的更新部分以了解其工作原理。

第二种方法取决于将用户操作(“我想将我的名字更改为 '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 获得最佳性能的一个坏主意)。但总体而言,该方法更简单,代价是不能同时完全更新所有数据。但最终消息将全部更新以匹配新值。

不在乎

第三种方法是最简单的:在许多情况下,您根本不需要更新重复的数据。在我们在这里使用的示例中,您可以说每条消息都记录了我当时使用的名称。我直到现在才改名字,所以旧消息显示我当时使用的名字是有道理的。这适用于辅助数据本质上是事务性的许多情况。当然,它并不适用于所有地方,但它适用于“不关心”是所有方法中最简单的方法。

总结

虽然以上只是对如何解决此问题的广泛描述,而且它们绝对不完整,但我发现每次我需要扇出重复数据时,它都会回到这些基本方法之一。

【讨论】:

感谢 Frank 的精彩回答!事实上,我采用了“不关心”的方法。我确实切换了我的写入操作,因此该项目位于索引之前,所以我不会冒险在列表中拥有一个链接到没有数据的地方的项目(但现在我可能会得到一个孤立的列表项目,这不是很重要)。 这么好的答案。您概述的建议非常有用,适用于许多场景,而不仅仅是 firebase。 小心,确保在最终一致性中延迟服务器任务,并检查名称是否再次更改。【参考方案2】:

为了补充 Franks 的精彩回复,我使用一组 Firebase Cloud Functions 实现了最终一致性方法。只要主值(例如用户名)发生更改,就会触发这些函数,然后将更改传播到非规范化字段。

它没有事务那么快,但在很多情况下它不需要。

【讨论】:

伟大的补充乌菲。只要您没有严格的实时或离线要求,Cloud Functions 就非常适合。

以上是关于如何在 Firebase 中写入非规范化数据的主要内容,如果未能解决你的问题,请参考以下文章

尝试了解在我的场景中使用 firebase 数据库进行非规范化工作的效果如何

AngularFire - 如何查询非规范化数据?

updateChildValue 写入数据

如何向 Firebase 验证服务器?

新的 Firebase 数据导致 TableView 单元格闪烁 (Firebase/iOS/Swift)

当外部程序将多条记录写入一条记录时,如何规范化数据?