Tengo problemas para 'completar' mi matriz. El siguiente código crea duplicados en mi matriz answers que definitivamente no quiero y por ahora es evidente que $push no funcionará. He intentado usar las diferentes metodologías que veo en SO por un tiempo, pero ninguna me funciona. Con esta aplicación web, los usuarios pueden ver una pregunta en el sitio web y responder con un 'sí' o 'no' response y se les permite cambiar (confirmar) su response en cualquier momento lo que significa que una especie de upsert tiene lugar en la base de datos en diferentes momentos. ¿Cómo evitar esto?

var QuestionSchema = Schema ({
    title       :String,
    admin       :{type: String, ref: 'User'},
    answers     :[{type:  Schema.Types.Mixed, ref: 'Answer'}]
});

var AnswerSchema = Schema ({
    _question   :{type: ObjectId, ref: 'Question'},
    employee    :{type: String, ref: 'User'},
    response    :String,
    isAdmin     :{type: Boolean, ref: 'User'}
})

var UserSchema = Schema({
    username    : String,
    isAdmin     : {type: Boolean, default: false}
});

module.exports = mongoose.model('Question', QuestionSchema);
module.exports = mongoose.model('Answer', AnswerSchema);
module.exports = mongoose.model('User', UserSchema);


           Question.update(
                {_id: req.body.id},
                {$push: {answers: {_question: req.body.id, 
                                    employee: req.body.employee, 
                                    response: req.body.response, //this variable changes (yes/no/null)
                                     isAdmin: req.body.isAdmin}}},
                {safe: true, upsert: true},
                function(err, model) {

                }
            );
2
colin_dev256 20 jun. 2017 a las 21:13

2 respuestas

La mejor respuesta

Según lo veo, pareces un poco confundido y se refleja en tu esquema. No parece comprender completamente las diferencias entre "incrustado" y "referenciado", ya que su esquema es en realidad una "mezcla" no válida de las dos técnicas.

Probablemente sea mejor guiarte a través de ambos.

Modelo integrado

Entonces, en lugar del esquema que ha definido, debería tener algo más como esto:

var QuestionSchema = Schema ({
    title       :String,
    admin       :{type: String, ref: 'User'},
    answers     :[AnswerSchema]
});

var AnswerSchema = Schema ({
    employee    :{type: String, ref: 'User'},
    response    :String,
    isAdmin     :{type: Boolean, ref: 'User'}
})

mongoose.model('Question', questionSchema);

NOTA : Question es el único modelo real aquí. El AnswerSchema está completamente "incrustado".

Tenga en cuenta la definición clara del "esquema" donde la propiedad "answers" en Question se define como una "matriz" de AnswerSchema. Así es como se incrusta y mantiene el control de los tipos dentro del objeto dentro de la matriz.

En cuanto a la actualización, hay un patrón lógico claro pero simplemente no lo está aplicando. Todo lo que necesita hacer es "decir" a la actualización que no desea "empujar" un nuevo elemento si ya existe algo para ese "único" "employee" en la matriz.

También. Esto es NO y "upsert". Upsert implica "crear uno nuevo", que es diferente a lo que quieres. Desea "empujar" a la matriz de una pregunta "existente". Si deja "upsert" allí, entonces algo no encontrado crea una nueva pregunta. Lo cual, por supuesto, está mal aquí.

Question.update(
  { 
    "_id": req.body.id,
    "answers.employee": { "$ne": req.body.employee },
    }
  },
  { "$push": {
    "answers": { 
      "employee": req.body.employee, 
      "response": req.body.response,
      "isAdmin": req.body.isAdmin
    }
  }},
  function(err, numAffected) {

  });

Eso buscará verificar que el "único" "employee" en los miembros de la matriz ya y solo $push donde todavía no está allí.

Como beneficio adicional, si tiene la intención de permitir que el usuario "cambie su respuesta", entonces hacemos este encantamiento con .bulkWrite():

Question.collection.bulkWrite(
  [
    { "updateOne": { 
      "filter": {
        "_id": req.body.id,
        "answers.employee": req.body.employee,
      },
      "update": {
        "$set": {
          "answers.$.response": req.body.response,
        }
      }
    }},
    { "updateOne": { 
      "filter": {
        "_id": req.body.id,
        "answers.employee": { "$ne": req.body.employee },
      },
      "update": {
        "$push": {
          "answers": { 
            "employee": req.body.employee, 
            "response": req.body.response,
            "isAdmin": req.body.isAdmin
          }
        }
      }
    }}
  ],
  function(err, writeResult) {

  }
);

Esto pone efectivamente dos actualizaciones en una. El primero en intentar alterar una respuesta existente y $set la respuesta en la posición coincidente, y la segunda para intentar agregar una nueva respuesta donde no se encontró una en la pregunta.

Modelo referenciado

Con un modelo "referenciado" en realidad tiene los miembros reales de Answer dentro de su propia colección. Entonces, en cambio, el esquema se define así:

var QuestionSchema = Schema ({
    title       :String,
    admin       :{type: String, ref: 'User'},
    answers     :[{ type: Schema.Types.ObjectId, ref: 'Answer' }]
});

