1010
1111import javax .persistence .EntityManager ;
1212import java .io .Serializable ;
13+ import java .util .HashMap ;
1314import java .util .HashSet ;
15+ import java .util .Map ;
1416import java .util .Optional ;
1517import java .util .Set ;
1618import 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