RethinkDB - 更新嵌套数组

16

我有一个调查表格,看起来像这样:

{
  id: Id,
  date: Date,
  clients: [{
    client_id: Id,
    contacts: [{
      contact_id: Id,
      score: Number,
      feedback: String,
      email: String
    }]
  }]
}

我需要更新特定联系人下的“score”和“feedback”字段。目前,我是这样运行更新操作的:
function saveScore(obj){
  var dfd = q.defer();
  var survey = surveys.get(obj.survey_id);

  survey 
    .pluck({ clients: 'contacts' })
    .run()
    .then(results => {

      results.clients.forEach((item, outerIndex) => {
        item.contacts.forEach((item, index, array) => {
          if(Number(item.contact_id) === Number(obj.contact_id)) {
            array[index].score = obj.score;
            console.log(outerIndex, index);
          }
        });
      });

      return survey.update(results).run()
    })
    .then(results => dfd.resolve(results))
    .catch(err => dfd.resolve(err));

  return dfd.promise;
};

当我看到update方法时,它指定了如何更新嵌套的键值对。然而,我找不到任何更新数组中单个项的示例。有没有更好且更清晰的方法来更新嵌套数组中的项?
6个回答

9
您可能需要获取数组,从数组中过滤出所需值,然后将其再次附加到数组中。然后您可以将更新后的数组传递给update方法。 示例 假设您有一个文档,其中包含两个客户端,每个客户端都有一个name和一个score,您想要更新其中一个的分数:
{
  "clients": [
    {
      "name":  "jacob" ,
      "score": 200
    } ,
    {
      "name":  "jorge" ,
      "score": 57
    }
  ] ,
  "id":  "70589f08-284c-495a-b089-005812ec589f"
}

您可以获取特定的文档,使用匿名函数运行update命令,然后将更新后的新数组传递到clients属性中。
r.table('jacob').get("70589f08-284c-495a-b089-005812ec589f")
  .update(function (row) {
    return {
      // Get all the clients, expect the one we want to update
      clients: row('clients').filter(function (client) {
        return client('name').ne('jorge')
      })
      // Append a new client, with the update information
      .append({ name: 'jorge', score: 57 })
    };
  });

我认为这有点繁琐,可能有更好、更优雅的方法来解决问题,但这应该可以解决你的问题。
数据库架构
也许值得为所有联系人创建一个“contacts”表,然后在数据上进行一些连接。那么你的“clients”数组中的“contacts”属性将会是这样的:
{
  id: Id,
  date: Date,
  clients: [{
    client_id: Id,
    contact_scores: {
      Id: score(Number)
    },
    contact_feedbacks: {
      Id: feedback(String)
    }
  }]
}

1
如果我理解正确的话,这个解决方案不允许更新数组中的单个对象,并且会改变数组对象的顺序?所有与 { name: 'jorge' } 匹配的数组对象都被过滤掉,然后更新后的 { name: 'jorge'} 对象被追加到数组末尾。 - tim-montague

6
数据库架构
{
  "clients": [
    {
      "name":  "jacob" ,
      "score": 200
    } ,
    {
      "name":  "jorge" ,
      "score": 57
    }
  ] ,
  "id":  "70589f08-284c-495a-b089-005812ec589f"
}

那么你可以使用 mapbranch 查询来实现这个功能。
r.db('users').table('participants').get('70589f08-284c-495a-b089-005812ec589f')
  .update({"clients": r.row('clients').map(function(elem){
     return r.branch(
      elem('name').eq("jacob"),
      elem.merge({ "score": 100 }),
      elem)})
    })

这绝对比顶部答案使用“append”更好,因为它不会改变数组中对象的顺序。但是,这个解决方案也没有解决用户的问题“我需要更新特定联系人下的得分和反馈字段”,而是更新了所有名为“jacob”的联系人。 - tim-montague
@tfmontague 谢谢。我开始学习RethinkDB,但缺少一些主要概念。 - zabusa
@zabusa 很好的回答,我认为。请看看我的回答,展示了如何将这种技术抽象化以轻松执行嵌套更新。 - Mulan

4

它对我很有效

r.table(...).get(...).update({
contacts: r.row('Contacts').changeAt(0,
  r.row('Contacts').nth(0).merge({feedback: "NICE"}))
 })

只有在我们知道数组中项目的位置时,这才有效。 - SonickSeven

1

ReQL解决方案

