2015-02-10 23 views
0

我有大量的数据,我希望使用GORM加载到数据库中。Grails DuplicateKeyException/NonUniqueObjectException异步承诺内批量加载时

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 
    } 
} 

我故意只对每一行的负载做了顶级服务transactional。实际上在顶层下有很多级别的服务,而不仅仅是所示的单个Aux A服务层。因此,我不希望产生多层事务的开销:我认为我应该只需要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中的最佳实践?为了确保内存中的对象在会话中保持一致,是否需要执行刷新/清除会话的操作?缓存对象时是否需要完成某些操作?

+0

首先,你不应该做这样的重批处理。使用像Spring Batch这样的真正的批处理框架。但是,您是否尝试过为每个任务使用新的休眠会话?这可能有帮助。 – 2015-02-10 19:37:38

+0

新的hibernate会话,意味着这里有一个新的hibernate会话吗? inputFile.eachLine {line,lineNumber - > this.handleLine(line,lineNumber) } – John 2015-02-10 19:42:30

+0

是的,使用这个:http://grails.github.io/grails-doc/latest/ref/Domain%20Classes/withNewSession。 html – 2015-02-10 19:45:06

回答

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 

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

具体而言,从第13章:

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

会话是一个便宜的非线程安全对象,应该使用 一次,然后丢弃:单个请求,对话或单个工作单元。除非需要,否则会话将不会获得JDBC连接或数据源。在使用前它不会消耗任何资源 。

可能会感兴趣的人那是什么我已经看到在随物体的数目该批式负载的性能逐渐降解被解析,甚至伯特贝克威思here建议的性能优化:和解释Ted Naleid here的更多详细信息。

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

当我删除了我的问题的cleanupGorm()方法,并以下列取代它,我得到了一个6倍的性能增加,绝对没有与批量大小增加了加载时间,甚至数百万10S后记录被解析:

// 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() 

虽然我接受的是使用类似SPR ing批可能会更好,它会涉及扔掉大量的代码,否则按预期工作。我将在下次需要这样做时考虑这一点,但同时,希望这可能对需要使用Grails进行大规模批处理的其他人有用,并且发现批处理大小的性能会降低。

约书亚记录:非常感谢你的帮助,非常感谢!