检测在使用JPA和Hibernate进行测试时的N+1查询问题

huangapple go评论80阅读模式
英文:

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

问题

  1. 实现这一点的最佳方法是什么?
  2. 你是否希望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

  1. What would be the best way of achieving this?
  2. 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&lt;Void&gt;) transactionStatus -&gt; {
			TestObject testObject = new TestObject(1L);
			repository.save(testObject);
			
			return null;
		});
	} catch (TransactionException e) {
		LOGGER.error(&quot;Failure&quot;, 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.

huangapple
  • 本文由 发表于 2020年4月11日 01:59:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/61145966.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定