Skip to content

Commit 4908ba0

Browse files
authored
Merge pull request #9 from yannbriancon/bug-fix-false-positive-findById-queries
Bug fix false positive find by id queries
2 parents a241796 + 0e65620 commit 4908ba0

File tree

12 files changed

+110
-73
lines changed

12 files changed

+110
-73
lines changed

README.md

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
* [Prerequisites](#prerequisites)
4242
* [Installation](#installation)
4343
* [Usage](#usage)
44-
* [N+1 Query Detection](#n1-query-detection)
44+
* [N+1 Queries Detection](#n1-queries-detection)
4545
* [Detection](#detection)
4646
* [Configuration](#configuration)
4747
* [Query Count](#query-count)
@@ -53,7 +53,9 @@
5353
<!-- ABOUT THE PROJECT -->
5454
## About The Project
5555

56-
While investigating the performance problems in my SpringBoot application, I discovered the infamous N+1 queries problem (more details on this problem [here](https://medium.com/@mansoor_ali/hibernate-n-1-queries-problem-8a926b69f618)) that was killing the performance of my services.
56+
While investigating the performance problems in my SpringBoot application, I discovered the infamous N+1 queries
57+
problem that was killing the performance of my services.
58+
Check the article [Eliminate Spring Hibernate N+1 Queries](https://medium.com/sipios/eliminate-spring-hibernate-n-plus-1-queries-f0bcf6a83de2?source=friends_link&sk=5ba0f2493af1d8496a46d5f116effa96) for more details.
5759

5860
After managing to fix this problem, I had to find a way to detect it and raise the alarm to avoid any developer to introduce new ones.
5961

@@ -84,29 +86,29 @@ Add the dependency to your project inside your `pom.xml` file
8486
<dependency>
8587
<groupId>com.yannbriancon</groupId>
8688
<artifactId>spring-hibernate-query-utils</artifactId>
87-
<version>1.0.1</version>
89+
<version>1.0.2</version>
8890
</dependency>
8991
```
9092

9193

9294
<!-- USAGE -->
9395
## Usage
9496

95-
### N+1 Query Detection
97+
### N+1 Queries Detection
9698

9799
#### Detection
98100

99-
The N+1 query detection is enabled by default so no configuration is needed.
101+
The N+1 queries detection is enabled by default so no configuration is needed.
100102

101-
Each time a N+1 query is detected in a transaction, a log of level error will be sent.
103+
Each time N+1 queries are detected in a transaction, a log of level error will be sent.
102104

103-
Here is an example catching the error log:
105+
Here is an example catching the N+1 queries detection error log:
104106

105107
```java
106108
@RunWith(MockitoJUnitRunner.class)
107109
@SpringBootTest
108110
@Transactional
109-
class NPlusOneQueryLoggingTest {
111+
class NPlusOneQueriesLoggingTest {
110112

111113
@Autowired
112114
private MessageRepository messageRepository;
@@ -124,42 +126,47 @@ class NPlusOneQueryLoggingTest {
124126
}
125127

126128
@Test
127-
void nPlusOneQueryDetection_isLoggingWhenDetectingNPlusOneQuery() {
129+
void nPlusOneQueriesDetection_isLoggingWhenDetectingNPlusOneQueries() {
128130
// Fetch the 2 messages without the authors
129131
List<Message> messages = messageRepository.findAll();
130-
131-
// Trigger N+1 query
132+
133+
// The getters trigger N+1 queries
132134
List<String> names = messages.stream()
133135
.map(message -> message.getAuthor().getName())
134136
.collect(Collectors.toList());
135-
137+
136138
verify(mockedAppender, times(2)).doAppend(loggingEventCaptor.capture());
137-
139+
138140
LoggingEvent loggingEvent = loggingEventCaptor.getAllValues().get(0);
139-
assertThat("N+1 query detected for entity: com.yannbriancon.utils.entity.User")
140-
.isEqualTo(loggingEvent.getMessage());
141+
assertThat(loggingEvent.getMessage())
142+
.isEqualTo("N+1 queries detected on a getter of the entity com.yannbriancon.utils.entity.User\n" +
143+
" at com.yannbriancon.interceptor.NPlusOneQueriesLoggingTest." +
144+
"lambda$nPlusOneQueriesDetection_isLoggingWhenDetectingNPlusOneQueries$0" +
145+
"(NPlusOneQueriesLoggingTest.java:56)\n" +
146+
" Hint: Missing Eager fetching configuration on the query that fetches the object of type" +
147+
" com.yannbriancon.utils.entity.User\n");
141148
assertThat(Level.ERROR).isEqualTo(loggingEvent.getLevel());
142149
}
143150
}
144151
```
145152

146153
#### Configuration
147154

148-
By default the detection of a N+1 query logs an error to avoid breaking your code.
155+
By default the detection of N+1 queries logs an error to avoid breaking your code.
149156

150-
However, my advise is to override the default error level to throw exceptions for your test profile.
157+
However, my advice is to override the default error level to throw exceptions for your test profile.
151158

152159
Now you will easily detect which tests are failing and be able to flag them and set the error level to error logs only on
153160
those tests while you are fixing them.
154161

155-
To do this, you can configure the error level when a N+1 query is detected using the property `hibernate.query.interceptor.error-level`.
162+
To do this, you can configure the error level when N+1 queries is detected using the property `hibernate.query.interceptor.error-level`.
156163

157164
4 levels are available to handle the detection of N+1 queries:
158165

159166
* **INFO**: Log a message of level info
160167
* **WARN**: Log a message of level warn
161168
* **ERROR** (default): Log a message of level error
162-
* **EXCEPTION**: Throw a NPlusOneQueryException
169+
* **EXCEPTION**: Throw a NPlusOneQueriesException
163170

164171
Here are two examples on how to use it globally or for a specific test:
165172

@@ -172,7 +179,7 @@ hibernate.query.interceptor.error-level=INFO
172179
```java
173180
@SpringBootTest("hibernate.query.interceptor.error-level=INFO")
174181
@Transactional
175-
class NPlusOneQueryLoggingTest {
182+
class NPlusOneQueriesLoggingTest {
176183
...
177184
}
178185
```
@@ -202,7 +209,7 @@ public class NotificationResourceIntTest {
202209
private HibernateQueryInterceptor hibernateQueryInterceptor;
203210

204211
@Test
205-
public void saveFile_isOk() throws Exception {
212+
public void getNotification_isOk() throws Exception {
206213
// Initialize the query to 0 and allow the counting
207214
hibernateQueryInterceptor.startQueryCount();
208215

@@ -211,8 +218,8 @@ public class NotificationResourceIntTest {
211218
.andExpect(status().isOk())
212219
.andReturn();
213220

214-
// Get the query count for this thread and check that it is equal to the number of query you expect, let's say 4.
215-
// The count is checked and we detect potential n+1 queries.
221+
// Get the query count for this thread and check that it is equal to the number of query you expect,
222+
// Let's say 4 for the example.
216223
Assertions.assertThat(hibernateQueryInterceptor.getQueryCount()).isEqualTo(4);
217224
}
218225
}

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<groupId>com.yannbriancon</groupId>
77
<artifactId>spring-hibernate-query-utils</artifactId>
8-
<version>1.0.1</version>
8+
<version>1.0.2</version>
99
<packaging>jar</packaging>
1010

1111
<name>spring-hibernate-query-utils</name>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.yannbriancon.exception;
2+
3+
import org.hibernate.CallbackException;
4+
5+
/**
6+
* Exception triggered when detecting N+1 queries
7+
*/
8+
public class NPlusOneQueriesException extends CallbackException {
9+
public NPlusOneQueriesException(String message, Exception cause) {
10+
super(message, cause);
11+
}
12+
13+
public NPlusOneQueriesException(String message) {
14+
super(message);
15+
}
16+
}

src/main/java/com/yannbriancon/exception/NPlusOneQueryException.java

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/main/java/com/yannbriancon/interceptor/HibernateQueryInterceptor.java

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.yannbriancon.interceptor;
22

3-
import com.yannbriancon.exception.NPlusOneQueryException;
3+
import com.yannbriancon.exception.NPlusOneQueriesException;
44
import org.hibernate.EmptyInterceptor;
55
import org.hibernate.Transaction;
66
import org.hibernate.proxy.HibernateProxy;
@@ -11,6 +11,7 @@
1111

1212
import java.io.Serializable;
1313
import java.util.HashSet;
14+
import java.util.Optional;
1415
import java.util.Set;
1516
import java.util.function.Supplier;
1617

@@ -62,7 +63,7 @@ public String onPrepareStatement(String sql) {
6263

6364
/**
6465
* Reset previously loaded entities after the end of a transaction to avoid triggering
65-
* N+1 query exceptions because of loading same instance in two different transactions
66+
* N+1 queries exceptions because of loading same instance in two different transactions
6667
*
6768
* @param tx Transaction having been completed
6869
*/
@@ -87,7 +88,8 @@ public Object getEntity(String entityName, Serializable id) {
8788
if (previouslyLoadedEntities.contains(entityName + id)) {
8889
previouslyLoadedEntities.remove(entityName + id);
8990
threadPreviouslyLoadedEntities.set(previouslyLoadedEntities);
90-
logDetectedNPlusOneQuery(entityName);
91+
Optional<String> errorMessage = detectNPlusOneQueries(entityName);
92+
errorMessage.ifPresent(this::logDetectedNPlusOneQueries);
9193
}
9294

9395
previouslyLoadedEntities.add(entityName + id);
@@ -97,12 +99,35 @@ public Object getEntity(String entityName, Serializable id) {
9799
}
98100

99101
/**
100-
* Log the detected N+1 query or throw an exception depending on the configured error level
102+
* Detect the N+1 queries by checking if the stack trace contains an Hibernate Proxy on the Entity
101103
*
102-
* @param entityName Name of the entity on which the N+1 query has been detected
104+
* @param entityName Name of the entity
105+
* @return If N+1 queries detected, return error message corresponding to the N+1 queries
103106
*/
104-
private void logDetectedNPlusOneQuery(String entityName) {
105-
String errorMessage = "N+1 query detected for entity: " + entityName;
107+
private Optional<String> detectNPlusOneQueries(String entityName) {
108+
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
109+
110+
for (int i = 0; i + 1 < stackTraceElements.length; i++) {
111+
StackTraceElement stackTraceElement = stackTraceElements[i];
112+
113+
if (stackTraceElement.getClassName().indexOf(entityName) == 0) {
114+
String errorMessage = "N+1 queries detected on a getter of the entity " + entityName +
115+
"\n at " + stackTraceElements[i + 1].toString() +
116+
"\n Hint: Missing Eager fetching configuration on the query that fetches the object of " +
117+
"type " + entityName + "\n";
118+
return Optional.of(errorMessage);
119+
}
120+
}
121+
122+
return Optional.empty();
123+
}
124+
125+
/**
126+
* Log the detected N+1 queries error message or throw an exception depending on the configured error level
127+
*
128+
* @param errorMessage Error message for the N+1 queries detected
129+
*/
130+
private void logDetectedNPlusOneQueries(String errorMessage) {
106131
switch (hibernateQueryInterceptorProperties.getErrorLevel()) {
107132
case INFO:
108133
LOGGER.info(errorMessage);
@@ -114,13 +139,13 @@ private void logDetectedNPlusOneQuery(String entityName) {
114139
LOGGER.error(errorMessage);
115140
break;
116141
default:
117-
throw new NPlusOneQueryException(errorMessage);
142+
throw new NPlusOneQueriesException(errorMessage, new Exception(new Throwable()));
118143
}
119144
}
120145
}
121146

122147
class EmptySetSupplier implements Supplier<Set<String>> {
123-
public Set<String> get(){
148+
public Set<String> get() {
124149
return new HashSet<>();
125150
}
126151
}

src/main/java/com/yannbriancon/interceptor/HibernateQueryInterceptorProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ enum ErrorLevel {
1414
}
1515

1616
/**
17-
* Error level for the N+1 query detection.
17+
* Error level for the N+1 queries detection.
1818
*/
1919
private ErrorLevel errorLevel = ErrorLevel.ERROR;
2020

src/test/java/com/yannbriancon/interceptor/NPlusOneQueryExceptionTest.java renamed to src/test/java/com/yannbriancon/interceptor/NPlusOneQueriesExceptionTest.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.yannbriancon.interceptor;
22

3-
import com.yannbriancon.exception.NPlusOneQueryException;
3+
import com.yannbriancon.exception.NPlusOneQueriesException;
44
import com.yannbriancon.utils.entity.Message;
55
import com.yannbriancon.utils.repository.MessageRepository;
66
import org.junit.jupiter.api.Test;
@@ -18,34 +18,39 @@
1818
@RunWith(SpringRunner.class)
1919
@SpringBootTest("hibernate.query.interceptor.error-level=EXCEPTION")
2020
@Transactional
21-
class NPlusOneQueryExceptionTest {
21+
class NPlusOneQueriesExceptionTest {
2222

2323
@Autowired
2424
private MessageRepository messageRepository;
2525

2626
@Test
27-
void nPlusOneQueryDetection_throwCallbackExceptionWhenFetchingWithoutEntityGraph() {
27+
void nPlusOneQueriesDetection_throwCallbackExceptionWhenNPlusOneQueries() {
2828
// Fetch the 2 messages without the authors
2929
List<Message> messages = messageRepository.findAll();
3030

3131
try {
32-
// Trigger N+1 query
32+
// Trigger N+1 queries
3333
List<String> names = messages.stream()
3434
.map(message -> message.getAuthor().getName())
3535
.collect(Collectors.toList());
3636
assert false;
37-
} catch (NPlusOneQueryException exception) {
37+
} catch (NPlusOneQueriesException exception) {
3838
assertThat(exception.getMessage())
39-
.isEqualTo("N+1 query detected for entity: com.yannbriancon.utils.entity.User");
39+
.isEqualTo("N+1 queries detected on a getter of the entity com.yannbriancon.utils.entity.User\n" +
40+
" at com.yannbriancon.interceptor.NPlusOneQueriesExceptionTest" +
41+
".lambda$nPlusOneQueriesDetection_throwCallbackExceptionWhenNPlusOneQueries$0" +
42+
"(NPlusOneQueriesExceptionTest.java:34)\n" +
43+
" Hint: Missing Eager fetching configuration on the query " +
44+
"that fetches the object of type com.yannbriancon.utils.entity.User\n");
4045
}
4146
}
4247

4348
@Test
44-
void nPlusOneQueryDetection_isOkWhenFetchingWithEntityGraph() {
49+
void nPlusOneQueriesDetection_isNotThrowingExceptionWhenNoNPlusOneQueries() {
4550
// Fetch the 2 messages with the authors
4651
List<Message> messages = messageRepository.getAllBy();
4752

48-
// Do not trigger N+1 query
53+
// Do not trigger N+1 queries
4954
List<String> names = messages.stream()
5055
.map(message -> message.getAuthor().getName())
5156
.collect(Collectors.toList());

src/test/java/com/yannbriancon/interceptor/NPlusOneQueryLoggingTest.java renamed to src/test/java/com/yannbriancon/interceptor/NPlusOneQueriesLoggingTest.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
@RunWith(MockitoJUnitRunner.class)
3030
@SpringBootTest
3131
@Transactional
32-
class NPlusOneQueryLoggingTest {
32+
class NPlusOneQueriesLoggingTest {
3333

3434
@Autowired
3535
private MessageRepository messageRepository;
@@ -47,32 +47,32 @@ public void setup() {
4747
}
4848

4949
@Test
50-
void nPlusOneQueryDetection_isLoggingWhenDetectingNPlusOneQuery() {
50+
void nPlusOneQueriesDetection_isLoggingWhenDetectingNPlusOneQueries() {
5151
// Fetch the 2 messages without the authors
5252
List<Message> messages = messageRepository.findAll();
5353

54-
// Trigger N+1 query
54+
// The getters trigger N+1 queries
5555
List<String> names = messages.stream()
5656
.map(message -> message.getAuthor().getName())
5757
.collect(Collectors.toList());
5858

5959
verify(mockedAppender, times(2)).doAppend(loggingEventCaptor.capture());
6060

6161
LoggingEvent loggingEvent = loggingEventCaptor.getAllValues().get(0);
62-
assertThat("N+1 query detected for entity: com.yannbriancon.utils.entity.User")
63-
.isEqualTo(loggingEvent.getMessage());
62+
assertThat(loggingEvent.getMessage())
63+
.isEqualTo("N+1 queries detected on a getter of the entity com.yannbriancon.utils.entity.User\n" +
64+
" at com.yannbriancon.interceptor.NPlusOneQueriesLoggingTest." +
65+
"lambda$nPlusOneQueriesDetection_isLoggingWhenDetectingNPlusOneQueries$0" +
66+
"(NPlusOneQueriesLoggingTest.java:56)\n" +
67+
" Hint: Missing Eager fetching configuration on the query that fetches the object of type" +
68+
" com.yannbriancon.utils.entity.User\n");
6469
assertThat(Level.ERROR).isEqualTo(loggingEvent.getLevel());
6570
}
6671

6772
@Test
68-
void nPlusOneQueryDetection_isNotLoggingWhenNotDetectingNPlusOneQuery() {
69-
// Fetch the 2 messages with the authors
70-
List<Message> messages = messageRepository.getAllBy();
71-
72-
// Do not trigger N+1 query
73-
List<String> names = messages.stream()
74-
.map(message -> message.getAuthor().getName())
75-
.collect(Collectors.toList());
73+
void nPlusOneQueriesDetection_isNotLoggingWhenNoNPlusOneQueries() {
74+
// Fetch the messages and does not trigger N+1 queries
75+
messageRepository.findById(1L);
7676

7777
verify(mockedAppender, times(0)).doAppend(any());
7878
}

src/test/java/com/yannbriancon/interceptor/QueryCountTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ void queryCount_isOkWhenCallingRepository() {
3333
messageRepository.findAll();
3434

3535
assertThat(hibernateQueryInterceptor.getQueryCount()).isEqualTo(1);
36-
3736
}
3837

3938
@Test

0 commit comments

Comments
 (0)