如何在 Firebase 中写入非规范化数据 [英] How to write denormalized data in Firebase

查看:19
本文介绍了如何在 Firebase 中写入非规范化数据的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已阅读关于结构化数据的 Firebase 文档.数据存储便宜,但用户的时间不便宜.我们应该针对get操作进行优化,并在多处写入.

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

我在我的 javascript 应用程序中使用 ES6 和 promises 来处理异步流,主要是在第一次数据推送后从 firebase 获取 ref 键.

let addIndexPromise = new Promise((resolve, reject) => {让 newRef = ref.child('list-index').push(newItem);解决(newRef.key());//为简洁起见忽略reject()});addIndexPromise.then( key => {ref.child('list').child(key).set(newItem);});

如何确保数据在所有地方保持同步,知道我的应用程序仅在客户端上运行?

为了完整性检查,我在我的承诺中设置了一个 setTimeout 并在它解决之前关闭了我的浏览器,实际上我的数据库不再一致,并且保存了一个没有相应列表的额外索引..>

有什么建议吗?

解决方案

很好的问题.我知道三种方法,我将在下面列出.

为此我会举一个稍微不同的例子,主要是因为它允许我在解释中使用更具体的术语.

假设我们有一个聊天应用程序,我们在其中存储两个实体:消息和用户.在我们显示消息的屏幕中,我们还显示了用户的姓名.因此,为了尽量减少阅读次数,我们也在每条聊天消息中存储了用户的姓名.

用户所以:209103姓名:弗兰克·范·普费伦"地点:加利福尼亚州旧金山"问题数:12所以:3648524名称:乐高桥"地点:伦敦、布拉格、巴塞罗那"问题数:4消息-Jabhsay3487消息:如何在 Firebase 中写入非规范化数据"用户:so:3648524用户名:乐高桥"-Jabhsay3591消息:好问题."用户:so:209103用户名:Frank van Puffelen"-Jabhsay3595消息:我知道三种方法,我将在下面列出."用户:so:209103用户名: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 更新 = {};//要更新的所有路径及其新值更新['用户/'+uid+'/名称'] = 名称;var query = ref.child('messages').orderByChild('user').equalTo(uid);query.once('value', function(snapshot) {快照.forEach(函数(消息快照){更新['messages/'+messageSnapshot.key()+'/username'] = name;})ref.update(更新);});}

这会向 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) {返回 "puf";},函数(错误,提交,快照){如果(错误){console.log('交易异常失败!', error);} else if (!committed) {console.log('交易被我们的代码中止了.');} 别的 {console.log('名称在配置文件中更新,现在在消息中更新');var query = ref.child('messages').orderByChild('user').equalTo(uid);query.on('child_ added', 函数(messageSnapshot) {messageSnapshot.ref().update({ 用户名: "puf" });});}console.log("威尔玛的数据:", snapshot.val());}, false/* 不要在本地应用更改 */);

相当复杂,精明的读者会注意到我在处理消息时作弊.第一个欺骗是我从不为侦听器调用 off,但我也不使用事务.

如果我们想从客户端安全地执行此类操作,我们需要:

  1. 确保两个地方的名称匹配的安全规则.但是规则需要允许足够的灵活性,以便在我们更改名称时暂时不同.所以这变成了一个非常痛苦的两阶段提交方案.

    1. so:209103 消息的所有 username 字段更改为 null(一些魔法值)
    2. 将用户so:209103name改为'puf'
    3. 将每条消息中的usernameso:209103null 更改为puf.
    4. 该查询需要 两个条件,Firebase 查询不支持这些条件.所以我们最终会得到一个额外的属性 uid_plus_name(值为 so:209103_puf),我们可以查询它.

  2. 以事务方式处理所有这些转换的客户端代码.

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

最终一致性

更新 (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) {快照.forEach(函数(消息快照){messageSnapshot.update({ username: name });})});}

