英文:
Detecting N+1 query issues during testing with JPA and Hibernate
问题
我们正在使用Hypersistence Utils,这是一个很棒的工具,但是我们在Session缓存方面遇到了一个挑战,正如在另一个问题中提到的;根据这里的回答无法禁用。
因此,下面的测试失败了,因为findOne
方法从会话缓存中获取对象:
@Test
public void validateQueries() {
// when (scenario definition)
TestObject testObject = new TestObject(1L);
repository.save(testObject);
SQLStatementCountValidator.reset();
repository.findOne(1L);
SQLStatementCountValidator.assertSelectCount(1);
}
有一个解决方法是在每次调用SQLStatementCountValidator.reset()
时调用entityManager.clear()
。
现在,这个解决方法是可行的,但容易出错,因为现在我们必须将EntityManager注入为测试的依赖,并且在保存表示场景的所有对象之后记得调用entityManager.clear()
。
问题
- 实现这一点的最佳方法是什么?
- 你是否希望SQLStatementCountValidator也清除entityManager?
这里您可以查看日志语句(最后一个)
09:59.956 [main] [TRACE] o.h.e.i.AbstractSaveEventListener - Transient instance of: TestObject
09:59.957 [main] [TRACE] o.h.e.i.DefaultPersistEventListener - Saving transient instance
09:59.962 [main] [TRACE] o.h.e.i.AbstractSaveEventListener - Saving [TestObject#<null>]
Hibernate:
insert
into
test_object
(id, creation_time, "update_time", "name")
values
(null, ?, ?, ?)
10:00.005 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Flushing session
10:00.005 [main] [DEBUG] o.h.e.i.AbstractFlushingEventListener - Processing flush-time cascades
10:00.007 [main] [DEBUG] o.h.e.i.AbstractFlushingEventListener - Dirty checking collections
10:00.007 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Flushing entities and processing referenced collections
10:00.011 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Processing unreferenced collections
10:00.011 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Scheduling collection removes/(re)creates/updates
10:00.011 [main] [DEBUG] o.h.e.i.AbstractFlushingEventListener - Flushed: 0 insertions, 0 updates, 0 deletions to 1 objects
10:00.011 [main] [DEBUG] o.h.e.i.AbstractFlushingEventListener - Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
10:00.015 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Executing flush
10:00.015 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Post flush
10:02.780 [main] [TRACE] o.h.e.i.DefaultLoadEventListener - Loading entity: [TestObject#1]
10:08.439 [main] [TRACE] o.h.e.i.DefaultLoadEventListener - Attempting to resolve: [TestObject#1]
10:08.439 [main] [TRACE] o.h.e.i.DefaultLoadEventListener - Resolved object in session cache: [TestObject#1]
com.vladmihalcea.sql.exception.SQLSelectCountMismatchException: 预期1个语句,但实际记录为0个!
这是解决方法代码的样子:
@Test
public void validateQueries() {
// when (scenario definition)
TestObject testObject = new TestObject(1L);
repository.save(testObject);
entityManager.clear();
SQLStatementCountValidator.reset();
repository.findOne(1L);
SQLStatementCountValidator.assertSelectCount(1);
}
英文:
We're using Hypersistence Utils, which is a great tool, but we're facing a challenge regarding Session cache, as mentioned in another question; it can't be disabled.
So, the following test fails because findOne
fetches the object from the session cache:
@Test
public void validateQueries() {
// when (scenario definition)
TestObject testObject = new TestObject(1L);
repository.save(testObject);
SQLStatementCountValidator.reset();
repository.findOne(1L);
SQLStatementCountValidator.assertSelectCount(1);
}
There's a workaround calling entityManager.clear()
every time SQLStatementCountValidator.reset()
is called.
Now, the workaround is fine, but error-prone because now we have to inject EntityManager as a dependency of our tests and remember to call entityManager.clear()
after saving all the objects that represent our scenario.
Questions
- What would be the best way of achieving this?
- Would you expect SQLStatementCountValidator to also clear the entityManager?
Here you can check the log statements (the last one)
09:59.956 [main] [TRACE] o.h.e.i.AbstractSaveEventListener - Transient instance of: TestObject
09:59.957 [main] [TRACE] o.h.e.i.DefaultPersistEventListener - Saving transient instance
09:59.962 [main] [TRACE] o.h.e.i.AbstractSaveEventListener - Saving [TestObject#<null>]
Hibernate:
insert
into
test_object
(id, creation_time, "update_time", "name")
values
(null, ?, ?, ?)
10:00.005 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Flushing session
10:00.005 [main] [DEBUG] o.h.e.i.AbstractFlushingEventListener - Processing flush-time cascades
10:00.007 [main] [DEBUG] o.h.e.i.AbstractFlushingEventListener - Dirty checking collections
10:00.007 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Flushing entities and processing referenced collections
10:00.011 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Processing unreferenced collections
10:00.011 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Scheduling collection removes/(re)creates/updates
10:00.011 [main] [DEBUG] o.h.e.i.AbstractFlushingEventListener - Flushed: 0 insertions, 0 updates, 0 deletions to 1 objects
10:00.011 [main] [DEBUG] o.h.e.i.AbstractFlushingEventListener - Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
10:00.015 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Executing flush
10:00.015 [main] [TRACE] o.h.e.i.AbstractFlushingEventListener - Post flush
10:02.780 [main] [TRACE] o.h.e.i.DefaultLoadEventListener - Loading entity: [TestObject#1]
10:08.439 [main] [TRACE] o.h.e.i.DefaultLoadEventListener - Attempting to resolve: [TestObject#1]
10:08.439 [main] [TRACE] o.h.e.i.DefaultLoadEventListener - Resolved object in session cache: [TestObject#1]
com.vladmihalcea.sql.exception.SQLSelectCountMismatchException: Expected 1 statements but recorded 0 instead!
This is how the workaround code looks like:
@Test
public void validateQueries() {
// when (scenario definition)
TestObject testObject = new TestObject(1L);
repository.save(testObject);
entityManager.clear();
SQLStatementCountValidator.reset();
repository.findOne(1L);
SQLStatementCountValidator.assertSelectCount(1);
}
答案1
得分: 1
交易处理
每个测试都应该管理事务。因此,您应该删除您在类级别添加的 @Transactional
注解。
然后,您注入一个 TransactionTemplate
bean:
@Autowired
private TransactionTemplate transactionTemplate;
然后,在一个事务中保存实体:
@Test
public void validateQueries() {
try {
transactionTemplate.execute((TransactionCallback<Void>) transactionStatus -> {
TestObject testObject = new TestObject(1L);
repository.save(testObject);
return null;
});
} catch (TransactionException e) {
LOGGER.error("Failure", e);
}
SQLStatementCountValidator.reset();
repository.findOne(1L);
SQLStatementCountValidator.assertSelectCount(1);
}
您可以将事务处理逻辑提取到一个基类方法中,以简化异常处理。
英文:
Transaction handling
Each test should manage transactions. So, you should remove the @Transactional
annotation you added at the class level.
So, you inject a TransactionTemplate
bean:
@Autowired
private TransactionTemplate transactionTemplate;
And, then you save the entity in one transaction:
@Test
public void validateQueries() {
try {
transactionTemplate.execute((TransactionCallback<Void>) transactionStatus -> {
TestObject testObject = new TestObject(1L);
repository.save(testObject);
return null;
});
} catch (TransactionException e) {
LOGGER.error("Failure", e);
}
SQLStatementCountValidator.reset();
repository.findOne(1L);
SQLStatementCountValidator.assertSelectCount(1);
}
You can extract the transaction handling logic in a base class method to simplify the exception handling.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论