在ReThinkDB(以及大多数查询语言)中,创建一个用于原地更新JSON对象数组的查询是一个相当复杂的过程。我所知道的ReQL中最好(也是唯一)的解决方案是使用updateoffsetsOfdochangeAtmerge函数的组合。此解决方案将保留数组中对象的顺序,并且仅修改与offsetsOf方法匹配的对象的值。

以下代码(或类似代码)可用于更新包含对象数组(即clients)的对象数组(即contracts)。

必须提供'%_databaseName_%''%_tableName_%''%_documentUUID_%'%_clientValue_%%_contractValue_%

r.db('%_databaseName_%').table('%_tableName_%').get('%_documentUUID_%').update(row =>

    row('clients')
      .offsetsOf(clients => client('client_id').eq('%_clientValue_%'))(0)
      .do(clientIndex => ({

        clients: row('clients')(clientIndex)
          .offsetsOf(contacts => contact('contact_id').eq('%_contactValue_%')))(0)
          .do(contactIndex => ({
            contacts: row(clientIndex)
              .changeAt(contractIndex, row(clientIndex)(contractIndex).merge({
                'score': 0,
                'feedback': 'xyz'
              }))
          })
      }))
)

为什么要将这个转换成ReQL格式,值得麻烦吗?
  survey 
    .pluck({ clients: 'contacts' }).run()
    .then(results => {

      results.clients.forEach((item, outerIndex) => {
        item.contacts.forEach((item, index, array) => {
          if(Number(item.contact_id) === Number(obj.contact_id)) {
            array[index].score = obj.score;
            console.log(outerIndex, index);
          }
        });
      });

      return survey.update(results).run()
    })

虽然Jacob提供的代码(在Stack Overflow上询问问题的用户 - 如上所示)可能看起来更简单易写,但性能可能不如ReQL解决方案。

1)ReQL解决方案在查询服务器(即数据库端)上运行,因此在数据库写入期间优化了代码(性能更高)。而上面的代码没有充分利用查询服务器,并且进行了读取和写入请求pluck().run()update().run(),并且数据在执行pluck()查询后在客户端请求端(即NodeJs端)进行处理(性能较低)。

2)上述代码需要查询服务器将所有数据发送回客户端请求端(即NodeJs端),因此响应负载(互联网带宽使用/下载大小)可以达到数兆字节。而ReQL解决方案在查询服务器上处理,因此响应负载通常只是确认写入已完成,换句话说,只有几个字节被发送回客户端请求端。这是通过单个请求完成的。

ReQL过于复杂

然而,当处理JSON时,ReQL(尤其是SQL)似乎过于复杂,我认为应该在处理JSON时使用JSON。我还建议ReThinkDB社区采用一种替代ReQL的方法,该方法使用JSON(https://github.com/rethinkdb/rethinkdb/issues/6736)。更新嵌套的JSON数组的解决方案应该很简单...
r('database.table').update({
  clients: [{
    client_id: 0,
    contacts: [{
      contact_id: 0,
      score: 0,
      feedback: 'xyz',
    }]
  }]
});

两个问题,1)每次使用offsetsOf(...)(0)都有抛出错误的风险。您可以通过适当使用r.branch来解决这个问题;2)您提出的“ReQL太复杂”的JSON查询没有意义。哪个客户端中的联系人会被更新?是客户端0中的联系人0吗? - Mulan
@NathanDrake - (1)您能解释一下抛出错误的风险以及为什么r.branch可以解决这个问题吗?我不太理解。(2)代码的意图只是为了传达这个想法。但是,我已经添加了client_idcontact_id字段,以使示例更加字面化。 - tim-montague
在更新的JSON更新提案中,您可以将{client_id,...}{contact_id,...}添加到补丁中,但没有任何内容告诉rethinkdb要更新数组中的哪个记录。您希望使用client_idcontact_id作为某种索引/键,但是rethinkdb对数组没有索引/键的概念 - 只适用于表格。 - Mulan
@NathanDrake - “但是rethinkdb在数组上没有索引/键的概念 - 只适用于表格” - 是的,但这不是我建议数组的原因。提案是使用定义好的JSON结构,并将其与数组中的元素(对象结构)进行匹配。因此,这甚至与ID无关,而是与两个结构的相似程度有关。 - tim-montague
@NathanDrake - "如果offsetsOf(...)返回一个空结果,那么就没有(0)可以返回。[并且]会导致错误,e:索引超出范围: 0"。- 那么问题是什么?如果要更新的键值不存在,则应该抛出错误,以便开发人员决定如何处理错误。 - tim-montague
显示剩余2条评论

