Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Update ecocode-rules-specifications to 1.4.6

## [unreleased]

- #188 Add Hibernate N+1 query detection for lazy-loaded relationship access inside loops and streams

## [2.1.2] - 2026-05-20
Comment on lines +119 to +123

[unreleased](https://github.com/green-code-initiative/creedengo-java/compare/2.1.2...HEAD)
[2.1.2](https://github.com/green-code-initiative/creedengo-java/compare/2.1.1...2.1.2)
[2.1.1](https://github.com/green-code-initiative/creedengo-java/compare/2.1.0...2.1.1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import java.util.List;

class AvoidHibernateLazyRelationAccessInLoopCheckSample {
Comment on lines +1 to +3

void badForEachLoop(List<Product> products) {
for (Product product : products) {
product.getOrders(); // Noncompliant
}
}

void badForLoop(List<Product> products) {
for (int i = 0; i < products.size(); i++) {
products.get(i).getOrders(); // Noncompliant
}
}

void badStreamForEach(List<Product> products) {
products.forEach(product -> {
product.getOrders(); // Noncompliant
});
}

void badStreamMap(List<Product> products) {
products.stream()
.map(product -> product.getOrders().size()); // Noncompliant
}

void goodSimpleGetter(List<Product> products) {
for (Product product : products) {
product.getName();
}
}

void goodIdGetter(List<Product> products) {
for (Product product : products) {
product.getId();
}
}

class Product {
List<Order> getOrders() {
return null;
}

String getName() {
return "";
}

Long getId() {
return 1L;
}
}

class Order {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.greencodeinitiative.creedengo.java.checks;

import org.sonar.check.Rule;
Comment on lines +1 to +3
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;

import java.util.Arrays;
import java.util.List;

@Rule(key = "CRJVM206")
public class AvoidHibernateLazyRelationAccessInLoopCheck extends IssuableSubscriptionVisitor {

Comment on lines +12 to +14
private static final String MESSAGE =
"Potential Hibernate N+1 query detected: avoid accessing a lazy relationship inside a loop. "
+ "Use JOIN FETCH, EntityGraph, or batch fetching.";

@Override
public List<Tree.Kind> nodesToVisit() {
return Arrays.asList(
Tree.Kind.FOR_EACH_STATEMENT,
Tree.Kind.FOR_STATEMENT,
Tree.Kind.WHILE_STATEMENT,
Tree.Kind.DO_STATEMENT,
Tree.Kind.METHOD_INVOCATION
);
Comment on lines +20 to +27
}

@Override
public void visitNode(Tree tree) {
if (tree.is(Tree.Kind.METHOD_INVOCATION)) {
MethodInvocationTree methodInvocationTree = (MethodInvocationTree) tree;

if (isStreamOperation(methodInvocationTree)) {
checkInsideTree(tree);
}
} else {
checkInsideTree(tree);
}
}

private void checkInsideTree(Tree tree) {
tree.accept(new BaseTreeVisitor() {
@Override
public void visitMethodInvocation(MethodInvocationTree methodInvocationTree) {
if (isPotentialLazyRelationGetter(methodInvocationTree)) {
reportIssue(methodInvocationTree, MESSAGE);
}
super.visitMethodInvocation(methodInvocationTree);
}
});
}

private boolean isStreamOperation(MethodInvocationTree methodInvocationTree) {
String methodName = methodInvocationTree.symbol().name();
return "forEach".equals(methodName)
|| "forEachOrdered".equals(methodName)
|| "map".equals(methodName)
|| "peek".equals(methodName);
}

private boolean isPotentialLazyRelationGetter(MethodInvocationTree methodInvocationTree) {
String methodName = methodInvocationTree.symbol().name();
return methodName.startsWith("get")
&& methodName.length() > 4
&& methodName.endsWith("s");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"GCI78",
"GCI79",
"GCI82",
"GCI94"
"GCI94",
"CRJVM206"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* creedengo - Java language - Provides rules to reduce the environmental footprint of your Java programs
* Copyright © 2024 Green Code Initiative (https://green-code-initiative.org/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.greencodeinitiative.creedengo.java.checks.CRJVM206;

import org.greencodeinitiative.creedengo.java.checks.AvoidHibernateLazyRelationAccessInLoopCheck;
import org.junit.jupiter.api.Test;
import org.sonar.java.checks.verifier.CheckVerifier;

class AvoidHibernateLazyRelationAccessInLoopCheckTest {

@Test
void test() {
CheckVerifier.newVerifier()
.onFile(System.getProperty("testfiles.path") + "/CRJVM206/AvoidHibernateLazyRelationAccessInLoopCheckSample.java")
.withCheck(new AvoidHibernateLazyRelationAccessInLoopCheck())
.verifyIssues();
}
}