Mongo Java条件求和(如果不为空)

3
我正在使用mongo java驱动程序,试图计算所有文档中特定字段不为空或不存在的数量。这是我目前的代码:
String field = "myfieldname";
BasicDBObject notNull = new BasicDBObject(field,BasicDBObject("$ne",null));
List<Object> condition = Arrays.asList(notNull,1,0);
BasicDBObject aggregation = new BasicDBObject("$cond",condition);
// boiler plate code to lookup my database/collection and pass the aggregation to it

这好像不起作用。

编辑: 感谢Rob Moore的帮助。以下是我最终采用的方法,使其起作用。

BasicDBObject ifNull = new BasicDBObject('$ifNull',Arrays.asList('$'+field,null));
BasicDBObject neq = new BasicDBObject('$ne', Arrays.asList(null,ifNull));
return new BasicDBObject('$cond', Arrays.asList(neq,1,0);

你混淆了两个不同的操作。$cond 必须与 $project 管道阶段一起使用。$sum 是 $group 阶段的一部分。这可以通过使用两个阶段来克服,但是......你也不能将 $exists 查询运算符用作 $cond 表达式的一部分,只能使用表达式运算符。"不存在或为空" ($ifNull) 是否是一个足够好的替代品? - Rob Moore
@RobMoore 谢谢。请看我的修改。我已经改成不使用exists而是使用$ne,但我仍然遇到了一些问题,特别是与null相关的问题。 - Jeff Storey
另外,我不认为$ifNull在这里起作用。如果不为空,则返回1,如果为空或不存在,则返回0。 - Jeff Storey
字段值需要以“$”开头。我假设这将被添加到更大的投影中? - Rob Moore
关于 $ifNull:我认为你可以在 $cond 中用 $eq 包装它,以获得正确的逻辑。类似这样:'$cond' : [ { '$eq' : [ { '$ifNull' : [ '$myfieldname', ObjectId('52cb90166c4a281586e13465') ] }, ObjectId('52cb90166c4a281586e13465') ] }, 0, 1 ] - Rob Moore
抱歉,$myfieldname 有个打字错误。让我花几分钟了解一下你对 $ifNull 的方法,并看看它是如何工作的。稍后我会回复你。感谢你的帮助。 - Jeff Storey
1个回答

5

这里有一个应用程序,实现了我认为你需要的逻辑。它使用异步Java驱动程序的辅助类来帮助构建正确的管道。在代码的最后,我们插入了99个文档。33个字段值为空,33个有值,33个没有值。该应用程序总共获得了66个结果,我认为这就是你想要的结果。

/*
 *           Copyright 2013 - Allanbank Consulting, Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package stackoverflow;

import static com.allanbank.mongodb.builder.AggregationGroupField.set;
import static com.allanbank.mongodb.builder.AggregationGroupId.constantId;
import static com.allanbank.mongodb.builder.AggregationProjectFields.include;
import static com.allanbank.mongodb.builder.Find.ALL;
import static com.allanbank.mongodb.builder.expression.Expressions.cond;
import static com.allanbank.mongodb.builder.expression.Expressions.constant;
import static com.allanbank.mongodb.builder.expression.Expressions.eq;
import static com.allanbank.mongodb.builder.expression.Expressions.field;
import static com.allanbank.mongodb.builder.expression.Expressions.ifNull;
import static com.allanbank.mongodb.builder.expression.Expressions.set;

import java.io.IOException;
import java.util.Random;

import com.allanbank.mongodb.MongoClient;
import com.allanbank.mongodb.MongoCollection;
import com.allanbank.mongodb.MongoFactory;
import com.allanbank.mongodb.bson.builder.BuilderFactory;
import com.allanbank.mongodb.bson.builder.DocumentBuilder;
import com.allanbank.mongodb.bson.element.ArrayElement;
import com.allanbank.mongodb.bson.element.ObjectId;
import com.allanbank.mongodb.builder.Aggregate;

/**
 * Count the number of documents that have a particular field.
 * 
 * @see <a
 *      href="https://dev59.com/JnrZa4cB1Zd3GeqP9v_U">StackOverflow
 *      Question</a>
 * 
 * @copyright 2013, Allanbank Consulting, Inc., All Rights Reserved
 */
public class SumIfExists {
    /**
     * A source of no so random values. Use a fixed seed to always get the same
     * values for fields.
     */
    private final static Random random = new Random(123456789L);

    /**
     * The handle to the MongoDB client. We assume MongoDB is running on your
     * machine on the default port of 27017.
     */
    private final static MongoClient client = MongoFactory
            .createClient("mongodb://localhost:27017/");

    /** The collection we will be using. */
    private final static MongoCollection theCollection = client.getDatabase(
            "db").getCollection("collection");

    /**
     * Run the demo.
     * 
     * @param args
     *            Command line arguments. Ignored.
     * @throws IOException
     *             On a failure closing the MongoCLient.
     */
    public static void main(String[] args) throws IOException {
        // Build the aggregation document/command.
        Aggregate.Builder builder = Aggregate.builder();

        // From the StackOverflow Question.
        String fieldName = "myfieldname";

        // A token ObjectId to use in comparisons for the null field.
        ObjectId nullToken = new ObjectId();

        builder.project(
                include("a", "b", "c", "etc"),
                set("myfieldnameExists",
                        cond(eq(ifNull(field(fieldName), constant(nullToken)),
                                constant(nullToken)), constant(0), constant(1))));
        builder.group(constantId("a"), set("count").sum("myfieldnameExists"));

        System.out.println(new ArrayElement("$pipeline", builder.build()
                .getPipeline()));

        // Insert some documents to test with.
        theCollection.delete(ALL);
        for (int i = 0; i < 99; ++i) {
            DocumentBuilder doc = BuilderFactory.start();
            if (i % 3 == 0) {
                doc.addNull(fieldName);
            }
            else if (i % 3 == 1) {
                doc.add(fieldName, random.nextDouble());
            }
            // else if (i % 3 == 2) -- Field does not exist.

            doc.add("a", random.nextBoolean());
            doc.add("b", random.nextInt());
            doc.add("c", random.nextLong());
            doc.add("etc", random.nextLong());

            theCollection.insert(doc);
        }

        // Run the aggregation.
        System.out.println(theCollection.aggregate(builder));
    }
}

代码的输出结果是:
'$pipeline' : [
  {
    '$project' : {
      a : 1,
      b : 1,
      c : 1,
      etc : 1,
      myfieldnameExists : {
        '$cond' : [
          {
            '$eq' : [
              {
                '$ifNull' : [
                  '$myfieldname', 
                  ObjectId('52cb94836c4a28185433c4d3')
                ]
              }, 
              ObjectId('52cb94836c4a28185433c4d3')
            ]
          }, 
          0, 
          1
        ]
      }
    }
  }, 
  {
    '$group' : {
      '_id' : 'a',
      count : { '$sum' : '$myfieldnameExists' }
    }
  }
]
[{
  '_id' : 'a',
  count : 66
}]

我离正确答案很近吗? 罗布。

感谢,Rob。我仍在我的端上工作,并将其构建到我们现有的流程中-比我希望的要花费更多时间。在这种情况下,我只想查找字段存在且不为空的记录,所以我正在寻找33,但我认为重点仍然相似。 - Jeff Storey
所以我发布了我的编辑过的代码,它可以工作。基本上我所做的就是检查 $ifNull,如果为真,则返回 null。将该值用作等于比较中的一侧与 null 进行比较。我仍然有点困惑,为什么我不能只使用“不等于 null”的条件语句,而必须首先在其中加入 ifNull 检查(如果这不清楚,请告诉我)。 - Jeff Storey
我怀疑问题在于 BSON 中 null 是一种不同的元素类型,因此 null 不等于不存在。 $ifNull 函数(作为副作用)将不存在和 null 值规范化为相同的值。 - Rob Moore
我曾考虑过这个问题,但是从mongo shell中,db.myrecords.find( { fieldname: null })将会查找一个字段不存在的记录。但是你可能是对的,某些地方可能无法正确转换到驱动程序中。无论如何,感谢你的帮助。 - Jeff Storey

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