RethinkDB - обновление вложенного массива

У меня есть таблица опроса, которая выглядит так:

{
  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, он указывает, как обновлять пары вложенных ключей: значение. Тем не менее, я не могу найти примеры для обновления отдельного элемента в массиве.

Есть ли лучший и, надеюсь, более чистый способ обновления элементов во вложенном массиве?

Ответы

Ответ 1

Вам может потребоваться получить массив filter из требуемого значения в массиве, а затем добавить его снова в массив. Затем вы можете передать обновленный массив методу 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 для всех ваших контактов, а затем выполнить какое-то соединение с вами. Тогда ваше свойство contacts в вашем массиве clients будет выглядеть примерно так:

{
  id: Id,
  date: Date,
  clients: [{
    client_id: Id,
    contact_scores: {
      Id: score(Number)
    },
    contact_feedbacks: {
      Id: feedback(String)
    }
  }]
}

Ответ 2

он работает для меня

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

Ответ 3

схема базы данных

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

то вы можете сделать это с помощью запросов map и branch.

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

Ответ 4

Решение ReQL

Создание запроса для обновления массива объектов JSON на месте - довольно сложный процесс в ReThinkDB (и в большинстве языков запросов). Лучшее (и единственное) решение в ReQL, о котором я знаю, - это использовать комбинацию функций update, offsetsOf, do, changeAt и merge. Это решение будет сохранять порядок объектов в массиве и изменять значения только тех объектов, которые совпадают в методах 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 (пользователь, который задал вопрос здесь о переполнении стека - показан выше), может показаться более простым для написания, производительность, вероятно, не так хороша, как решение ReQL.

1) Решение ReQL работает на сервере запросов (то есть на стороне базы данных), и поэтому код оптимизируется во время записи в базу данных (более высокая производительность). Принимая во внимание, что код выше, не в полной мере использует сервер запросов, и делает запрос на чтение и запись pluck().run() и update().run(), а данные обрабатываются на стороне клиентского запроса ( то есть сторона NodeJs) после выполнения запроса pluck pluck() (более низкая производительность).

2) Приведенный выше код требует, чтобы сервер запросов отправлял обратно все данные стороне запроса клиента (то есть стороне NodeJ), и поэтому полезная нагрузка ответа (использование полосы пропускания интернета/размер загрузки) может составлять несколько мегабайт. В то время как решение ReQL обрабатывается на сервере запросов, и поэтому полезная нагрузка ответа, как правило, просто подтверждает, что запись была завершена, другими словами, только несколько байтов отправляются обратно на сторону клиентского запроса. Что делается в одном запросе.

ReQL слишком сложный

Однако ReQL (и особенно SQL) кажется слишком сложным при работе с JSON, и мне кажется, что 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',
    }]
  }]
});

Ответ 5

Tfmontague находится на правильном пути, но я думаю, что его ответ может быть значительно улучшен. Поскольку он использует ...(0) есть возможность для его ответа выбросить ошибки.

zabusa также предоставляет решение ReQL с использованием map и branch но не показывает полное вложенное обновление. Я остановлюсь на этой технике.

Выражения 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' }
          ]
      }
    ]
}