diff --git a/ecocode-rules-specifications/src/main/rules/EC475/EC475.json b/ecocode-rules-specifications/src/main/rules/EC475/EC475.json new file mode 100644 index 000000000..e58205d8f --- /dev/null +++ b/ecocode-rules-specifications/src/main/rules/EC475/EC475.json @@ -0,0 +1,11 @@ +{ + "title": "Optimize Database Queries", + "type": "CODE_SMELL", + "status": "ready", + "tags": [ + "performance", + "eco-design", + "ecocode" + ], + "defaultSeverity": "Minor" +} \ No newline at end of file diff --git a/ecocode-rules-specifications/src/main/rules/EC475/java/EC475.asciidoc b/ecocode-rules-specifications/src/main/rules/EC475/java/EC475.asciidoc new file mode 100644 index 000000000..7f3dae7c2 --- /dev/null +++ b/ecocode-rules-specifications/src/main/rules/EC475/java/EC475.asciidoc @@ -0,0 +1,36 @@ +Databases are typically essential application components. As many queries are used to retrieve and store data, they end up having a significant impact on the solution's resource use when executed frequently. + +With this in mind, it is important to pay attention for these queries and ensure, at least for the most expensive ones that they are well optimized. + +The most common optimization tips are: + +- Use less data and limit it to the bare minimum. For example, the LIMIT clause limits the number of result rows in relational databases. When possible, using the 'LIMIT' clause reduces the amount of transferred data. Performance gains will be even more significant if records contain a large number of voluminous fields. + +- Only use necessary fields in the tables or documents in order to avoid needlessly transferring data that will not be used and to avoid using database server and application server resources to manipulate them. + +- Add indexes on fields used as keys in your model. Adding them can completely change queries performance. Be careful: adding indexes makes writing longer as it must be updated for each added, modified or deleted document. This must be done if there are more reads than writes or if reads are particularly expensive. + +- Use database management system tools to analyze queries in order to identify improvement areas, such as EXPLAIN for RDBMS. + +- Cache the most expensive queries results as well as data that changes little or never (reference data). + +- Optionally, modifying data models to be able to access information more easily without joins (denormalization) + + +## Noncompliant Code Example + +```java + String sql = "SELECT user FROM myTable"; +``` + +## Compliant Code Example + +```java + String sql = "SELECT user FROM myTable LIMIT 50"; +``` + +### Index creation example + +```sql +CREATE INDEX idx_people_lastname_firstname ON people(lastname, firstname) +``` \ No newline at end of file diff --git a/java-plugin/src/main/java/fr/greencodeinitiative/java/JavaCheckRegistrar.java b/java-plugin/src/main/java/fr/greencodeinitiative/java/JavaCheckRegistrar.java index b66e81cfa..9161dcdc6 100644 --- a/java-plugin/src/main/java/fr/greencodeinitiative/java/JavaCheckRegistrar.java +++ b/java-plugin/src/main/java/fr/greencodeinitiative/java/JavaCheckRegistrar.java @@ -39,6 +39,7 @@ import fr.greencodeinitiative.java.checks.OptimizeReadFileExceptions; import fr.greencodeinitiative.java.checks.UnnecessarilyAssignValuesToVariables; import fr.greencodeinitiative.java.checks.UseCorrectForLoop; +import fr.greencodeinitiative.java.checks.OptimizeDatabaseQueries; import org.sonar.plugins.java.api.CheckRegistrar; import org.sonar.plugins.java.api.JavaCheck; import org.sonarsource.api.sonarlint.SonarLintSide; @@ -70,7 +71,8 @@ public class JavaCheckRegistrar implements CheckRegistrar { AvoidUsingGlobalVariablesCheck.class, AvoidSetConstantInBatchUpdate.class, FreeResourcesOfAutoCloseableInterface.class, - AvoidMultipleIfElseStatement.class + AvoidMultipleIfElseStatement.class, + OptimizeDatabaseQueries.class ); /** diff --git a/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeDatabaseQueries.java b/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeDatabaseQueries.java new file mode 100644 index 000000000..4c397c05c --- /dev/null +++ b/java-plugin/src/main/java/fr/greencodeinitiative/java/checks/OptimizeDatabaseQueries.java @@ -0,0 +1,38 @@ +package fr.greencodeinitiative.java.checks; + +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.LiteralTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.Tree.Kind; + +import java.util.List; +import java.util.function.Predicate; + +import static java.util.Collections.singletonList; +import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static java.util.regex.Pattern.compile; + +@Rule(key = "EC475") +public class OptimizeDatabaseQueries extends IssuableSubscriptionVisitor{ + public static final String MESSAGE_RULE = "Optimize Database Queries (Clause LIMIT)"; + private static final Predicate LIMIT_REGEXP = + compile("limit", CASE_INSENSITIVE).asPredicate(); + private static final Predicate SELECT_REGEXP = + compile("select", CASE_INSENSITIVE).asPredicate(); + private static final Predicate FROM_REGEXP = + compile("from", CASE_INSENSITIVE).asPredicate(); + + @Override + public List nodesToVisit() { + return singletonList(Kind.STRING_LITERAL); + } + + @Override + public void visitNode(Tree tree) { + String value = ((LiteralTree) tree).value(); + if (SELECT_REGEXP.test(value) && FROM_REGEXP.test(value) && !LIMIT_REGEXP.test(value)) { + reportIssue(tree, MESSAGE_RULE); + } + } +} \ No newline at end of file diff --git a/java-plugin/src/test/files/OptimizeDatabaseQueries.java b/java-plugin/src/test/files/OptimizeDatabaseQueries.java new file mode 100644 index 000000000..7e2c46b30 --- /dev/null +++ b/java-plugin/src/test/files/OptimizeDatabaseQueries.java @@ -0,0 +1,21 @@ +class OptimizeDatabaseQueries { + + OptimizeDatabaseQueries(OptimizeDatabaseQueries mc) { + } + + public void literalSQLrequest() { + dummyCall("SELECT user FROM myTable"); // Noncompliant + dummyCall("SELECT user FROM myTable LIMIT 50"); // Compliant + } + + @Query("select t from Todo t where t.status != 'COMPLETED'") // Noncompliant + @Query("select t from Todo t where t.status != 'COMPLETED' LIMIT 25") // Compliant + + private void callQuery() { + String sql1 = "SELECT user FROM myTable"; // Noncompliant + String sql2 = "SELECT user FROM myTable LIMIT 50"; // Compliant + } + + private void dummyCall(String request) { + } +} \ No newline at end of file diff --git a/java-plugin/src/test/java/fr/greencodeinitiative/java/JavaCheckRegistrarTest.java b/java-plugin/src/test/java/fr/greencodeinitiative/java/JavaCheckRegistrarTest.java index db1383ae5..43c9d0a7c 100644 --- a/java-plugin/src/test/java/fr/greencodeinitiative/java/JavaCheckRegistrarTest.java +++ b/java-plugin/src/test/java/fr/greencodeinitiative/java/JavaCheckRegistrarTest.java @@ -31,7 +31,7 @@ void checkNumberRules() { final JavaCheckRegistrar registrar = new JavaCheckRegistrar(); registrar.register(context); - assertThat(context.checkClasses()).hasSize(19); + assertThat(context.checkClasses()).hasSize(20); assertThat(context.testCheckClasses()).isEmpty(); } diff --git a/java-plugin/src/test/java/fr/greencodeinitiative/java/JavaRulesDefinitionTest.java b/java-plugin/src/test/java/fr/greencodeinitiative/java/JavaRulesDefinitionTest.java index 2c6551436..13ecd6563 100644 --- a/java-plugin/src/test/java/fr/greencodeinitiative/java/JavaRulesDefinitionTest.java +++ b/java-plugin/src/test/java/fr/greencodeinitiative/java/JavaRulesDefinitionTest.java @@ -78,12 +78,12 @@ void testMetadata() { assertThat(repository.name()).isEqualTo("ecoCode"); assertThat(repository.language()).isEqualTo("java"); assertThat(repository.key()).isEqualTo("ecocode-java"); - assertThat(repository.rules()).hasSize(19); + assertThat(repository.rules()).hasSize(20); } @Test void testRegistredRules() { - assertThat(repository.rules()).hasSize(19); + assertThat(repository.rules()).hasSize(20); } @Test diff --git a/java-plugin/src/test/java/fr/greencodeinitiative/java/checks/OptimizeDatabaseQueriesTest.java b/java-plugin/src/test/java/fr/greencodeinitiative/java/checks/OptimizeDatabaseQueriesTest.java new file mode 100644 index 000000000..5e9dabeb5 --- /dev/null +++ b/java-plugin/src/test/java/fr/greencodeinitiative/java/checks/OptimizeDatabaseQueriesTest.java @@ -0,0 +1,16 @@ +package fr.greencodeinitiative.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +class OptimizeDatabaseQueriesTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile("src/test/files/OptimizeDatabaseQueries.java") + .withCheck(new OptimizeDatabaseQueries()) + .verifyIssues(); + } + +} \ No newline at end of file