在Async Promise中批量加载时Grails DuplicateKeyException / NonUniqueObjectException

时间:2015-02-10 19:04:51

标签: grails asynchronous gorm promise batch-processing

我希望使用GORM将大量数据加载到数据库中。

class DbLoadingService {

    static transactional = false    
    // these are used to expedite the batch loading process
    def sessionFactory
    def propertyInstanceMap = org.codehaus.groovy.grails.plugins.DomainClassGrailsPlugin.PROPERTY_INSTANCE_MAP

    // these are example services that will assist in the parsing of the input data    
    def auxLoadingServiceA
    def auxLoadingServiceB

    def handleInputFile(String filename) {
        def inputFile = new File(filename)
        // parse each line and process according to record type
        inputFile.eachLine { line, lineNumber ->
            this.handleLine(line, lineNumber)
        }
    }


    @Transactional
    def handleLine(String line, int lineNumber) {
        // do some further parsing of the line, based on its content
        // example here is based on 1st 2 chars of line
        switch (line[0..1]) {
            case 'AA':
                auxLoadingServiceA.doSomethingWithLine(line)
                break;

            case 'BB':
                auxLoadingServiceB.doSomethingElseWithLine(line)
                break;

            default:
                break;

        }
        if (lineNumber % 100 == 0) cleanUpGorm()
    }

    def cleanUpGorm() {
        def session = sessionFactory.getCurrentSession()
        session.flush()
        session.clear()
        propertyInstanceMap.get().clear()
    }

}

class AuxLoadingServiceA {
    static transactional = false

    doSomethingWithLine(String line) {
        // do something here
    }
}

class AuxLoadingServiceB {
    static transactional = false

    doSomethingElseWithLine(String line) {
        // do something else here
    }
}

我故意只为每一行的负载制作顶级服务事务。在顶层,实际上有很多级别的服务,而不仅仅是单一的Aux A& A;显示B服务层。因此,我不希望有多层事务的开销:我想我应该只需要1。

加载到数据库中的数据模型包含几个具有hasMany / belongsTo关系的域对象。这种与域对象的交互是在子层内完成的,并没有在我的代码中显示,以保持示例的可管理性。

似乎导致问题的域对象看起来类似于:

class Parent {
    static hasMany = [children: Child]
    static mapping = {
        children lazy: false
        cache true
    }
}

class Child {
    String someValue
    // also contains some other sub-objects

    static belongsTo = [parent : Parent]

    static mapping = {
        parent index: 'parent_idx'
        cache true
    }
}

显示的cleanupGorm()方法是必需的,否则服务会在大量行之后完全停止。

当我启动数据库加载时,所有工作都完全符合预期:

// Called from with a service / controller
dbLoadingService.handleInputFile("someFile.txt")

但是,只要我将负载移动到异步进程中,就像这样:

def promise = task {
    dbLoadingService.handleInputFile("someFile.txt")
}

我收到DuplicateKeyException / NonUniqueObjectException:

error details: org.springframework.dao.DuplicateKeyException: A different object with the same identifier value was already associated with the session : [com.example.SampleDomainObject#1]; nested exception is org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [com.example.SampleDomainObject#1]

所以,我的问题是,将大量数据异步加载到Grails DB中的最佳实践是什么?是否需要在刷新/清除会话方面做些什么才能确保内存中的对象在会话中保持一致?缓存对象时是否需要执行某些操作?

1 个答案:

答案 0 :(得分:0)

解决方案是按照@JoshuaMoore的建议进行,并使用新的会话。此外,还有一个域对象的引用,该对象是从一个事务外部引用的,然后在新会话中没有调用merge(),从而导致错误。

def obj = DomainObject.findBySomeProperty('xyz')

// now start new session

obj.someProperty // causes exception
obj = obj.merge()
obj.someProperty // doesn't cause an exception

Joshua的评论促使我深入研究Hibernate的文档(https://docs.jboss.org/hibernate/orm/3.6/reference/en-US/html/transactions.html

具体来说,从第13章开始:

  

SessionFactory是一个昂贵的创建线程安全对象,   旨在由所有应用程序线程共享。它被创建一次,   通常在应用程序启动时,从Configuration实例。

     

Session是一种廉价的非线程安全对象,应该使用它   一次然后丢弃:单个请求,一个会话或一个   单一工作单位。会话不会获得JDBC连接,或者   数据源,除非需要。它不会消耗任何资源   直到使用。

其他人可能感兴趣的是,即使使用Burt Beckwith here建议的性能优化,我也看到批量加载的性能逐渐下降,同时解析的对象数量也在增加:并解释Ted Naleid here进一步详细说明。

因此,使用文档中的提示,性能问题的答案不是尝试将会话用于所有处理 - 而是使用它进行少量处理,然后将其丢弃并创建一个新的之一。

当我在我的问题中删除了cleanupGorm()方法并将其替换为以下内容时,我得到了 6倍的性能提升,批量大小的加载时间绝对没有增加,甚至在解析了数百万条记录之后:

// somewhere in the service method that is doing the batch parse
def currentSession = sessionFactory.openSession()

// start some form of batch parse, perhaps in a loop

    // do work here
    // periodically, perhaps in the %N way shown above
    currentSession.flush()
    currentSession.close()
    currentSession = sessionFactory.openSession()

// end of loop

我需要在跨越服务的事务中包装东西,我做了以下事情:

currentSession = sessionFactory.openSession()
currentSession.beginTransaction()

// start loop
// do work

// when we want to commit
def tx = currentSession?.getTransaction()
if (tx?.isActive()) tx.commit()
currentSession?.close()

// if we're in a loop and we need a new transaction
currentSession = sessionFactory.openSession()
currentSession.beginTransaction()

虽然我接受使用类似Spring Batch的东西可能更好,但它会丢弃大量代码,否则会按预期工作。我将在下次需要这样做时进行调查,但与此同时,希望这可能对需要使用Grails进行大规模批处理的其他人有用,并且发现批量大小的性能会下降

约书亚的注意事项:非常感谢你的帮助,非常感谢!