我希望使用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中的最佳实践是什么?是否需要在刷新/清除会话方面做些什么才能确保内存中的对象在会话中保持一致?缓存对象时是否需要执行某些操作?
答案 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进行大规模批处理的其他人有用,并且发现批量大小的性能会下降
约书亚的注意事项:非常感谢你的帮助,非常感谢!