var AnswerSchema = Schema ({
    _question   :{type: ObjectId, ref: 'Question'},
    employee    :{type: String, ref: 'User'},
    response    :String,
    isAdmin     :{type: Boolean, ref: 'User'}
})

mongoose.model('Answer', answerSchema);
mongoose.model('Question', questionSchema);

N.B La otra referencia está aquí para User como:

    employee    :{type: String, ref: 'User'},
    isAdmin     :{type: Boolean, ref: 'User'}

Estos también son realmente incorrectos, y también deben ser Schema.Type.ObjectId ya que "harán referencia" al campo real _id de User. Pero esto en realidad está fuera del alcance de esta pregunta, así que si aún no comprende eso después de esta lectura, entonces Haga una nueva pregunta para que alguien pueda explicarlo. Con el resto de la respuesta.

Sin embargo, esa es la forma "general" del esquema, y lo importante es el "ref" al 'Anwser' "modelo", que es por el nombre registrado. Opcionalmente, puede usar su campo "_question" en versiones modernas de mangosta con un "virtual", pero por ahora estoy omitiendo "Uso avanzado" y lo mantengo simple con una serie de "referencias" aún en el {{ X3}} modelo.

En este caso, dado que el modelo Answer está realmente en su propia "colección", las operaciones se convierten en "upserts". Donde solo queremos "crear" cuando no hay una respuesta "employee" para la identificación "_question" dada.

También se demuestra con una cadena Promise en lugar:

Answer.update(
  { "_question": req.body.id, "employee": req.body.employee },
  { 
    "$set": {
      "response": req.body.reponse
    },
    "$setOnInsert": {
      "isAdmin": req.body.isAdmin
    }
  },
  { "upsert": true }
).then(resp => {
  if ( resp.hasOwnProperty("upserted") ) {
    return Question.update(
      { "_id": req.body.id, "answers": { "$ne": resp.upserted[0]._id  },
      { "$push": { "answers": resp.upserted[0]._id  } }
    ).exec()
  }
  return;
}).then(resp => {
   // either undefined where it was not an upsert or 
   // the update result from Question where it was
}).catch(err => { 
   // something
})

Esto es en realidad una declaración simple ya que "cuando coincide" queremos cambiar los datos "response" con la carga útil de la solicitud, y realmente solo cuando "upserting" o "crear / insertar" es cuando realmente cambiamos otros datos como "employee" (que siempre está implícito para crear como parte de la expresión de consulta) y "isAdmin" que claramente no debería cambiar con cada actualización solicitamos que luego usemos explícitamente $setOnInsert para que < strong> only escribe esos dos campos en una "creación" real.

En la "Cadena de promesas", realmente miramos para ver si la solicitud de actualización a Answer realmente resultó en un "upsert", y cuando lo haga, queremos agregarlo a la matriz de Question donde no ya existe. De la misma manera que en el ejemplo "incrustado", es mejor mirar para ver si la matriz realmente tiene el elemento antes de modificar con la "actualización". Alternativamente, puede $addToSet aquí y simplemente deje que el la consulta coincide con Question por _id. Para mí, eso es una escritura desperdiciada.


Resumen

Esos son sus diferentes enfoques sobre cómo maneja esto. Cada uno tiene sus propios casos de uso para los cuales puede ver un resumen general de algunas otras respuestas mías en:

No es una lectura "obligatoria", pero puede ayudar a ampliar su conocimiento sobre qué enfoque es mejor para su caso particular.


Ejemplo de trabajo

Cópielos y colóquelos en un directorio y haga un npm install para instalar las dependencias locales. El código se ejecutará y creará las colecciones en la base de datos haciendo las modificaciones.

El registro se activa con mongoose.set(debug,true), por lo que debe mirar la salida de la consola y ver qué hace, junto con las colecciones resultantes donde las respuestas se registrarán a las preguntas relacionadas y se sobrescribirán en lugar de "duplicar" donde eso también la intención

Cambie la cadena de conexión si es necesario. Pero eso es todo lo que debe cambiar en este listado para su propósito. Ambos enfoques descritos en la respuesta se demuestran.

package.json

{
  "name": "colex",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "async": "^2.4.1",
    "mongodb": "^2.2.29",
    "mongoose": "^4.10.7"
  }
}

index.js

const async = require('async'),
    mongoose = require('mongoose'),
    Schema = mongoose.Schema,
    ObjectId = require('mongodb').ObjectID

mongoose.Promise = global.Promise;
mongoose.set('debug',true);
mongoose.connect('mongodb://localhost/coltest');

const userSchema = new Schema({
  username: String,
  isAdmin: { type: Boolean, default: false }
});

const answerSchemaA = new Schema({
  employee: { type: Schema.Types.ObjectId, ref: 'User' },
  response: String,
});

const answerSchemaB = new Schema({
  question: { type: Schema.Types.ObjectId, ref: 'QuestionB' },
  employee: { type: Schema.Types.ObjectId, ref: 'User' },
  response: String,
});

