【问题标题】:Firebase related fields populationFirebase 相关领域人口
【发布时间】:2023-04-02 20:13:01
【问题描述】:

我正在尝试使用 Firebase + Express 重新创建 Mongoose + Express 应用。但我试图用参考模型“填充”(如 Mongoose)相关字段,但这有点手动,也许有更短的方法来实现这一点。

在 Mongoose 中,您只需编码 Ticket.find({}).populate('category'),它就会自动为您提供相关的对象映射。我不知道如何在 Firebase ORM 中实现这一点。我的代码看起来像这样,但没有正确填充类别:

export const tickets = (req, res) => {
    // First model reference
    const ref = database.ref('tickets');
    ref.once('value', snapshot => {
        let tickets: object = [];
        if (snapshot.val()) {
            const obj = snapshot.val();
            tickets = Object.keys(obj).map(key => {
                const ticket = { key, ...obj[key] };
                if (ticket.category) {
                    // Related model reference
                    const categoriesRef = database.ref(`categories/${ticket.category}`);
                    categoriesRef.once('value', snapshot => {
                        if (snapshot.val()) {
                            ticket.category = { key: snapshot.key, ...snapshot.val() };
                        }   
                    }).catch(error => res.status(500).json({ error: error.message }));
                }
                return ticket;
            });
        }
        res.json(tickets);
    }).catch(error => res.status(500).json({ error: error.message }));
};

此代码返回所有票证。它们应根据其键包括其相关的“类别”。你知道更短的路吗?

【问题讨论】:

  • Firebase 中没有 JOIN 语句的概念,所以你所做的对我来说看起来并不过分。运行这段代码有什么问题吗?
  • let tickets: object = []; 应该是let tickets = [] as any[];
  • @FrankvanPuffelen,是的,category 没有被填写。我猜这是因为 JS 在得到它的类别之前返回了票。

标签: javascript node.js firebase express firebase-realtime-database


【解决方案1】:

我建议只使用 Promise API 而不是回调,因为这样可以更轻松地处理错误。另外,在您的代码中,您还将回调和 Promise API 混合在一起,这将导致一些不一致。

不包含您​​的类别的原因是因为您在 category 字段更新之前返回了 ticket 对象。

if (ticket.category) {
    // Related model reference
    const categoriesRef = database.ref(`categories/${ticket.category}`);
    categoriesRef.once('value', snapshot => {
        if (snapshot.val()) {
            ticket.category = { key: snapshot.key, ...snapshot.val() };
        }   
    }).catch(error => res.status(500).json({ error: error.message }));
}
return ticket; // this is evaluated before any of the above asynchronous stuff

要解决这个问题,您必须返回一个带有“组装”票证的 Promise。

if (!ticket.category) {
  return Promise.resolve(ticket);
}

return categoriesRef.once('value')
  .then(categorySnapshot => {
    const category = categorySnapshot.val();
    category.key = categorySnapshot.key;
    ticket.category = category;

    return ticket;
  });

代码

将其混合到您的代码中并删除一些语法糖以提高性能,结果是:

export const tickets = (req, res) => {
  const categoriesRef = database.ref('categories'); // Changed: moved to start of function
  const ref = database.ref('tickets');

  ref.once('value')
    .then(snapshot => {
      const ticketPromises = []; // array of promises to assembled tickets
      snapshot.forEach(ticketSnapshot => { // NOTE: This is `DataSnapshot#forEach()` not `Array#forEach()`
        // changed: val() creates a fresh object, so we can modify
        //          it without using the spread operator
        const ticket = ticketSnapshot.val(); 
        ticket.key = ticketSnapshot.key;

        if (!ticket.category) {
          // no further assembly required, return the ticket as is
          ticketPromises.push(Promise.resolve(ticket));
          return;
        }

        const ticketPromise = categoriesRef.child(ticket.category).once('value')
          .then(categorySnapshot => {
            // same as before, no need for spread operator
            const category = ticket.category = categorySnapshot.val();
            category.key = categorySnapshot.key;

            return ticket; // return assembled ticket
          });

        ticketPromises.push(ticketPromise);
      });

      return Promise.all(ticketPromises); // wait for all tickets
    })
    .then(tickets => {
      res.json(tickets); // return tickets to client
    })
    .catch(error => {
      console.log(error);
      res.status(500).json({ error: error.message })
    });
};

带值缓存的代码

因为您也可能在不修改数据的情况下多次请求同一类别,您还可以使用以下代码缓存类别 {...data, key} 对象以节省内存和边际计算时间:

export const tickets = (req, res) => {
  const categoryCachedValues = new CachedValues(database.ref('categories'), "key");
  const ref = database.ref('tickets');

  ref.once('value')
    .then(snapshot => {
      const ticketPromises = [];
      snapshot.forEach(ticketSnapshot => { // NOTE: This is `DataSnapshot#forEach()` not `Array#forEach()`
        const ticket = ticketSnapshot.val(); 
        ticket.key = ticketSnapshot.key;

        if (!ticket.category) {
          // no further assembly required, return the ticket as is
          ticketPromises.push(Promise.resolve(ticket));
          return;
        }

        const ticketPromise = categoryCachedValues.get(ticket.category)
          .then(categoryData => {
            ticket.category = categoryData;
            return ticket; // return assembled ticket
          });

        ticketPromises.push(ticketPromise);
      });

      return Promise.all(ticketPromises); // wait for all tickets
    })
    .then(tickets => {
      res.json(tickets); // return tickets to client
    })
    .catch(error => {
      console.log(error);
      res.status(500).json({ error: error.message })
    });
};

class CachedValues {
  constructor(ref, keyFieldName) {
    this._ref = ref;
    this._promises = {};
    if (keyFieldName) {
      this._extractData = (snapshot) => {
        const data = snapshot.val();
        data[keyFieldName] = data.key;
        return data;
      };
    } else {
      this._extractData = (snapshot) => snapshot.val();
    }
  }

  get(path) {
    if (!this._promises[path]) {
      this._promises[path] = this._ref.child(path)
        .once('value')
        .then(this._extractData);
    }
    return this._promises[path];
  }
}

【讨论】:

  • 第一个选项效果很好。我可能需要缓存值,但我必须更好地理解它们。谢谢。