{"id":15294741,"url":"https://github.com/adgadev/jplusone","last_synced_at":"2025-04-13T14:54:21.476Z","repository":{"id":38411813,"uuid":"254864396","full_name":"adgadev/jplusone","owner":"adgadev","description":"Tool for automatic detection and asserting \"N+1 SELECT problem\" occurences in JPA based Spring Boot Java applications and finding origin of JPA issued SQL statements in general","archived":false,"fork":false,"pushed_at":"2024-02-21T00:06:31.000Z","size":1470,"stargazers_count":109,"open_issues_count":1,"forks_count":6,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-04-13T14:54:13.121Z","etag":null,"topics":["hibernate","instrumentation","java","jdbc","jpa","jvm","lazy-loading","microservice","n-plus-1","optimization","orm","performance-analysis","performance-testing","spring-boot","spring-data-jpa","sql","tracing"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adgadev.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-04-11T12:36:08.000Z","updated_at":"2024-07-08T23:07:54.000Z","dependencies_parsed_at":"2022-08-09T03:30:41.938Z","dependency_job_id":null,"html_url":"https://github.com/adgadev/jplusone","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adgadev%2Fjplusone","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adgadev%2Fjplusone/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adgadev%2Fjplusone/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adgadev%2Fjplusone/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adgadev","download_url":"https://codeload.github.com/adgadev/jplusone/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248732512,"owners_count":21152851,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hibernate","instrumentation","java","jdbc","jpa","jvm","lazy-loading","microservice","n-plus-1","optimization","orm","performance-analysis","performance-testing","spring-boot","spring-data-jpa","sql","tracing"],"created_at":"2024-09-30T17:06:33.150Z","updated_at":"2025-04-13T14:54:21.448Z","avatar_url":"https://github.com/adgadev.png","language":"Java","readme":":jdk-icon: https://img.shields.io/badge/java-9+-4c7e9f.svg\n:jdk-link: https://www.oracle.com/technetwork/java/javase/downloads\n\n:maven-central-icon: https://img.shields.io/maven-central/v/com.adgadev.jplusone/jplusone-modules\n:maven-central-link: https://search.maven.org/artifact/com.adgadev.jplusone/jplusone-modules\n\n:apache-license-icon: https://img.shields.io/badge/License-Apache%202.0-blue.svg\n:apache-license-link: http://www.apache.org/licenses/LICENSE-2.0.txt\n\n:action-status-icon: https://github.com/adgadev/jplusone/workflows/Build%20and%20Test/badge.svg\n:action-status-link: https://github.com/adgadev/jplusone/actions\n\n:maintainability-icon: https://api.codeclimate.com/v1/badges/d3cfc1cc05d724ea52b4/maintainability\n:maintainability-link: https://codeclimate.com/github/adgadev/jplusone/maintainability\n\n:codacy-icon: https://app.codacy.com/project/badge/Grade/ccb1fea2aa554aceb691cb32ed270c14\n:codacy-link: https://www.codacy.com/gh/adgadev/jplusone/dashboard?utm_source=github.com\u0026utm_medium=referral\u0026utm_content=adgadev/jplusone\u0026utm_campaign=Badge_Grade\n\n:codecov-icon: https://codecov.io/gh/adgadev/jplusone/branch/master/graph/badge.svg?token=BPXXOVXP8D\n:codecov-link: https://codecov.io/gh/adgadev/jplusone\n\n:fossa-icon: https://app.fossa.com/api/projects/git%2Bgithub.com%2Fadgadev%2Fjplusone.svg?type=shield\n:fossa-link: https://app.fossa.com/projects/git%2Bgithub.com%2Fadgadev%2Fjplusone?ref=badge_shield\n\n:snyk-icon: https://snyk.io/test/github/adgadev/jplusone/badge.svg\n:snyk-link: https://snyk.io/test/github/adgadev/jplusone/\n\n:fossa-large-icon: https://app.fossa.com/api/projects/git%2Bgithub.com%2Fadgadev%2Fjplusone.svg?type=large\n:fossa-large-link: https://app.fossa.com/projects/git%2Bgithub.com%2Fadgadev%2Fjplusone?ref=badge_large\n\n:assertion-api-gif: https://github.com/adgadev/jplusone/blob/master/fluent-api.gif\n\n:release_version: 2.0.0\n\n= JPlusOne\n\nimage:{jdk-icon}[JDK, link={jdk-link}]\nimage:{apache-license-icon}[Apache License 2, link={apache-license-link}]\nimage:{maven-central-icon}[Maven Central Repository, link={maven-central-link}]\nimage:{action-status-icon}[Action Status, link={action-status-link}]\nimage:{maintainability-icon}[Maintainability, link={maintainability-link}]\nimage:{codacy-icon}[Codacy code quality, link={codacy-link}]\nimage:{codecov-icon}[Code coverage, link={codecov-link}]\nimage:{fossa-icon}[FOSSA Status, link={fossa-link}]\nimage:{snyk-icon}[Known Vulnerabilities, link={snyk-link}]\n\n\n\n*JPlusOne* is a Java library for correlating SQL statements with executions of JPA operations (explicit calls, lazy-loading, flush on commit)\nwhich triggered them and places in source code of your application which were involved in it.\n\nBased on collected data JPlusOne can report complete tree of JPA persistence related activity or just situations when lazy loading is occurring, potentially leading to N+1 SELECT problem.\nMoreover JPlusOne provides API which allows to write tests checking how effectively, from performance point of view, your application is using JPA (i.e. assert amount of lazy loading operations )\n\n== Prerequisites\nJPlusOne can be applied only to projects which meets following requirements:\n\n* JPlusOne v1.x (1.1.1 is the latest)\n** Java 9 or later\n** Spring Boot 2.x\n** JPA 2.x (Hibernate as implementation provider required)\n** Slf4j\n* JPlusOne v2.x\n** Java 17 or later\n** Spring Boot 3.x\n** JPA 3.x (Hibernate 6 or higher)\n** Slf4j\n\n== Quickstart\nAdd the following Maven dependency to your project:\n[source,xml,subs=\"verbatim,attributes\"]\n----\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.adgadev.jplusone\u003c/groupId\u003e\n    \u003cartifactId\u003ejplusone-core\u003c/artifactId\u003e\n    \u003cversion\u003e{release_version}\u003c/version\u003e\n    \u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n----\n\nThe jplusone logger must be configured to debug level in order to log JPA operations / SQL statements reports.\nIt can be accomplised by adding following lines to your `application.yml`\n[source,yaml]\n----\nlogging.level:\n    com.adgadev.jplusone: DEBUG\n----\n\nNow run any integration test in your project which directly on indirectly utilizes JPA persistence,\n(i.e. by calling a business service which loads some JPA entity via EntityManager).\nFor each such test a report of JPA operations / SQL statements, like the one below, will be generated and written to logs\n\n----\n2020-09-14 19:41:45.138 DEBUG 14913 --- [           main] c.a.j.core.report.ReportGenerator        :\n    ROOT\n        com.adgadev.jplusone.test.domain.bookshop.BookshopControllerTest.shouldGetBookDetailsLazily(BookshopControllerTest.java:65)\n        com.adgadev.jplusone.test.domain.bookshop.BookshopController.getSampleBookUsingLazyLoading(BookshopController.java:31)\n        com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading [PROXY]\n            SESSION BOUNDARY\n                OPERATION [EXPLICIT]\n                    com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading(BookshopService.java:34)\n                    com.adgadev.jplusone.test.domain.bookshop.BookRepository.findById [PROXY]\n                        STATEMENT [READ]\n                            select [...] from\n                                book book0_\n                            where\n                                book0_.id=1\n                OPERATION [IMPLICIT]\n                    com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading(BookshopService.java:35)\n                    com.adgadev.jplusone.test.domain.bookshop.Author.getName [PROXY]\n                    com.adgadev.jplusone.test.domain.bookshop.Author [FETCHING ENTITY]\n                        STATEMENT [READ]\n                            select [...] from\n                                author author0_\n                                left outer join genre genre1_ on author0_.genre_id=genre1_.id\n                            where\n                                author0_.id=1\n                OPERATION [IMPLICIT]\n                    com.adgadev.jplusone.test.domain.bookshop.BookshopService.getSampleBookDetailsUsingLazyLoading(BookshopService.java:36)\n                    com.adgadev.jplusone.test.domain.bookshop.Author.countWrittenBooks(Author.java:53)\n                    com.adgadev.jplusone.test.domain.bookshop.Author.books [FETCHING COLLECTION]\n                        STATEMENT [READ]\n                            select [...] from\n                                book books0_\n                            where\n                                books0_.author_id=1\n----\n\nThere is also option to generate such report to separate file or stdout or both file and log.\n\nSample project showing `jplusone-core` in action is available https://github.com/adgadev/jplusone/tree/master/jplusone-tests/jplusone-tests-main[here]. It's a simple spring boot project with some JPA domain modeled. Just run any test there to see a report.\n\nThe article showing how JPlusOne can simplify finding origin and context of JPA issued SQL statements and step-by-step guide how to use it is available\nhttps://adgadev.com/finding-origin-and-context-of-jpa-issued-sql-statemets/[here]\n\n== Assertion API\n\nJPlusOne provides Assertion API which can be used to write tests checking various aspects of JPA operations / SQL statements, i.e.\nif lazy loading has not started occurring in given test scenario or amount of SELECT queries is not larger than some amount.\n\nSuch tests may be very useful in situation when you have optimized lazy loading processes in your business operations and you want to enforce\nthat further development changes in your application, often done by other developers, won't degrade the performance accidentally\ni.e. by introducing some additional lazy loading operations.\n\n=== How to use assertion API\nEnsure current JPlusOne https://github.com/adgadev/jplusone#configuration[Configuration] matches your application.\n\nAdd following Maven dependency:\n\n[source,xml,subs=\"verbatim,attributes\"]\n----\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.adgadev.jplusone\u003c/groupId\u003e\n    \u003cartifactId\u003ejplusone-assert\u003c/artifactId\u003e\n    \u003cversion\u003e{release_version}\u003c/version\u003e\n    \u003cscope\u003etest\u003c/scope\u003e\n\u003c/dependency\u003e\n----\nAdd spring boot tests which defines `JPlusOneAssertionRule`, executes one of the business operation in your application and asserts results of it against the defined rule.\n\n[source,java]\n----\n@SpringBootTest\nclass LazyLoadingTest {\n\n    @Autowired\n    private JPlusOneAssertionContext assertionContext;\n\n    @Autowired\n    private SampleService sampleService;\n\n    @Test\n    public void shouldBusinessCheckOperationAgainstJPlusOneAssertionRule() {\n        JPlusOneAssertionRule rule = JPlusOneAssertionRule\n                .within().lastSession()\n                .shouldBe().noImplicitOperations().exceptAnyOf(exclusions -\u003e exclusions\n                        .loadingEntity(Author.class).times(atMost(2))\n                        .loadingCollection(Author.class, \"books\")\n                );\n\n        // trigger business operation which you wish to be asserted against the rule,\n        // i.e. via directly calling a service or via sending request to your API controller\n        sampleService.executeBusinessOperation();\n\n        rule.check(assertionContext);\n    }\n}\n----\n\nThere is a fluent, self-descriptive API for building assertion rules, just start typing `JPlusOneAssertionRule.within()`\nand your IDE will guide you through the process of defining it:\n\nimage:{assertion-api-gif}[Fluent API for assertions]\n\nSample spring boot test showing jplusone assertion in action is available https://github.com/adgadev/jplusone/tree/master/jplusone-assert/src/test/java/com/adgadev/jplusone/asserts/api/JPlusOneAssertionIntegrationTest.java[here].\n\nMore examples presenting building various assertion rules can be found https://github.com/adgadev/jplusone/tree/master/jplusone-assert/src/test/java/com/adgadev/jplusone/asserts/api/JPlusOneAssertionRuleTest.java[here].\n\n\n== Features\n* Shows general activity at JPA and SQL level related with some business scenarios, not necessarily related with lazy loading.\n* Shows tree of application method calls and SQL statements (discarding all non-relevant data) associated with some business operation (i.e. REST controller endpoint call) in a handy way.\n* Allows to easily correlate JPA operations with resulting SQL statements.\n* Shows SQL statements with their parameters in simplified and pretty-printed form.\n* Uses Spring Boot Auto Configuration to seamlessly integrate with your application.\n* Provides https://github.com/adgadev/jplusone#assertion-api[Assertion API] which can be used to write tests checking various aspects of JPA operations / SQL statements, i.e.\nif lazy loading has not started occurring in given test scenario or amount of SELECT queries is not larger than some amount\n\nMore about N+1 SELECT problem you can find in link:https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping/39696775[this] thread at StackOverflow.\n\n\n== Overview\nJPlusOne tool is able to generate report showing all kind of JPA operations / SQL statements, but in order to generate such report you need\nto have Spring Boot based integration tests of your application (`@SpringBootTest`), which covers scenarios you are interested in\n(i.e. integration test of some endpoint of your application).\n\nIt's possible to use JPlusOne not in test but in production code, but this approach may add some additional overhead and may not be stable in some cases.\n\n=== How it works\nIn order to collect data JPlusOne intercepts all operations invoked on EntityManager / EntityManagerFactory and all SQL statements invoked on DataSource and, by wrapping those beans in proxies\n\nOne report is generated per each JPA EntityManager instance (Hibernate session). A report is written to log just after EntityManager is closed.\nIt's worth to mention that behaviour determining when EntityManager is closed (session is closed) can be altered by:\n\n* using `@Transactional` on integration test class or test case method - it extends scope of SUT's session / transaction so that it span across testcase method, session is effectively closed when test method finishes\n* enabling property `spring.jpa.open-in-view` - session closes not when a method of a service annotated with `@Transactional` is finished, but when controller which invoked such service method is finished\nUsually Spring uses separate session per transaction strategy, but\n\n\n=== Types of operations\nJPlusOne uses following terms to categorize operations:\n\n* *Explicit operation* - explicit invocation of some API utilizing Java Persistence API (JPA) which in result triggers some kind of SQL statement,\ni.e. SpringDataJPA repository or EntityManager or QueryDsl\n* *Commit operation* - JPA transaction commit resulting in session flush\n* *Implicit operation* - All kind of situations where SQL statements were triggered without some explicit call on JPA based API,\ni.e. as a result of traversing domain entities graph which was not fully loaded or invoking method on proxy entity, or by flushing dirty entity / collection\n\n\n== Configuration\n==== Default configuration\nWhen no configuration is provided JPlusOne assumes following settings:\n\n* The root package where application classes is located is the same as the package where the class annotated with `@SpringBootApplication` is located\n* Only implicit operations are reported\n* Only SQL SELECT statements are reported\n* Operations / SQL statements triggered by Flyway are ignored\n\n==== Custom configuration\nYou can overwrite default configuration by adding some of the following properties to your `application.yml` (optional):\n\n[source,yaml]\n----\n# com.adgadev.jplusone.core.properties.JPlusOneProperties\njplusone:\n  enabled: true\n  application-root-package: \"com.sampleorganisation.sampleproject\"\n  debug-mode: false\n  report:\n    enabled: true\n    output: LOGGER\n    proxy-call-frames-hidden: true\n    operation-filtering-mode: ALL_OPERATIONS\n    statement-filtering-mode: ALL_STATEMENTS\n    file-path: target/jplusone-report.txt\n----\n\n==== Configuration properties:\n[cols=2*]\n|===\n|`jplusone.enabled`\n|Flag determining if JPlusOne autoconfiguration is enabled, all SQL statements intercepted.\n\nDefault value: `true`\n\n|`jplusone.application-root-package`\n|Root package of your project. Calls made to methods of classes outside the root package won't be analysed and visible in the report.\n\nDefault value: package where the class annotated with `@SpringBootApplication` is located\n\n|`jplusone.debug-mode`\n|Flag determining if JPlusOne debug mode is enabled.\n\nDefault value: `false`\n\n|`jplusone.report.enabled`\n|Flag determining if report should be written to logs.\n\nDefault value: `true`\n\n|`jplusone.report.operation-filtering-mode`\n|Defines what kind of operations should be visible in the report. Possible values: `IMPLICIT_OPERATIONS_ONLY`, `EXPLICIT_OPERATIONS_ONLY`, `COMMIT_OPERATIONS_ONLY`, `ALL_OPERATIONS`\n\nDefault value: `IMPLICIT_OPERATIONS_ONLY`\n\n|`jplusone.report.statement-filtering-mode`\n|Defines what kind of SQL statements should be visible in the report. Possible values: `READ_STATEMENTS_ONLY`, `WRITE_STATEMENTS_ONLY`, `ALL_STATEMENTS`\n\nDefault value: `READ_STATEMENTS_ONLY`\n\n|`jplusone.report.proxy-call-frames-hidden`\n|Flag determining if proxy call frames are hidden. It does not affect last frame of the call stack.\n\nDefault value: `true`\n\n|`jplusone.report.output`\n|Defines which output will be used to print report. Possible values: `LOGGER`, `STDOUT`, `FILE`, `LOGGER_AND_FILE`\n\nDefault value: `LOGGER`\n\n|`jplusone.report.file-path`\n|Absolute or relative path to a file with the report. It has an effect only if output is `FILE` or `LOGGER_AND_FILE`\n\nDefault value: `target/jplusone-report.txt`\n|===\n\n== Troubleshooting\n==== Problems\nHaving JPlusOne configured, each testcase method which tests logic related with JPA persistence operations (direct or indirect use of EntityManager)\nshould result in either detailed report being printed in logs or the information in the logs that no JPA operations / SQL statements matching criteria has been captured.\n\nThere are multiple reasons why no logs entries for `com.adgadev.jplusone` are printed or such log entries are printed only for part of the persistence related tests. Most common cases are:\n\n* Logging system configuration (i.e. logback) has been changed / overwritten dynamically i.e by autoconfiguration when spring boot works in debug mode (\"debug: true\" YAML property)\n* Due to the fact that SpringRunner caches spring contexts used in tests and logging system configuration is being refreshed only during new spring context creation,\nthere might be a situation that invalid logging system configuration is being used when test runner intertwines execution of tests from various spring contexts.\n\n+\nIn example, assuming there are two spring contexts (S1 context with logger configuration L1 and S2 context with logger configuration L2) and three test classes (A, B, C), where A and B uses S1 context and C uses S2 context.\nWhen test runner executes tests in order A,B,C everything is fine, but when order execution is A,C,B testcases from class B will use the same logger configuration as C - L2 logger instead of L1.\n\n==== Workarounds\nThere are two possible workarounds for such issue:\n\n* Refresh JPlusOne logger configuration before executing first testcase for each integration test class:\n+\n[source,java]\n----\n@BeforeClass\npublic static void refreshLoggerConfiguration() {\n   LoggingSystem.get(ClassLoader.getSystemClassLoader())\n                .setLogLevel(\"com.adgadev.jplusone\", LogLevel.DEBUG);\n}\n----\n* Force JPlusOne reports to be printed directly to the stdout, instead of logger, using property:\n`jplusone.report.output=STDOUT`\n\n== License\nimage:{fossa-large-icon}[FOSSA Status, link={fossa-large-link}]\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadgadev%2Fjplusone","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadgadev%2Fjplusone","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadgadev%2Fjplusone/lists"}