const questionSchemaA = new Schema({
  title: String,
  admin: { type: Schema.Types.ObjectId, ref: 'User' },
  answers: [answerSchemaA]
});

const questionSchemaB = new Schema({
  title: String,
  admin: { type: Schema.Types.ObjectId, ref: 'User' },
  answers: [{ type: Schema.Types.ObjectId, ref: 'AnswerB' }]
});

const User = mongoose.model('User', userSchema);

const AnswerB = mongoose.model('AnswerB', answerSchemaB);

const QuestionA = mongoose.model('QuestionA', questionSchemaA);
const QuestionB = mongoose.model('QuestionB', questionSchemaB);


async.series(
  [
    // Clear data
    (callback) => async.each(mongoose.models,(model,callback) =>
      model.remove({},callback),callback),

    // Create some data
    (callback) =>
      async.each([
        {
          "model": "User",
          "object": {
            "_id": "594a322619ddbd437193c759",
            "name": "Admin",
            "isAdmin": true
          }
        },
        {
          "model": "User",
          "object": {
            "_id":  "594a323919ddbd437193c75a",
            "name": "Bill"
          }
        },
        {
          "model": "User",
          "object": {
            "_id":  "594a327b19ddbd437193c75b",
            "name": "Ted"
          }
        },
        {
          "model": "QuestionA",
          "object": {
            "_id": "594a32f719ddbd437193c75c",
            "admin": "594a322619ddbd437193c759",
            "title": "Question A Model"
          }
        },
        {
          "model": "QuestionB",
          "object": {
            "_id": "594a32f719ddbd437193c75c",
            "admin": "594a322619ddbd437193c759",
            "title": "Question B Model"
          }
        }
      ],(data,callback) => mongoose.model(data.model)
        .create(data.object,callback),
      callback
    ),

    // Submit Answers for Users - Question A
    (callback) =>
      async.eachSeries(
        [
          {
            "_id": "594a32f719ddbd437193c75c",
            "employee": "594a323919ddbd437193c75a",
            "response": "Bills Answer"
          },
          {
            "_id": "594a32f719ddbd437193c75c",
            "employee": "594a327b19ddbd437193c75b",
            "response": "Teds Answer"
          },
          {
            "_id": "594a32f719ddbd437193c75c",
            "employee": "594a323919ddbd437193c75a",
            "response": "Bills Changed Answer"
          }
        ].map(d => ([
          { "updateOne": {
            "filter": {
              "_id": ObjectId(d._id),
              "answers.employee": ObjectId(d.employee)
            },
            "update": {
              "$set": { "answers.$.response": d.response }
            }
          }},
          { "updateOne": {
            "filter": {
              "_id": ObjectId(d._id),
              "answers.employee": { "$ne": ObjectId(d.employee) }
            },
            "update": {
              "$push": {
                "answers": {
                  "employee": ObjectId(d.employee),
                  "response": d.response
                }
              }
            }
          }}
        ])),
        (data,callback) => QuestionA.collection.bulkWrite(data,callback),
        callback
      ),

    // Submit Answers for Users - Question A
    (callback) =>
      async.eachSeries(
        [
          {
            "_id": "594a32f719ddbd437193c75c",
            "employee": "594a323919ddbd437193c75a",
            "response": "Bills Answer"
          },
          {
            "_id": "594a32f719ddbd437193c75c",
            "employee": "594a327b19ddbd437193c75b",
            "response": "Teds Anwser"
          },
          {
            "_id": "594a32f719ddbd437193c75c",
            "employee": "594a327b19ddbd437193c75b",
            "response": "Ted Changed it"
          }
        ],
        (data,callback) => {
          AnswerB.update(
            { "question": data._id, "employee": data.employee },
            { "$set": { "response": data.response } },
            { "upsert": true }
          ).then(resp => {
            console.log(resp);
            if (resp.hasOwnProperty("upserted")) {
              return QuestionB.update(
                { "_id": data._id, "employee": { "$ne": data.employee } },
                { "$push": { "answers": resp.upserted[0]._id } }
              ).exec()
            }
            return;
          }).then(() => callback(null))
          .catch(err => callback(err))
        },
        callback
      )
  ],
  (err) => {
    if (err) throw err;
    mongoose.disconnect();
  }
)
2
Neil Lunn 21 jun. 2017 a las 10:58

Aquí estaba mi trabajo rápido antes de que Neill actualizara su respuesta (usé $pull & $push). Funciona igual que el suyo, pero marcaré su correcto porque creo que es más eficiente.

                Question.update(
                    {_id: req.body.id},
                    {$pull: {answers: { employee: req.body.employee}}},
                    {safe: true, multi:true, upsert: true},
                    function(err, model) {

                    }
                );

                Question.update(
                    {_id: req.body.id},
                    {$push: {answers: {_question: req.body.id, 
                                        employee: req.body.employee, 
                                        response: req.body.response,
                                         isAdmin: req.body.isAdmin}}},
                    {safe: true, upsert: true},
                    function(err, model) {

                    }
                );
0
colin_dev256 23 jun. 2017 a las 07:52