在Grails和MongoDB插件的条件查询中使用allowDiskUse?

时间:2015-04-16 16:32:16

标签: mongodb grails

为了使用Grails(2.5.0)和MongoDB插件(3.0.2)迭代MongoDB(2.6.9)集合中的所有文档,我创建了一个这样的forEach:

class MyObjectService {
    def forEach(Closure func) {
        def criteria = MyObject.createCriteria()
        def ids = criteria.list { projections { id() } }
        ids.each { func(MyObject.get(it)) }
    }
}

然后我这样做:

class AnalysisService{
    def myObjectService

    @Async
    def analyze(){
        MyObject.withStatelessSession {
            myObjectService.forEach { myObject ->
                doSomethingAwesome(myObject)
            }
        }
    }
}

这很有效......直到我点击一个大的集合(> 500K文档),此时抛出了一个CommandFailureException,因为聚合结果的大小大于16MB。

Caused by CommandFailureException: { "serverUsed" : "foo.bar.com:27017" , "errmsg" : "exception: aggregation result exceeds maximum document size (16MB)" , "code" : 16389 , "ok" : 0.0}

在阅读本文时,我认为处理这种情况的一种方法是在MongoDB端运行的聚合函数中使用选项allowDiskUse,以便16MB内存限制不会适用,我可以获得更大的聚合结果。

如何将此选项传递给条件查询?我一直在阅读Grails MongoDB插件的文档和Javadoc,但我似乎无法找到它。是否有另一种方法来处理通用问题(迭代大量域对象的所有成员)?

1 个答案:

答案 0 :(得分:0)

对于MongoDB Grails插件的当前实现,这是不可能的。 https://github.com/grails/grails-data-mapping/blob/master/grails-datastore-gorm-mongodb/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.java#L957

如果查看上面的行,那么您将看到默认选项用于构建AggregationOptions实例,因此没有方法可以提供选项。

但是还有另一种使用Groovy的元类的hackish方法。我们这样做..: - )

在为您的服务编写标准之前,先存储builder()方法的原始方法参考:

MetaMethod originalMethod = AggregationOptions.metaClass.static.getMetaMethod("builder", [] as Class[])

然后,替换构建器方法以提供实现。

AggregationOptions.metaClass.static.builder = { ->
    def builderInstance = new AggregationOptions.Builder()
    builderInstance.allowDiskUse(true)       // solution to your problem
    return builderInstance
}

现在,您的服务方法将使用条件查询进行调用,并且不应导致您收到的聚合错误,因为我们尚未将allowDiskUse属性设置为true。

现在,重新恢复原始方法,使其不会影响任何其他调用(可选)。

AggregationOptions.metaClass.static.addMetaMethod(originalMethod)

希望这有帮助!

除此之外,为什么要在forEach方法中提取所有ID,然后使用get()方法重新获取实例?您正在浪费会影响性能的数据库查询。此外,如果您遵循此操作,则无需进行上述更改。

具有相同的示例:(更新

class MyObjectService {

    void forEach(Closure func) {
        List<MyObject> instanceList = MyObject.createCriteria().list {
            // Your criteria code
            eq("status", "ACTIVE")   // an example
        }

        // Don't do any of this
        // println(instanceList)
        // println(instanceList.size())

        // *** explained below
        instanceList.each { myObjectInstance ->       
            func(myObjectInstance)
        }
    }
}

(我没有添加AnalysisService的代码,因为没有变化)

***此时的重点在于此。因此,无论何时在域类中编写任何条件(没有投影和mongo),执行条件代码后,Grails / gmongo都不会立即从数据库中获取记录,除非您调用某些方法,如toString(),'size( )or dump()`on。

现在,当您在该实例列表中应用each时,您实际上并不会将所有实例加载到内存中,而是在场景后面迭代Mongo Cursor,而在MongoDB中,游标使用批量从数据库中提取记录。非常安全。因此,您可以安全地直接调用每个标准结果,除非您调用任何触发从数据库加载所有记录的方法,否则不会炸毁JVM。

即使在代码中,您也可以确认此行为:https://github.com/grails/grails-data-mapping/blob/master/grails-datastore-gorm-mongodb/src/main/groovy/org/grails/datastore/mapping/mongo/query/MongoQuery.java#L1775

每当你编写没有投影的任何标准时,你将得到一个MongoResultList的实例,并且有一个名为initializeFully()的方法正在toString()和其他方法上调用。但是,您可以看到MongoResultList正在实现迭代器,而迭代器又调用MongoDB游标方法来迭代大型集合,这又是内存安全的。

希望这有帮助!