Вычисляемые поля группировки в MongoDB

Для этого примера из документации MongoDB, как мне написать запрос, используя MongoTemplate?

db.sales.aggregate(
   [
      {
        $group : {
           _id : { month: { $month: "$date" }, day: { $dayOfMonth: "$date" }, year: { $year: "$date" } },
           totalPrice: { $sum: { $multiply: [ "$price", "$quantity" ] } },
           averageQuantity: { $avg: "$quantity" },
           count: { $sum: 1 }
        }
      }
   ]
)

Или вообще как сгруппировать по вычисляемому полю?


person Navin Viswanath    schedule 22.08.2014    source источник


Ответы (2)


На самом деле вы можете сначала сделать что-то подобное с "проектом", но мне немного нелогично требовать этап $project перед рукой:

    Aggregation agg = newAggregation(
        project("quantity")
            .andExpression("dayOfMonth(date)").as("day")
            .andExpression("month(date)").as("month")
            .andExpression("year(date)").as("year")
            .andExpression("price * quantity").as("totalAmount"),
        group(fields().and("day").and("month").and("year"))
            .avg("quantity").as("averavgeQuantity")
            .sum("totalAmount").as("totalAmount")
            .count().as("count")
    );

Как я уже сказал, это нелогично, поскольку вы должны просто объявить все это на этапе $group, но помощники, похоже, не работают таким образом. Сериализация получается немного забавной (оборачивает аргументы оператора даты массивами), но, похоже, она работает. Но все же это два этапа конвейера, а не один.

В чем проблема с этим? Что ж, разделяя этапы на этапы, часть «проекта» заставляет обрабатывать все документы в конвейере, чтобы получить вычисляемые поля, что означает, что он проходит через все, прежде чем перейти к групповому этапу.

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

Таким образом, кажется, что единственный существующий способ правильно построить это - создать объект конвейера самостоятельно:

    ApplicationContext ctx =
            new AnnotationConfigApplicationContext(SpringMongoConfig.class);
    MongoOperations mongoOperation = (MongoOperations) ctx.getBean("mongoTemplate");

    BasicDBList pipeline = new BasicDBList();
    String[] multiplier = { "$price", "$quantity" };

    pipeline.add(
        new BasicDBObject("$group",
            new BasicDBObject("_id",
                new BasicDBObject("month", new BasicDBObject("$month", "$date"))
                    .append("day", new BasicDBObject("$dayOfMonth", "$date"))
                    .append("year", new BasicDBObject("$year", "$date"))
            )
            .append("totalPrice", new BasicDBObject(
                "$sum", new BasicDBObject(
                    "$multiply", multiplier
                )
            ))
            .append("averageQuantity", new BasicDBObject("$avg", "$quantity"))
            .append("count",new BasicDBObject("$sum",1))
        )
    );

    BasicDBObject aggregation = new BasicDBObject("aggregate","collection")
        .append("pipeline",pipeline);

    System.out.println(aggregation);

    CommandResult commandResult = mongoOperation.executeCommand(aggregation);

Или, если все это кажется вам кратким, вы всегда можете работать с источником JSON и анализировать его. Но, конечно, это должен быть действительный JSON:

    String json = "[" +
        "{ \"$group\": { "+
            "\"_id\": { " +
                "\"month\": { \"$month\": \"$date\" }, " +
                "\"day\": { \"$dayOfMonth\":\"$date\" }, " +
                "\"year\": { \"$year\": \"$date\" } " +
            "}, " +
            "\"totalPrice\": { \"$sum\": { \"$multiply\": [ \"$price\", \"$quantity\" ] } }, " +
            "\"averageQuantity\": { \"$avg\": \"$quantity\" }, " +
            "\"count\": { \"$sum\": 1 } " +
        "}}" +
    "]";

    BasicDBList pipeline = (BasicDBList)com.mongodb.util.JSON.parse(json);
person Neil Lunn    schedule 22.08.2014
comment
Спасибо, Нил, что нашли время ответить на этот вопрос. На самом деле мне больше нравится ваше первое решение. Возможно, это связано с тем, что я больше знаком с реляционными базами данных, а не с конвейерной структурой. - person Navin Viswanath; 22.08.2014
comment
@NavinViswanath Делать это на отдельных этапах плохо. Вы создаете дополнительную передачу данных, которая занимает больше времени. В три раза больше на моем тестовом образце. Я расширил это в ответе, так как это полезно понять. - person Neil Lunn; 22.08.2014
comment
Я понимаю. Еще раз спасибо, Нил. Для запроса, который я пишу, который очень похож на этот, у меня есть «совпадение» перед «проектом». Я предполагаю, что это еще один этап, хотя сопоставление должно значительно отфильтровать документы, поступающие на стадию проекта. - person Navin Viswanath; 22.08.2014

Другой альтернативой является использование пользовательского класса операции агрегации, определенного следующим образом:

public class CustomAggregationOperation implements AggregationOperation {
    private DBObject operation;

    public CustomAggregationOperation (DBObject operation) {
        this.operation = operation;
    }

    @Override
    public DBObject toDBObject(AggregationOperationContext context) {
        return context.getMappedObject(operation);
    }
}

Затем реализуйте его в конвейере следующим образом:

Aggregation aggregation = newAggregation(
    new CustomAggregationOperation(
        new BasicDBObject(
            "$group",
            new BasicDBObject("_id",
                new BasicDBObject("day", new BasicDBObject("$dayOfMonth", "$date" ))
                .append("month", new BasicDBObject("$month", "$date"))
                .append("year", new BasicDBObject("$year", "$date"))
            )
            .append("totalPrice", new BasicDBObject(
                "$sum", new BasicDBObject(
                    "$multiply", Arrays.asList("$price","$quantity")
                )
            ))
            .append("averageQuantity", new BasicDBObject("$avg", "$quantity"))
            .append("count",new BasicDBObject("$sum",1))
        )
    )
)

Таким образом, это в основном просто интерфейс, который совместим с тем, который используется существующими помощниками конвейера, но вместо использования других вспомогательных методов он напрямую принимает определение DBObject для возврата при построении конвейера.

person Blakes Seven    schedule 16.03.2016