Skip to content

Commit 7ecf10e

Browse files
committed
Fill SelectQueriesInfo to refine the EagerFetching detection
1 parent d8c1b96 commit 7ecf10e

File tree

1 file changed

+72
-20
lines changed

1 file changed

+72
-20
lines changed

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

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
import javax.persistence.EntityManager;
1212
import java.io.Serializable;
13+
import java.util.HashMap;
1314
import java.util.HashSet;
15+
import java.util.Map;
1416
import java.util.Optional;
1517
import java.util.Set;
1618
import java.util.function.Supplier;
@@ -22,10 +24,10 @@ public class HibernateQueryInterceptor extends EmptyInterceptor {
2224
private transient ThreadLocal<Long> threadQueryCount = new ThreadLocal<>();
2325

2426
private transient ThreadLocal<Set<String>> threadPreviouslyLoadedEntities =
25-
ThreadLocal.withInitial(new EmptySetSupplier());
27+
ThreadLocal.withInitial(new EmptySetSupplier<>());
2628

27-
private transient ThreadLocal<Set<String>> threadPrevioulyQueriedProxyMethods =
28-
ThreadLocal.withInitial(new EmptySetSupplier());
29+
private transient ThreadLocal<Map<String, SelectQueriesInfo>> threadSelectQueriesInfoPerProxyMethod =
30+
ThreadLocal.withInitial(new EmptyMapSupplier<>());
2931

3032
private static final Logger LOGGER = LoggerFactory.getLogger(HibernateQueryInterceptor.class);
3133

@@ -45,7 +47,7 @@ public HibernateQueryInterceptor(
4547
*/
4648
private void resetNPlusOneQueryDetectionState() {
4749
threadPreviouslyLoadedEntities.set(new HashSet<>());
48-
threadPrevioulyQueriedProxyMethods.set(new HashSet<>());
50+
threadSelectQueriesInfoPerProxyMethod.set(new HashMap<>());
4951
}
5052

5153
/**
@@ -84,7 +86,7 @@ public Long getQueryCount() {
8486
@Override
8587
public String onPrepareStatement(String sql) {
8688
if (hibernateQueryInterceptorProperties.isnPlusOneDetectionEnabled()) {
87-
detectNPlusOneQueriesOfMissingEntityFieldLazyFetching();
89+
updateSelectQueriesInfoPerProxyMethod(sql);
8890
}
8991
Long count = threadQueryCount.get();
9092
if (count != null) {
@@ -113,7 +115,8 @@ public void afterTransactionCompletion(Transaction tx) {
113115
@Override
114116
public Object getEntity(String entityName, Serializable id) {
115117
if (hibernateQueryInterceptorProperties.isnPlusOneDetectionEnabled()) {
116-
detectNPlusOneQueriesOfMissingQueryEagerFetching(entityName, id);
118+
detectNPlusOneQueriesFromMissingEagerFetchingOnAQuery(entityName, id);
119+
detectNPlusOneQueriesFromClassFieldEagerFetching(entityName);
117120
}
118121

119122
return null;
@@ -122,17 +125,15 @@ public Object getEntity(String entityName, Serializable id) {
122125
/**
123126
* Detect the N+1 queries caused by a missing eager fetching configuration on a query with a lazy loaded field
124127
* <p>
125-
* <p>
126128
* Detection checks:
127129
* - The getEntity was called twice for the couple (entity, id)
128-
* <p>
129130
* - There is an occurrence of hibernate proxy followed by entity class in the stackTraceElements
130131
* Avoid detecting calls to queries like findById and queries with eager fetching on some entity fields
131132
*
132133
* @param entityName Name of the entity
133134
* @param id Id of the entity objecy
134135
*/
135-
private void detectNPlusOneQueriesOfMissingQueryEagerFetching(String entityName, Serializable id) {
136+
private void detectNPlusOneQueriesFromMissingEagerFetchingOnAQuery(String entityName, Serializable id) {
136137
Set<String> previouslyLoadedEntities = threadPreviouslyLoadedEntities.get();
137138

138139
if (!previouslyLoadedEntities.contains(entityName + id)) {
@@ -170,25 +171,70 @@ private void detectNPlusOneQueriesOfMissingQueryEagerFetching(String entityName,
170171
}
171172

172173
/**
173-
* Detect the N+1 queries caused by a missing lazy fetching configuration on an entity field
174+
* Update the select queries info per proxy method to be able to detect potential N+1 queries
175+
* due to Eager Fetching on a field of a class
174176
* <p>
175-
* Detection checks that several queries were generated from the same proxy method
177+
* Checks:
178+
* - Detect queries that would not fit the N+1 queries problem, non select queries, and remove the entry
179+
* - Detect multiple calls to same proxy method and reset the entry to avoid false positive
180+
* - Detect select queries that could be potential N+1 queries and increment the count
176181
*/
177-
private void detectNPlusOneQueriesOfMissingEntityFieldLazyFetching() {
182+
private void updateSelectQueriesInfoPerProxyMethod(String sql) {
178183
Optional<String> optionalProxyMethodName = getProxyMethodName();
179184
if (!optionalProxyMethodName.isPresent()) {
180185
return;
181186
}
182187
String proxyMethodName = optionalProxyMethodName.get();
183188

184-
Set<String> previouslyQueriedProxyMethods = threadPrevioulyQueriedProxyMethods.get();
189+
boolean isSelectQuery = sql.toLowerCase().startsWith("select");
190+
191+
Map<String, SelectQueriesInfo> selectQueriesInfoPerProxyMethod = threadSelectQueriesInfoPerProxyMethod.get();
192+
193+
// The N+1 queries problem is only related to select queries
194+
// So we remove the entry when detecting non select query for the proxy method
195+
if (!isSelectQuery) {
196+
selectQueriesInfoPerProxyMethod.remove(proxyMethodName);
197+
threadSelectQueriesInfoPerProxyMethod.set(selectQueriesInfoPerProxyMethod);
198+
return;
199+
}
200+
201+
SelectQueriesInfo selectQueriesInfo = selectQueriesInfoPerProxyMethod.get(proxyMethodName);
185202

186-
if (!previouslyQueriedProxyMethods.contains(proxyMethodName)) {
187-
previouslyQueriedProxyMethods.add(proxyMethodName);
203+
// Handle several calls to the same proxy method by resetting the SelectQueriesInfo
204+
// when the initial select query is detected
205+
if (selectQueriesInfo == null || selectQueriesInfo.getInitialSelectQuery().equals(sql)) {
206+
selectQueriesInfoPerProxyMethod.put(proxyMethodName, new SelectQueriesInfo(sql));
207+
threadSelectQueriesInfoPerProxyMethod.set(selectQueriesInfoPerProxyMethod);
188208
return;
189209
}
190210

191-
String errorMessage = "N+1 queries detected with eager fetching on the query";
211+
selectQueriesInfoPerProxyMethod.put(proxyMethodName, selectQueriesInfo.incrementSelectQueriesCount());
212+
threadSelectQueriesInfoPerProxyMethod.set(selectQueriesInfoPerProxyMethod);
213+
}
214+
215+
/**
216+
* Detect the N+1 queries caused by a missing lazy fetching configuration on an entity field
217+
* <p>
218+
* Detection checks that several select queries were generated from the same proxy method
219+
*/
220+
private void detectNPlusOneQueriesFromClassFieldEagerFetching(String entityName) {
221+
Optional<String> optionalProxyMethodName = getProxyMethodName();
222+
if (!optionalProxyMethodName.isPresent()) {
223+
return;
224+
}
225+
String proxyMethodName = optionalProxyMethodName.get();
226+
227+
Map<String, SelectQueriesInfo> selectQueriesInfoPerProxyMethod = threadSelectQueriesInfoPerProxyMethod.get();
228+
SelectQueriesInfo selectQueriesInfo = selectQueriesInfoPerProxyMethod.get(proxyMethodName);
229+
if (selectQueriesInfo == null || selectQueriesInfo.getSelectQueriesCount() < 2) {
230+
return;
231+
}
232+
233+
// Reset the count to 1 to log a message once per additional query
234+
selectQueriesInfoPerProxyMethod.put(proxyMethodName, selectQueriesInfo.resetSelectQueriesCount());
235+
threadSelectQueriesInfoPerProxyMethod.set(selectQueriesInfoPerProxyMethod);
236+
237+
String errorMessage = "N+1 queries detected with eager fetching on the entity " + entityName;
192238

193239
// Find origin of the N+1 queries in client package
194240
// by getting oldest occurrence of proxy method in stack elements
@@ -201,8 +247,8 @@ private void detectNPlusOneQueriesOfMissingEntityFieldLazyFetching() {
201247
}
202248
}
203249

204-
errorMessage += "\n Hint: Missing Lazy fetching configuration on a field of one of the entities " +
205-
"fetched in the query\n";
250+
errorMessage += "\n Hint: Missing Lazy fetching configuration on a field of type " + entityName + " of " +
251+
"one of the entities fetched in the query\n";
206252

207253
logDetectedNPlusOneQueries(errorMessage);
208254
}
@@ -248,8 +294,14 @@ private void logDetectedNPlusOneQueries(String errorMessage) {
248294
}
249295
}
250296

251-
class EmptySetSupplier implements Supplier<Set<String>> {
252-
public Set<String> get() {
297+
class EmptySetSupplier<T> implements Supplier<Set<T>> {
298+
public Set<T> get() {
253299
return new HashSet<>();
254300
}
255301
}
302+
303+
class EmptyMapSupplier<T> implements Supplier<Map<String, T>> {
304+
public Map<String, T> get() {
305+
return new HashMap<>();
306+
}
307+
}

0 commit comments

Comments
 (0)