MongoosJS: лучший подход для производного/вычисляемого значения

Я создаю приложение для ставок на американский футбол для своей семьи. Вот мои схемы:

const GameSchema = new mongoose.Schema({
    home: {
        type: String,
        required: true
    },
    opponent: {
       type: String,
       required: true
    },
    homeScore: Number,
    opponentScore: Number,
    week:{ 
       type: Number,
       required: true
    },
    winner: String,
    userPicks: [
        {
            user: {
                type: mongoose.Schema.Types.ObjectId,
                ref: 'User'
            },
            choosenTeam: String
        }
    ]
}); 

const UserSchema = new mongoose.Schema({
    name: String
});

Мне нужно иметь возможность рассчитать еженедельный счет каждого пользователя (т.е. количество футбольных игр, которые они правильно прогнозируют каждую неделю) и их совокупный счет (т.е. количество игр, которые каждый пользователь правильно прогнозирует в целом)

Я все еще новичок в MongoDB и Mongoose, поэтому не знаю, как решить эту проблему. Поскольку документ игры никогда не превысит 200 записей, я думаю, что оба счета должны быть получены или рассчитаны на основе данных, хранящихся в базе данных.

Вот возможные решения, о которых я думал до сих пор:

  • Сделайте обе оценки виртуальными атрибутами, не знаю, как это будет работать для нескольких пользователей.
  • Сохраните атрибуты документа, но используйте ПО промежуточного слоя для пересчета очков, когда результаты игр недели сохраняются в базе данных.
  • Используйте статический метод для расчета баллов.

Любой совет будет принят во внимание.


person Sierra Gregg    schedule 13.09.2016    source источник
comment
привет, это часть машинного обучения? как вы хотите рассчитать домашний счет? это увеличение по сравнению с предыдущим счетом?   -  person vdj4y    schedule 13.09.2016


Ответы (1)


Вы можете использовать структуру агрегации для расчета агрегатов. Это более быстрая альтернатива Map/Reduce для обычных операций агрегирования. В MongoDB конвейер состоит из ряда специальных операторов, применяемых к коллекции для обработки записей данных и возврата вычисленных результатов. Операции агрегирования группируют значения из нескольких документов вместе и могут выполнять различные операции над сгруппированными данными, чтобы вернуть один результат. Дополнительные сведения см. в документации.

Рассмотрите возможность запуска следующего конвейера, чтобы получить желаемый результат:

var pipeline = [
    { "$unwind": "$userPicks" },
    {
        "$group": {
            "_id": {
                "week": "$week",
                "user": "$userPicks.user"
            },
            "weeklyScore": {
                "$sum": {
                    "$cond": [
                        { "$eq": ["$userPicks.chosenTeam", "$winner"] },
                        1, 0
                    ]
                }
            }           
        }
    },
    {
        "$group": {
            "_id": "$_id.user",
            "weeklyScores": {
                "$push": {
                    "week": "$_id.week",
                    "score": "$weeklyScore"
                }
            },
            "totalScores": { "$sum": "$weeklyScore" }
        }
    }
];

Game.aggregate(pipeline, function(err, results){
    User.populate(results, { "path": "_id" }, function(err, results) {
        if (err) throw err;
        console.log(JSON.stringify(results, undefined, 4));
    });
})

В приведенном выше конвейере первым шагом является $unwind оператор

{ "$unwind": "$userPicks" }

что очень удобно, когда данные хранятся в виде массива. Когда оператор раскрутки применяется к полю данных списка, он создает новую запись для каждого элемента поля данных списка, к которому применяется раскрутка. Это в основном сглаживает данные.

Это необходимая операция для следующего этапа конвейера, $group шаг, на котором вы группируете сведенные документы по полям week и "userPicks.user"

    {
        "$group": {
            "_id": {
                "week": "$week",
                "user": "$userPicks.user"
            },
            "weeklyScore": {
                "$sum": {
                    "$cond": [
                        { "$eq": ["$userPicks.chosenTeam", "$winner"] },
                        1, 0
                    ]
                }
            }           
        }
    }

Конвейер $group оператор похож на предложение SQL GROUP BY. В SQL вы не можете использовать GROUP BY, если вы не используете какую-либо из функций агрегирования. Точно так же вы должны использовать функцию агрегации и в MongoDB. Подробнее о функциях агрегации можно прочитать здесь.

В этой $group операции, логика расчета недельного счета каждого пользователя (т. е. количества футбольных игр, которые они правильно прогнозируют каждую неделю) выполняется с помощью тернарного оператора $cond, который принимает логическое условие в качестве первого аргумента (если), а затем возвращает второй аргумент, если оценка верна (тогда) или третий аргумент, где ложь (иначе). Это делает истинные/ложные возвраты в 1 и 0 для подачи на $sum соответственно:

"$cond": [
    { "$eq": ["$userPicks.chosenTeam", "$winner"] },
    1, 0
]

Таким образом, если в обрабатываемом документе поле "$userPicks.chosenTeam" совпадает с полем "$winner", $cond передает значение 1 сумме, иначе суммирует нулевое значение.

Второй групповой конвейер:

{
    "$group": {
        "_id": "$user",
        "weeklyScores": {
            "$push": {
                "week": "$_id.week",
                "score": "$weeklyScore"
            }
        },
        "totalScores": { "$sum": "$weeklyScore" }
    }
}

берет документы из предыдущего конвейера и группирует их дальше по полю user и вычисляет другой агрегат, то есть общий балл, используя $sum. В рамках того же конвейера вы можете агрегировать список еженедельных оценок, используя $push, который возвращает массив значений выражений для каждой группы.

Здесь следует отметить одну вещь: при выполнении конвейера MongoDB передает операторы друг другу. «Канала» здесь имеет значение Linux: вывод оператора становится вводом следующего оператора. Результатом работы каждого оператора является новая коллекция документов. Таким образом, Mongo выполняет указанный выше конвейер следующим образом:

collection | $unwind | $group | $group => result

Теперь, когда вы запускаете конвейер агрегации в Mongoose, результаты будут иметь ключ _id, который является идентификатором пользователя, и вам нужно заполнить результаты в этом поле, т.е. Mongoose выполнит «присоединение» к коллекции пользователей и вернет документы с схема пользователя в результатах.


В качестве примечания: чтобы помочь понять конвейер или отладить его, если вы получите неожиданные результаты, запустите агрегацию только с первым оператором конвейера. Например, запустите агрегацию в оболочке mongo как:

db.games.aggregate([
    { "$unwind": "$userPicks" }
])

Проверьте результат, чтобы увидеть, правильно ли деконструирован массив userPicks. Если это дает ожидаемый результат, добавьте следующее:

db.games.aggregate([
    { "$unwind": "$userPicks" },
    {
        "$group": {
            "_id": {
                "week": "$week",
                "user": "$userPicks.user"
            },
            "weeklyScore": {
                "$sum": {
                    "$cond": [
                        { "$eq": ["$userPicks.chosenTeam", "$winner"] },
                        1, 0
                    ]
                }
            }           
        }
    }
])

Повторяйте шаги, пока не дойдете до последнего шага конвейера.

person chridam    schedule 13.09.2016
comment
Я все еще новичок в MongoDB/Mongoose. Не могли бы вы объяснить, что именно это делает? - person Sierra Gregg; 13.09.2016
comment
@SierraGregg Я добавил некоторые пояснения - person chridam; 13.09.2016
comment
Благодарю вас! Использование агрегации намного чище, чем любая из моих других идей. - person Sierra Gregg; 13.09.2016