0

tfmontague 的回答是正确的,但我认为他的答案可以有很大的改进空间。因为他使用了 ...(0),所以他的答案可能会出现错误。

zabusa 也提供了一个使用 mapbranch 的 ReQL 解决方案,但没有展示完整的嵌套更新。我将扩展这个技术。

ReQL 表达式是可组合的,因此我们可以隔离复杂性并避免重复。这使得代码简洁而清晰。

首先编写一个简单的函数 mapIf

const mapIf = (rexpr, test, f) =>
  rexpr.map(x => r.branch(test(x), f(x), x));

现在我们可以编写简化的updateClientContact函数

const updateClientContact = (doc, clientId, contactId, patch) =>
  doc.merge
  ( { clients:
        mapIf
        ( doc('clients')
        , c => c('client_id').eq(clientId)
        , c =>
            mapIf
            ( c('contacts')
            , c => c('contact_id').eq(contactId)
            , c =>
                c.merge(patch)
            )
        )
    }
  );

使用方法如下

// fetch the document to update
const someDoc =
  r.db(...).table(...).get(...);

// create patch for client id [1] and contact id [12]
const patch =
  updateClientContact(someDoc, 1, 12, { name: 'x', feedback: 'z' });

// apply the patch
someDoc.update(patch);

这里有一个具体的例子,你可以在 reql> ... 中运行。

const testDoc =
  { clients:
      [ { client_id: 1
        , contacts:
            [ { contact_id: 11, name: 'a' }
            , { contact_id: 12, name: 'b' }
            , { contact_id: 13, name: 'c' }
            ]
        }
      , { client_id: 2
        , contacts:
            [ { contact_id: 21, name: 'd' }
            , { contact_id: 22, name: 'e' }
            , { contact_id: 23, name: 'f' }
            ]
        }
      , { client_id: 3
        , contacts:
            [ { contact_id: 31, name: 'g' }
            , { contact_id: 32, name: 'h' }
            , { contact_id: 33, name: 'i' }
            ]
        }
      ]
  };

updateClientContact(r.expr(testDoc), 2, 23, { name: 'x', feedback: 'z' });

结果将会是

{ clients:
    [ { client_id: 1
      , contacts:
          [ { contact_id: 11, name: 'a' }
          , { contact_id: 12, name: 'b' }
          , { contact_id: 13, name: 'c' }
          ]
      }
    , { client_id: 2
      , contacts:
          [ { contact_id: 21, name: 'd' }
          , { contact_id: 22, name: 'e' }
          , { contact_id: 23, name: 'x', feedback: 'z' } // <--
          ]
      }
    , { client_id: 3
      , contacts:
          [ { contact_id: 31, name: 'g' }
          , { contact_id: 32, name: 'h' }
          , { contact_id: 33, name: 'i' }
          ]
      }
    ]
}

"因为他使用了...(0),所以他的答案有可能会抛出错误。" - 是啊,但为什么不处理这个错误呢?如果用户试图在不存在的键值对上进行更新操作,那么应该抛出一个错误。 - tim-montague

0

迟做总比不做好

我曾经有和你一样的问题,我找到了两种解决方法:

使用特定的client_id

r.db('nameDB').table('nameTable').get('idRegister')
.update({'clients': r.row('clients')
    .map(elem=>{
        return r.branch(
            elem('client_id').eq('your_specific_client_id'),
            elem.merge({
                contacts: elem('contacts').map(elem2=>
                    r.branch(
                        elem2('contact_id').eq('idContact'),
                        elem2.merge({
                            score: 99999,
                            feedback: 'yourString'
                        }),
                        elem2
                    )
                )
            }),
            elem
        )
    })
})

没有特定的client_id

r.db('nameDB').table('nameTable').get('idRegister')
.update({'clients': r.row('clients')
    .map(elem=>
        elem.merge({
            contacts: elem('contacts').map(elem2=>
                r.branch(
                    elem2('contact_id').eq('idContact'),
                    elem2.merge({
                        score: 99999,
                        feedback: 'yourString'
                    }),
                    elem2
                )
            )
        })
    )
})

我希望这对你有用,即使很久以前发生过。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接