JPA에서 게시한 SQL 문의 소스와 컨텍스트 찾기


우리는 여기서 무슨 문제를 해결합니까?
Hibernate와 다른 JPA 실현자와 같은 ORM 프레임워크는 지구층의 개발을 현저하게 간소화할 수 있다.실체 추상 단계를 도입하면 깨끗한 업무 영역을 모델링하고 영역의 지속성을 실현하는 데 사용되는 하위 SQL 문장을 숨깁니다.개발자가 더 이상 응용 프로그램에서 사용하는 모든 SQL 문장을 만들고 유지할 필요가 없기 때문에 이런 방법은 대형 영역에서 특히 유용하다.도메인 클래스 및@Entity 또는 사용자 정의 저장소에 @ManyToOne/@OneToMany/EntityManager/fetchType = FetchType.EAGER 등의 JPA 주석을 추가하면 도메인 집합 루트를 얻을 수 있으며 지금부터 시작할 수 있습니다.이렇게 하면 지속성과 관련된 절대 다수의 용례는 역 모델에서 JPA 등급을 연결하고 반복적으로 연결하며 이를 실행하는 조작을 통해 처리할 수 있다.
모든 것은 대가가 있는 것이다.청결하고 편리한 발전에 대한 교환으로 몇 가지 문제가 조만간 나타날 것이다.
  • N+1 선택 문제
  • 로 인한 과도한 로드로 인한 대량의 SQL 문
  • 비효율적인 방식으로 수행되는 긴급 복구(즉, application.yml 관련 문제)
  • 일부 용례/서비스 호출에서 호출된 모든 SQL 문장을 열거하고 정확한 코드 줄과 연결하는 것은 간단한 방법이 아니다.
  • 어떤 용례에 대해서도 캡처가 최적화되어 필요한 모든 데이터를 효과적으로 불러올 수 있어도 미래의 코드 변경은 조용히 파괴될 수 있고 쉽게 막을 수 있는 방법이 없다.
  • 다음 단계에서 나는 이 문제들을 어떻게 검출하는지 새로운 방법을 보여줄 것이지만, 우선 고전적인 방법을 보여 주겠다.

    Hibernate를 통해 JPA가 게시한 SQL 문을 기록합니다.
    현재로서는 JPA가 어떤 SQL 문장을 실행했는지 검사하는 가장 간단한 방법은 JPA가 발표한 모든 SQL 문장의 로그 기록을 사용한 다음, 일부 데이터를 가져오거나 새로 고치거나, 사무 제출로 촉발되었는지 하나하나 조사하는 것이다.JPA가 logger를 통해 실행한 SQL 문장을 기록하기 위해서는 spring.jpa.show-sql에 다음과 같은 속성을 추가해야 한다
        logging.level:
            org.hibernate.SQL: DEBUG
    
        spring:
          jpa:
            properties:
              hibernate:
                format_sql: true        # optional
                use_sql_comments: true  # optional
    
    기록기의 로그 기록이 아닌 표준 출력 (남겨진 것) 속성을 활성화할 수 있습니다.
    따라서 다음과 같은 로그를 보고할 수 있습니다.
        2020-10-31 19:37:40.730 DEBUG 15775 --- [           main] org.hibernate.SQL                        :
            select
                products0_.order_id as order_id1_9_0_,
                products0_.product_id as product_2_9_0_,
                product1_.id as id1_14_1_,
                product1_.manufacturer_id as manufact3_14_1_,
                product1_.name as name2_14_1_
            from
                order_product products0_
            inner join
                product product1_
                    on products0_.product_id=product1_.id
            where
                products0_.order_id=?
    
    hibernate 로그의 일부에는 SQL 문을 JPA 솔리드 클래스와 연결하는 SQL 주석이 포함될 수 있습니다.
        2020-11-25 23:51:58.390 DEBUG 28750 --- [           main] org.hibernate.SQL                        :
            /* insert com.adgadev.blog.examples.bookshop.Book */ insert
                into
                    book
                    (author_id, title, id)
                values
                    (?, ?, ?)
    
    일반적으로 이런 로그는 매우 유용할 수 있지만, 응용 프로그램 소스 코드에서 그것을 터치하는 위치를 어떻게 찾는지 관건적인 정보가 부족하다.이것은 JPA 실체 연결을 통해 실체 에이전트를 초기화한 결과입니까? 아니면 서비스가 JPQL 조회를 현시적으로 실행한 결과입니까?만약 응용 프로그램에 여러 개의 다른 서비스가 이러한 검색을 촉발할 수 있다면, 어느 것입니까?흐름은 무엇입니까?
    현대 코드 라이브러리의 영역과 논리는 매우 간단하거나, SQL 문장이 코드의 특정한 위치만을 가리킬 때, 추측을 시도할 수 있지만, 이것은 항상 쉽지 않다. 특히 JPA가 생성한 검색이 일부 실체를 불러오는 지연되는 상황에서는.이 경우 코드를 디버깅하고 SQL 문을 검사하는 행이 유일한 선택일 수 있습니다.디버그의 또 다른 문제는 IDE가 단점에서 변수의 값을 해석하기 때문에 실체의 불러오는 방식을 바꿀 수 있다는 것이다.한 서비스를 호출할 때 코드의 다른 위치에서 수십 개의 SQL을 촉발할 수 있다고 상상해 보세요. 이 SQL들은 모두 이런 서비스에서 호출됩니다.이 일을 조사하는 것은 매우 번거롭고, 게다가 정말 많은 시간이 필요하다.

    제프리슨이 구원하러 왔어요.
    도서관은 이런 문제들을 해결하는 데 목적을 두고 있다.일부 용례를 처리할 때 프로그램이 트리거하는 모든 JPA/SQL 활동을 포함하는 간결한 보고서를 만들 수 있습니다.이렇게 하면 호출된 SQL 문구를 JPA 작업과 쉽게 연결하고, 호출된 코드에서 정확한 줄을 찾을 수 있습니다.
        2020-11-26 22:20:52.096 DEBUG 9990 --- [           main] c.a.j.core.report.ReportGenerator        :
            ROOT
                com.adgadev.examples.bookshop.BookshopControllerTest.shouldGetBookDetails(BookshopControllerTest.java:46)
                com.adgadev.examples.bookshop.BookshopController.getSampleBook(BookshopController.java:31)
                com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails [PROXY]
                    SESSION BOUNDARY
                        OPERATION [EXPLICIT]
                            com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails(BookshopService.java:32)
                            com.adgadev.examples.bookshop.BookRepository.findById [PROXY]
                                STATEMENT [READ]
                                    select [...] from
                                        book book0_
                                    where
                                        book0_.id=1
                        OPERATION [IMPLICIT]
                            com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails(BookshopService.java:34)
                            com.adgadev.examples.bookshop.Author.getName [PROXY]
                            com.adgadev.examples.bookshop.Author [FETCHING ENTITY]
                                STATEMENT [READ]
                                    select [...] from
                                        author author0_
                                    where
                                        author0_.id=1
                        OPERATION [IMPLICIT]
                            com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails(BookshopService.java:35)
                            com.adgadev.examples.bookshop.Author.countWrittenBooks(Author.java:41)
                            com.adgadev.examples.bookshop.Author.books [FETCHING COLLECTION]
                                STATEMENT [READ]
                                    select [...] from
                                        book books0_
                                    where
                                        books0_.author_id=1
    
    또한 트리거하는 동작을 감지합니다.
  • 지연 로드를 초기화하기 위해 단일 엔티티를 암시적으로 가져오기@ManyToOne/@OneToOnefetch로 설정LAZY
  • 암시적으로 실체 집합을 가져와 관련 지연 로드'다'단@OneToMany/@ManyToMany을 초기화하고 fetch으로 설정LAZY
  • 저장소 호출(SpringData JPA, 사용자 정의 DAO) 또는 직접 EntityManager
  • 를 통해 데이터 가져오기
    트랜잭션 제출 시 트리거
  • INSERT/SELECTJPlusOne 모든 스프링 커버 2와 호환됩니다.x 기반 응용 프로그램은 JDK 9+에서 실행되며 Hibernate는 JPA 구현자입니다.
    JPlusOne
    JPA 도메인 및 서비스 예
    이러한 예를 더 잘 설명하기 위해서, 우리는 두 개의 JPA 실체로 구성된 간단한 영역을 가정한다.첫 번째는 Author 솔리드입니다.
        @Getter
        @Entity
        class Author {
    
            @Id
            private Long id;
    
            private String name;
    
            @OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
            private Set<Book> books = new HashSet<>();
    
            int countWrittenBooks() {
                return books.size();
            }
        }
    
    두 번째는 Book 솔리드입니다.
        @Getter
        @Entity
        @EqualsAndHashCode(of = "id")
        class Book {
    
            @Id
            private Long id;
    
            private String title;
    
            @ManyToOne(fetch = FetchType.LAZY)
            @JoinColumn(name = "author_id")
            private Author author;
    
        }
    
    또 하나의 간단한 서비스BookshopService:
        @Service
        @Transactional
        @RequiredArgsConstructor
        class BookshopService {
    
            private final BookRepository bookRepository;
    
            public BookDto getSampleBookDetails() {
                Book book = bookRepository.findById(1L).get();
                String authorName = book.getAuthor().getName();
                int amountOfBooks = book.getAuthor().countWrittenBooks();
    
                return new BookDto(authorName, book.getTitle(), amountOfBooks);
            }
        }
    
    간단한 MVC 디렉터가 하나 더 있습니다.
        @RestController
        @RequiredArgsConstructor
        class BookshopController {
    
            private final BookshopService bookshopService;
    
            @GetMapping("/book/lazy")
            BookDto getSampleBook() {
                return bookshopService.getSampleBookDetails();
            }
        }
    

    JPA 지연 로드 작업 및 관련 SQL 문 보고
    JPA 로드 지연을 감지하기 위해 다음 종속성을 추가합니다.
        <dependency>
            <groupId>com.adgadev.jplusone</groupId>
            <artifactId>jplusone-core</artifactId>
            <version>1.0.1</version>
            <scope>test</scope>
        </dependency>
    
    마지막 단계는 jplusone에 레코더를 설정하는 것입니다. application.yml 에 다음 줄을 추가합니다.
        logging.level:
            com.adgadev.jplusone: DEBUG
    
    Spring Boot'a 자동 구성은 나머지 구성을 자동으로 완료합니다.현재 JPA의 지속성을 직접 또는 간접적으로 이용하는 통합 테스트, 즉 BookshopController 단점에 요청하는 테스트를 실행합니다.
        @ActiveProfiles("integration-test")
        @SpringBootTest(webEnvironment = MOCK)
        @AutoConfigureMockMvc
        class BookshopControllerTest {
    
            @Autowired
            private MockMvc mvc;
    
            @Test
            void shouldGetBookDetails() throws Exception {
                mvc.perform(MockMvcRequestBuilders
                        .get("/book/lazy")
                        .accept(MediaType.APPLICATION_JSON))
                        .andExpect(status().isOk());
            }
        }
    
    그러면 다음과 같이 JPA 작업/SQL 문에 대한 JPlusOne 보고서가 포함된 로그 항목이 추가됩니다.
        2020-11-26 22:27:59.683 DEBUG 10730 --- [           main] c.a.j.core.report.ReportGenerator        :
            ROOT
                com.adgadev.examples.bookshop.BookshopControllerTest.shouldGetBookDetails(BookshopControllerTest.java:46)
                com.adgadev.examples.bookshop.BookshopController.getSampleBook(BookshopController.java:31)
                com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails [PROXY]
                    SESSION BOUNDARY
                        OPERATION [IMPLICIT]
                            com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails(BookshopService.java:34)
                            com.adgadev.examples.bookshop.Author.getName [PROXY]
                            com.adgadev.examples.bookshop.Author [FETCHING ENTITY]
                                STATEMENT [READ]
                                    select [...] from
                                        author author0_
                                    where
                                        author0_.id=1
                        OPERATION [IMPLICIT]
                            com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails(BookshopService.java:35)
                            com.adgadev.examples.bookshop.Author.countWrittenBooks(Author.java:41)
                            com.adgadev.examples.bookshop.Author.books [FETCHING COLLECTION]
                                STATEMENT [READ]
                                    select [...] from
                                        book books0_
                                    where
                                        books0_.author_id=1
    
    이 예에서는 다음과 같은 두 가지 지연 로드 작업을 볼 수 있습니다.
  • 방법BookshopService.getSampleBookDetailsUsingLazyLoading에서 JPA 실체getName의 대리 대상에서 집행방법Author.그 결과 프록시 객체의 초기화가 트리거되어 SQL 질의가 수행됩니다.
  • 방법Author.countWrittenBooks에서 집합books의 내용을 방문했는데 이 집합은 Book 실체와 관련된'많은'부분을 대표한다.그 결과 SQL 조회를 실행할 때 이 집합의 초기화Author.books를 촉발했다.
  • JPA에서 생성된 조회, 특히 소량의 연결을 응용한 조회는 대량의 선택된 열을 포함할 수 있기 때문에 성능 최적화 측면에서 볼 때 많은 가치를 가져오지는 않지만 복잡한 SQL 문장의 가독성을 떨어뜨릴 수 있기 때문에 JPlusOne은 보고서에서 이를 […​]로 대체했다.

    기타 JPA 작업 및 관련 SQL 문 유형 보고
    기본적으로 보고서에는 SQL SELECT 문을 지연시키는 로드 작업만 포함됩니다(IMPLICIT.일부 데이터 가져오기/새로 고침EXPLICIT에 대한 명시적 호출과 관련된 JPA 작업은 포함되지 않습니다.마찬가지로, 사무를 제출할 때의 세션 리셋과 관련된 작업 (COMMIT.application.yml에 다음 줄을 추가하여 사용자 정의 필터 조건을 정의하면 기본 동작을 쉽게 변경할 수 있습니다. 지원하는 필터 모드와 기타 설정 옵션에 대한 자세한 정보는 를 참조하십시오.
        jplusone:
          report:
            operation-filtering-mode: EXPLICIT_OPERATIONS_ONLY
            statement-filtering-mode: ALL_STATEMENTS
    
    이전 예와 동일한 테스트를 실행하면 다음 보고서가 생성됩니다.
        2020-11-26 22:30:13.497 DEBUG 10997 --- [           main] c.a.j.core.report.ReportGenerator        :
            ROOT
                com.adgadev.examples.bookshop.BookshopControllerTest.shouldGetBookDetails(BookshopControllerTest.java:46)
                com.adgadev.examples.bookshop.BookshopController.getSampleBook(BookshopController.java:31)
                com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails [PROXY]
                    SESSION BOUNDARY
                        OPERATION [EXPLICIT]
                            com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails(BookshopService.java:32)
                            com.adgadev.examples.bookshop.BookRepository.findById [PROXY]
                                STATEMENT [READ]
                                    select [...] from
                                        book book0_
                                    where
                                        book0_.id=1
    
    보고서에 따르면 34행BookshopService.getSampleBookDetailsUsingLazyLoading에서 Spring Data JPA repositoryfindById를 호출하는 방법BookRepository을 통해 현저한 가져오기 동작을 촉발하여 SQL 조회를 실행했다.
    JPlusOne documentation
    JPA/SQL 최적화 서비스의 향후 최적화 보장
    이제 테스트는 코드가 예상대로 작동하고 버그가 없는지 확인하기 위한 것이 아니다.코드의 다른 측면도 테스트할 수 있다.하나의 좋은 예는 인데 응용 프로그램의 단원 테스트 구조, 즉 응용 프로그램의 층 구조를 유지보수할 수 있도록 허용한다.시스템 구조에 대해 단원 테스트를 할 수 있는 이상 왜 JPA/SQL 성능 측면에서 최적화된 용례 논리가 미래의 코드 변경 후에 최적화 상태를 유지하는지 테스트하지 않습니까?일부 JPA 맵/연결을 추가하거나 수정하면 일부 흐름에 추가 지연 로드 작업을 쉽게 도입할 수 있지만, 이러한 변경 사항을 적용할 때 이 점을 발견하기 어려울 수도 있습니다.
    이전 예시에서 통합 테스트를 확장합니다. 검증 검사는 두 개의 JPA 지연 불러오기 동작만 있습니다. 첫 번째는 불러오기 Author 에 사용되고, 두 번째는 불러오기 Author.books 집합에 사용됩니다.이러한 테스트를 작성하려면 다음 종속성을 추가해야 합니다.
        <dependency>
            <groupId>com.adgadev.jplusone</groupId>
            <artifactId>jplusone-assert</artifactId>
            <version>1.0.1</version>
            <scope>test</scope>
        </dependency>
    
    그리고 JPlusOneAssertionRule 대상을 정의하고 주입assertionContext에 따라 검사해야 한다.
        @ActiveProfiles("integration-test")
        @SpringBootTest(webEnvironment = MOCK)
        @AutoConfigureMockMvc
        class BookshopControllerTest {
    
            @Autowired
            private JPlusOneAssertionContext assertionContext;
    
            @Autowired
            private MockMvc mvc;
    
            @Test
            void shouldGetBookDetails() throws Exception {
                mvc.perform(MockMvcRequestBuilders
                        .get("/book/lazy")
                        .accept(MediaType.APPLICATION_JSON))
                        .andExpect(status().isOk());
    
                JPlusOneAssertionRule rule = JPlusOneAssertionRule
                        .within().lastSession()
                        .shouldBe().noImplicitOperations().exceptAnyOf(exclusions -> exclusions
                                .loadingEntity(Author.class)
                                .loadingCollection(Author.class, "books")
                        );
                rule.check(assertionContext);
            }
        }
    
    규정에 부합되는 이상 테스트 결과는 녹색이다.실행 중 세 번째 지연 불러오기 동작을 도입할 때 어떤 일이 일어나는지 보여 줍니다.가장 간단한 방법은 BookshopService.getSampleBookDetailsUsingLazyLoading의 단일행을 다음과 같이 변경하는 것입니다.
                Book book = bookRepository.findById(1L).get();
    
    다음을 수행합니다.
                Book book = bookRepository.getOne(1L);
    
    그러면 JPA 엔티티가 아닌 JPA 에이전트로 돌아갑니다.에이전트는 위에서 호출된 첫 번째 작업에서 초기화되어 Book 실체의 로드 지연을 초래합니다.테스트AssertionError를 다시 실행할 때 던지기:
        java.lang.AssertionError: Actual amount of IMPLICIT operations after applying exclusions is different than the expected amount
            Expected: exactly 
            Actual  : 
    
        Operations after applying requested exclusions:
            ROOT
                com.adgadev.examples.bookshop.BookshopControllerTest.shouldGetBookDetails(BookshopControllerTest.java:46)
                com.adgadev.examples.bookshop.BookshopController.getSampleBook(BookshopController.java:31)
                com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails [PROXY]
                    SESSION BOUNDARY
                        OPERATION [IMPLICIT]
                            com.adgadev.examples.bookshop.BookshopService.getSampleBookDetails(BookshopService.java:34)
                            com.adgadev.examples.bookshop.Book.getAuthor [PROXY]
                            com.adgadev.examples.bookshop.Book [FETCHING ENTITY]
                                STATEMENT [READ]
                                    select [...] from
                                        book book0_
                                    where
                                        book0_.id=1
    
            at com.adgadev.jplusone.asserts.impl.rule.AmountVerifier.checkAmount(AmountVerifier.java:44)
            at com.adgadev.jplusone.asserts.impl.rule.Condition.checkOperation(Condition.java:84)
            at com.adgadev.jplusone.asserts.impl.rule.Condition.check(Condition.java:54)
            at com.adgadev.jplusone.asserts.impl.rule.Rule.lambda$check$0(Rule.java:48)
            at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
            at com.adgadev.jplusone.asserts.impl.rule.Rule.check(Rule.java:48)
            at com.adgadev.jplusone.asserts.impl.Assertion.check(Assertion.java:38)
            at com.adgadev.jplusone.asserts.impl.ConditionDoneBuilderImpl.check(ConditionDoneBuilderImpl.java:38)
            at com.adgadev.examples.bookshop.BookshopControllerTest.shouldGetBookDetails(BookshopControllerTest.java:57)
            ...
    
    ArchUnit
    요약
    예시된 전체 소스 코드 를 사용할 수 있습니다.
    here가 유용하고 더 개발할 가치가 있다고 생각되면 JPlusOne library에 프로젝트 라이브러리를 표시하십시오. 감사합니다!

    좋은 웹페이지 즐겨찾기