我再一次在这里采用了一些捷径,例如使用 once('value'(这对于 Firebase 的最佳性能来说通常是一个坏主意).但总体而言,该方法更简单,在不会同时完全更新所有数据的成本.但最终消息将全部更新以匹配新值.

不在乎

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

总结

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

I've read the Firebase docs on Stucturing Data. Data storage is cheap, but the user's time is not. We should optimize for get operations, and write in multiple places.

So then I might store a list node and a list-index node, with some duplicated data between the two, at very least the list name.

I'm using ES6 and promises in my javascript app to handle the async flow, mainly of fetching a ref key from firebase after the first data push.

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);
 });

How do I make sure the data stays in sync in all places, knowing my app runs only on the client?

For sanity check, I set a setTimeout in my promise and shut my browser before it resolved, and indeed my database was no longer consistent, with an extra index saved without a corresponding list.

Any advice?

解决方案

Great question. I know of three approaches to this, which I'll list below.

I'll take a slightly different example for this, mostly because it allows me to use more concrete terms in the explanation.

Say we have a chat application, where we store two entities: messages and users. In the screen where we show the messages, we also show the name of the user. So to minimize the number of reads, we store the name of the user with each chat message too.

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"

So we store the primary copy of the user's profile in the users node. In the message we store the uid (so:209103 and so:3648524) so that we can look up the user. But we also store the user's name in the messages, so that we don't have to look this up for each user when we want to display a list of messages.

So now what happens when I go to the Profile page on the chat service and change my name from "Frank van Puffelen" to just "puf".

Transactional update

Performing a transactional update is the one that probably pops to mind of most developers initially. We always want the username in messages to match the name in the corresponding profile.

Using multipath writes (added on 20150925)

Since Firebase 2.3 (for JavaScript) and 2.4 (for Android and iOS), you can achieve atomic updates quite easily by using a single multi-path update:

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);
  });
}

This will send a single update command to Firebase that updates the user's name in their profile and in each message.

Previous atomic approach

So when the user change's the name in their profile:

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 */);

Pretty involved and the astute reader will notice that I cheat in the handling of the messages. First cheat is that I never call off for the listener, but I also don't use a transaction.

If we want to securely do this type of operation from the client, we'd need:

  1. security rules that ensure the names in both places match. But the rules need to allow enough flexibility for them to temporarily be different while we're changing the name. So this turns into a pretty painful two-phase commit scheme.

    1. change all username fields for messages by so:209103 to null (some magic value)
    2. change the name of user so:209103 to 'puf'
    3. change the username in every message by so:209103 that is null to puf.
    4. that query requires an and of two conditions, which Firebase queries don't support. So we'll end up with an extra property uid_plus_name (with value so:209103_puf) that we can query on.

  2. client-side code that handles all these transitions transactionally.

This type of approach makes my head hurt. And usually that means that I'm doing something wrong. But even if it's the right approach, with a head that hurts I'm way more likely to make coding mistakes. So I prefer to look for a simpler solution.

Eventual consistency

Update (20150925): Firebase released a feature to allow atomic writes to multiple paths. This works similar to approach below, but with a single command. See the updated section above to read how this works.

The second approach depends on splitting the user action ("I want to change my name to 'puf'") from the implications of that action ("We need to update the name in profile so:209103 and in every message that has user = so:209103).

I'd handle the rename in a script that we run on a server. The main method would be something like this:

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 again I take a few shortcuts here, such as using once('value' (which is in general a bad idea for optimal performance with Firebase). But overall the approach is simpler, at the cost of not having all data completely updated at the same time. But eventually the messages will all be updated to match the new value.

Not caring

The third approach is the simplest of all: in many cases you don't really have to update the duplicated data at all. In the example we've used here, you could say that each message recorded the name as I used it at that time. I didn't change my name until just now, so it makes sense that older messages show the name I used at that time. This applies in many cases where the secondary data is transactional in nature. It doesn't apply everywhere of course, but where it applies "not caring" is the simplest approach of all.

Summary

While the above are just broad descriptions of how you could solve this problem and they are definitely not complete, I find that each time I need to fan out duplicate data it comes back to one of these basic approaches.

这篇关于如何在 Firebase 中写入非规范化数据的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