반응형
05-15 00:00
Today
Total
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
관리 메뉴

개발하는 고라니

[JPA] Spring Data JPA 본문

Framework/JPA (Hibernate)

[JPA] Spring Data JPA

조용한고라니 2021. 10. 9. 19:35
반응형

JPA를 사용함으로써 단순 반복하는 CRUD의 SQL 작성을 획기적으로 줄일 수 있었다.

예를 들어, 테이블을 만들 때마다 Insert 쿼리, Select 쿼리, Update 쿼리, Delete 쿼리 1개씩은 대게 기본으로 SQL을 짜던 것을 JPA에서는 메서드로 가능하게 해주었다.

 

하지만 인간의 욕심은 끝이없다던가,, 이 간단한 CRUD 작업을 메서드로 하는 것 마저 반복을 줄일 수 없을까 고민을 하게되고, 처음에 이를 해결한 오픈소스 프로젝트를 Spring 측에서 함께 만들어 출시한 것이 Spring Data 프로젝트의 Spring Data JPA이다. 즉, Spring Data JPA를 사용한다면 간단한 저장/삭제/조회 메서드는 기본으로 장착이 되어있고, 커스터마이징 하는 것 또한 더 간편하게 할 수 있다는 장점이 있다.

Spring Data JPA

Spring Data JPA를 사용하기 위해 보편적으로 'JpaRepository<T, ID>' 인터페이스를 상속한 Repository 인터페이스를 정의한다. 단지 인터페이스를 상속했을 뿐인데, 기본적인 메서드를 이미 장착한 상태이고, 심지어 정의한 인터페이스를 구현할 필요도 없다. Spring이 알아서 해주기 때문이다.

 

JpaRepository는 위와 같은 구조를 갖는다. 즉, JpaRepository는 오직 JPA만을 위해 특화된 녀석이고, 위의 상위 타입은 Spring Data 프로젝트에서 공통으로 갖는 기능을 정의해두는 녀석들이다.

JpaRepository 주요 메서드

이 인터페이스에서 자주 쓰이는 메서드는 다음과 같다.

 

  • save(S) : 새로운 엔티티는 저장하고, 이미 존재하는 엔티티는 병합한다.
  • delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 사용
  • findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 사용
  • getById(ID) : 엔티티 하나를 프록시로 조회한다. 내부에서 EntityManager.getReference() 사용
    • getOne(ID) : Deprecated
  • findAll(...) : 모든 엔티티를 조회하며, Sort나 Pageable 조건을 인자로 전달할 수 있다.

그 밖에도 다음과 같은 메서드가 있다.

deleteAllInBatch(Iterable<t>)
deleteAllByIdInBatch(Iterable<ID>)
deleteAllInBatch()

saveAll(Iterable<S>)
saveAndFlush(S)
saveAllAndFlush(Iterable<S>)

...

예제를 위한 도메인

User와 Game도메인이 있고, User : Game= N : 1이다. 즉 하나의 Game는 여러 User를 가질 수 있다.

쿼리 메서드

위의 메서드들은 어찌되었건 모든 엔티티에 대해 공통으로 쓰일 수 있는 메서드를 제공하지만, 사실 비즈니스 로직을 다루는 것은 그리 간단하지 않다. 조건을 달아 조회하거나, 제거하거나 저장할 수 있는 기능들을 커스터마이징 해야한다.

 

Spring Data JPA는 Repository를 커스터마이징 하기위해 쿼리 메서드 기능을 제공하는데, 3가지 방법이 있다.

  • 1) 메서드 이름으로 쿼리 생성 (간단한 것 쓸 때 좋음)
  • 2) 메서드 이름으로 JPA NamedQuery 호출 (잘 안쓰임)
  • 3) @Query 안에 JPQL 정의 (중요)

1) 메서드 이름으로 쿼리 생성

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

위 링크에 보면 상세하게 나와있지만, 그 중에 몇 가지만 보자.

Select

- find...By

- read...By

- query...By

- get...By

 

Count (return Long)

- count...By

 

Exist (return Boolean)

exists...By

 

Delete (return Long)

- delete...By

- remove...By

 

Distinct

- findDistinct

- findMemberDistinctBy

 

Limit

- findFirst3

- findTop

 

public interface UserRepository extends JpaRepository<User, Long> {

    //나이가 age살 이상인 유저 조회
    List<User> findByAgeGreaterThanEqual(int age);

    //이름으로 조회
    List<User> findByName(String name);
}
    //Test
    @Test
    @DisplayName("이름으로 조회 테스트")
    public void findByNameTest() throws Exception{
        //given
        List<User> list = new ArrayList<>();
        list.add(new User("김", 10));
        list.add(new User("나", 20));
        list.add(new User("박", 30));
        list.add(new User("이", 10));
        list.add(new User("정", 20));

        userRepository.saveAll(list);
        em.flush();
        em.clear();
        //when
        List<User> result = userRepository.findByName("김");

        //then
        assertThat(result.get(0).getName()).isEqualTo("김");
        assertThat(result.size()).isEqualTo(1);
        System.out.println(result.get(0));
    }
    //Test
    @Test
    public void 나이가age살이상_테스트() throws Exception{
    
        //given
        List<User> list = new ArrayList<>();
        list.add(new User("김", 10));
        list.add(new User("나", 20));
        list.add(new User("박", 30));
        list.add(new User("이", 10));
        list.add(new User("정", 20));

        userRepository.saveAll(list);
        em.flush();
        em.clear();
        //when

        List<User> result = userRepository.findByAgeGreaterThanEqual(20);

        //then
        assertThat(result.size()).isEqualTo(3);
        assertThat(result.get(0).getAge()).isGreaterThanOrEqualTo(20);
    }

 

(장점)

엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야한다. 이는 애플리케이션 로딩시점에 오류를 잡아낼 수 있는 매우 좋은 기능이다.

3) @Query 안에 JPQL 정의

이는 실행할 메서드에 '정적 쿼리'를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다.

이것 또한 NamedQuery처럼 애플리케이션 로딩 시점에 문법 오류를 잡아낼 수 있는 큰 장점이 있다.

@Query, 값, DTO 조회

@Query를 사용했을 때 반환타입은 여러가지를 사용할 수 있다. 대표적으로 다음과 같은 것이 있다.

  • Primitive
  • Wrapper
  • Entity
  • DTO
  • List
  • Collections
  • Optional<>
※ em.getSingleResult()
- 반드시 하나만 있는 값을 조회할 때는 getSingleResult() 메서드를 사용하는데, JPA와 Spring Data JPA는 차이가 있다.

JPA)
- 값이 없으면 NoResultException 예외가 발생한다.

Spring Data JPA)
- 값이 없으면 내부적으로 try-catch해서 null을 반환한다.

※ em.getResultList()
- 컬렉션을 조회할 때 값이 없으면 null을 반환하는 것이 아니라, 빈 컬렉션을 반환하는 것을 기억하자!

Parameter 바인딩

파라미터를 바인딩 하는 것은 2가지 방법이 있으나, 1가지만 쓰도록 하자.

  • 위치 기반 (X) -> 위치가 달라지면 버그를 유발할 수 있음
  • 이름 기반 (O)
  • Collection (in 절을 지원한다)
@Query("select u from User u where u.name = ?0") //위치
List<User> findUsersByName(@Param("name") String name);

@Query("select u from User u where u.name = :name") //이름
List<User> findUsersByName(@Param("name") String name);

@Query("select u from User u where u.id in :ids") //Collection
List<User> findUsersByIds(@Param("ids") List<Long> ids);

Spring Data JPA 페이징과 정렬

정렬을 할 때 Sort라는 녀석을 사용하고, 페이징할 때 Pageable이라는 녀석을 사용하는데 이들은 JPA에 국한된 것이 아니다.

org.springframework.data.domain.Sort //정렬
org.springframework.data.domain.Pageable //페이징

----------------------------

//추가 count 쿼리 결과를 포함하는 페이징
org.springframework.data.domain.Page
//추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1의 결과를 조회)
org.springframework.data.domain.Slice
//추가 count 쿼리 없이 결과만 반환
java.util.List

 

유저를 페이징하고 정렬하여 조회)

    @Query("select u from User u")
    Page<User> findUsersUsingPagingAndSort(Pageable pageable);
    @Test
    @DisplayName("간단한 페이징을 이용한 유저 조회 테스트")
    public void 간단한_페이징_테스트() throws Exception {
        //given
        List<User> list = new ArrayList<>();
        list.add(new User("김", 10));
        list.add(new User("나", 20));
        list.add(new User("박", 30));
        list.add(new User("이", 10));
        list.add(new User("정", 20));
        list.add(new User("윤", 10));
        list.add(new User("강", 20));
        userRepository.saveAll(list);

        em.flush();
        em.clear();
        //when
        //3명씩 2페이지에 있는 User 조회 (0페이지부터 시작한다)
        PageRequest pageable = PageRequest.of(1, 3, Sort.by(Sort.Direction.DESC, "id"));
        Page<User> result = userRepository.findUsersUsingPagingAndSort(pageable);

        //then
        assertThat(result.getContent().size()).isEqualTo(3);
        //전체 데이터 수
        System.out.println("result.getTotalElements() = " + result.getTotalElements());
        //전체 페이지 수
        System.out.println("result.getTotalPages() = " + result.getTotalPages());
        //현재 페이지 번호
        System.out.println("result.getNumber() = " + result.getNumber());
        //현재 조회한 개수
        System.out.println("result.getNumberOfElements() = " + result.getNumberOfElements());
        //첫번째 항목인지?
        System.out.println("result.isFirst() = " + result.isFirst());
    }
    select
        user0_.user_id as user_id1_3_,
        user0_.age as age2_3_,
        user0_.game_id as game_id4_3_,
        user0_.name as name3_3_ 
    from
        user user0_ 
    order by
        user0_.user_id desc limit ? offset ?
        
    select
        count(user0_.user_id) as col_0_0_ 
    from
        user user0_

Page로 조회하면 User를 조회하는 쿼리와, 총 몇개의 데이터를 갖는지 countQuery가 함께 실행된다.

 

유저 join 게임 + 페이징 + 정렬)

    //유저와 게임 조인 + 페이징 + 정렬
    @Query("select u, g from User u left join u.game g")
    Page<User> findUsersAndGameUsingPagingAndSort(Pageable pageable);
    @Test
    @DisplayName("유저와 게임 조인 + 정렬 + 페이징")
    public void 유저와게임_조인_테스트() throws Exception{
        //given
        //게임
        Game game = Game.builder().name("game A").build();
        em.persist(game);
        //유저
        List<User> list = new ArrayList<>();
        list.add(new User("김", 10, game));
        list.add(new User("나", 20, game));
        list.add(new User("박", 30, game));
        list.add(new User("이", 10, game));
        list.add(new User("정", 20, game));
        list.add(new User("윤", 10, game));
        list.add(new User("강", 20, game));
        userRepository.saveAll(list);

        em.flush();
        em.clear();
        //when
        PageRequest pageable = PageRequest.of(0, 4, Sort.by(Sort.Direction.DESC, "id"));
        Page<User> result = userRepository.findUsersAndGameUsingPagingAndSort(pageable);

        //then
        assertThat(result.getContent().size()).isEqualTo(4);
    }
    select
        user0_.user_id as user_id1_3_0_,
        game1_.game_id as game_id1_0_1_,
        user0_.age as age2_3_0_,
        user0_.game_id as game_id4_3_0_,
        user0_.name as name3_3_0_,
        game1_.name as name2_0_1_ 
    from
        user user0_ 
    left outer join
        game game1_ 
            on user0_.game_id=game1_.game_id 
    order by
        user0_.user_id desc limit ?
        
    select
        count(user0_.user_id) as col_0_0_ 
    from
        user user0_ 
    left outer join
        game game1_ 
            on user0_.game_id=game1_.game_id

마찬가지로 User를 조회하는 쿼리와 countQuery가 출력된다. 하지만 count쿼리에도 조인이 되어 날아간다. 굳이 이럴 필요 있을까? 어짜피 User의 개수는 조인한다고 해서 변하지 않는다. (where 조건절을 안쓸 경우) 따라서 성능을 위해 countQuery를 명시해주자.

 

    //유저와 게임 조인 + 페이징 + 정렬 + countQuery 명시
    @Query(value = "select u, g from User u left join u.game g"
            , countQuery = "select count(u) from User u")
    Page<User> findUsersAndGameUsingPagingAndSort(Pageable pageable);
    select
        user0_.user_id as user_id1_3_0_,
        game1_.game_id as game_id1_0_1_,
        user0_.age as age2_3_0_,
        user0_.game_id as game_id4_3_0_,
        user0_.name as name3_3_0_,
        game1_.name as name2_0_1_ 
    from
        user user0_ 
    left outer join
        game game1_ 
            on user0_.game_id=game1_.game_id 
    order by
        user0_.user_id desc limit ?
        
    select
        count(user0_.user_id) as col_0_0_ 
    from
        user user0_

이제 countQuery에 조인이 들어가지 않는다!

 

유저 fetch join 게임 + 페이징 + 정렬)

    //유저 fetch join 게임 + 페이징 + 정렬
    @Query(value = "select u from User u join fetch u.game")
    Page<User> findUsersAndGameUsingFetchJoin(Pageable pageable);
    @Test
    @DisplayName("유저와 게임 페치 조인 + 정렬 + 페이징")
    public void 유저와게임_페치조인_테스트() throws Exception{
        //given
        //게임
        Game game = Game.builder().name("game A").build();
        em.persist(game);
        //유저
        List<User> list = new ArrayList<>();
        list.add(new User("김", 10, game));
        list.add(new User("나", 20, game));
        list.add(new User("박", 30, game));
        list.add(new User("이", 10, game));
        list.add(new User("정", 20, game));
        list.add(new User("윤", 10, game));
        list.add(new User("강", 20, game));
        userRepository.saveAll(list);

        em.flush();
        em.clear();
        //when
        PageRequest pageable = PageRequest.of(0, 4, Sort.by(Sort.Direction.DESC, "id"));
        Page<User> result = userRepository.findUsersAndGameUsingFetchJoin(pageable);

        //then
        assertThat(result.getContent().size()).isEqualTo(4);
    }

위 테스트를 돌리면 예외가 뜬다. 예외는 다음과 같다.

java.lang.IllegalStateException: Failed to load ApplicationContext
...
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository' defined in study.datajpa.test.UserRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Invocation of init method failed; nested exception is org.springframework.data.repository.query.QueryCreationException: Could not create query for public abstract org.springframework.data.domain.Page study.datajpa.test.UserRepository.findUsersAndGameUsingFetchJoin(org.springframework.data.domain.Pageable)! Reason: Count query validation failed for method public abstract org.springframework.data.domain.Page study.datajpa.test.UserRepository.findUsersAndGameUsingFetchJoin(org.springframework.data.domain.Pageable)!; nested exception is java.lang.IllegalArgumentException: Count query validation failed for method public abstract org.springframework.data.domain.Page study.datajpa.test.UserRepository.findUsersAndGameUsingFetchJoin(org.springframework.data.domain.Pageable)!
...
Caused by: java.lang.IllegalArgumentException: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=null,role=study.datajpa.test.User.game,tableName=game,tableAlias=game1_,origin=user user0_,columns={user0_.game_id,className=study.datajpa.test.Game}}] [select count(u) from study.datajpa.test.User u join fetch u.game]
...
Caused by: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=null,role=study.datajpa.test.User.game,tableName=game,tableAlias=game1_,origin=user user0_,columns={user0_.game_id,className=study.datajpa.test.Game}}] [select count(u) from study.datajpa.test.User u join fetch u.game]
...

 

예외가 발생하는 이유는 이와 같다.

페이징을 하기 위해서는 전체 카운트가 꼭 있어야한다. 그래야 몇 page까지 있는지 알 수 있다.

그래서 countQuery가 없으면 Spring Data JPA가 임의로 원본 쿼리를 보고 countQuery를 작성하는데, 이때 쿼리에 페치 조인이 들어가게 된다.

페치 조인은 객체 그래프를 조회하는 기능이기 때문에 연관된 부모가 반드시 있어야하나, countQuery의 경우 count(u)로 조회 결과가 변경되었기 때문에 오류가 발생한 것이다.

따라서 fetch join이나 복잡한 쿼리의 경우 반드시 countQuery를 분리해서 사용하자.

 

이를 반영해 수정하면 다음과 같이 바꿀 수 있다.

    //유저 fetch join 게임 + 페이징 + 정렬
    @Query(value = "select u from User u join fetch u.game"
            , countQuery = "select count(u) from User u")
    Page<User> findUsersAndGameUsingFetchJoin(Pageable pageable);

벌크성 수정 쿼리

JPA에서 엔티티를 수정하려면 주로 변경 감지(Dirty Checking)을 이용한다. 하지만 만약 10만개의 엔티티를 한번에 변경해야 할 일이 있다고 해보자. 가령, 해가 지나 전 직원의 나이를 1씩 더해야 하는 상황이라면 10만건의 데이터를 조회 후, 각 데이터의 나이를 +1 해주면 영속성 컨텍스트에 의해 변경 감지가 일어나고 10만번의 UPDATE 처리가 일어난다. 이게 맞을까...? 그냥 'UPDATE MEMBER SET AGE = AGE + 1' 해주면 1번의 UPDATE 쿼리로 해결 되는 것인데...

 

그렇다, 몇 안되는 엔티티는 변경 감지로 수정을 하는 것이 바람직하나, 여러 데이터를 변경 감지로 수정하려면 너무 많은 리소스 낭비임이 분명하다. 그래서 JPA에서도 벌크(Bulk) 연산을 사용했다. Spring Data JPA에서도 존재한다.

/* JPA Bulk */
public int updateAll(){
    return em.createQuery("update User u set u.age = u.age + 1")
            .executeUpdate();
}

/* Spring Data JPA Bulk */
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Query("update User u set u.age = u.age + 1")
    int updateAll();
}

 

별로 다를 건 없다. 쿼리 메서드로 사용하는 것 말곤... 하지만 여기서 하나 다른점이 있다. 위를 그대로 동작하면 예외가 터진다. 다음 테스트를 돌려보자.

    @Test
    @DisplayName("벌크연산 테스트")
    public void 벌크연산_테스트() throws Exception {

        //given
        List<User> list = new ArrayList<>();
        list.add(new User("김", 10));
        list.add(new User("나", 20));
        list.add(new User("박", 30));
        list.add(new User("이", 10));
        list.add(new User("정", 20));
        list.add(new User("윤", 10));
        list.add(new User("강", 20));
        userRepository.saveAll(list);

        em.flush();
        em.clear();
        //when
        int count = userRepository.updateAll();

        //then
        assertThat(count).isEqualTo(7);
    }

 

뭐.. 대충 지원하지 않는 DML... select를 예상했는데 update가 나와서 고쳐주세요 라고 말하나보다. 그래서 해결 방안은 이건 조회가 아니라 데이터 수정입니다 말해주는 것이다.

 

@Modifying

을 붙여준다.

/* JPA Bulk */
public int updateAll(){
    return em.createQuery("update User u set u.age = u.age + 1")
            .executeUpdate();
}

/* Spring Data JPA Bulk */
public interface UserRepository extends JpaRepository<User, Long> {
    
    @Modifying
    @Query("update User u set u.age = u.age + 1")
    int updateAll();
}

 

아까 그 테스트를 돌려보면.. 7번의 update가 된다.

    update
        user 
    set
        age=age+1
        
    update
        user 
    set
        age=age+1
        
        ...
        
    update
        user 
    set
        age=age+1

 

자 이제 중요한 개념이 하나 등장한다. 벌크 연산은 영속성 컨텍스트를 무시하고 바로 데이터베이스에 적용해버리기 때문에 데이터 정합성이 깨질 수 있다. 

 

즉, 동일한 트랜잭션, 영속성 컨텍스트에서 벌크 연산을 하고 멤버의 나이를 조회하면 +1되기 이전의 값이 나온다. 이미 영속성 컨텍스트에 엔티티가 존재하기 때문에 그 엔티티를 그대로 반환하는 것이다. 이를 해결하기 위해서는 em.clear()를 호출해 벌크 연산 후 영속성 컨텍스트를 비워주는 작업이 필요하나, @Modifying(clearAutomatically = true)를 해주면 알아서 된다.

@EntityGraph

기존 JPA는 없던 EntityGraph라는 것이 Spring Data JPA에 등장한다. 이는 무엇을 해주는 녀석일까. 간단히 말하면 JPA의 fetch join을 간편하게, 쉽게 해주는 역할이다. fetch join이란 JPA에서 연관된 엔티티를 한번에 조인해서 조회할 수 있는 기능이다. 

 

EntityGraph를 알려면 먼저 fetch join과 LAZY 로딩, 그리고 그로 인한 문제를 알아야 한다. 대표적인 문제로 N + 1문제가 있다.

 

그럼 fetch join을 쓰면되지 왜 또 새로운 기능을 들고오는가? Spring Data JPA에서 쿼리 메서드는 위에 설명했듯,

1) 메서드 이름

2) NamedQuery

3) @Query(JPQL)

 

의 방법으로 사용할 수 있다. 그런데, 예를 들어 메서드 이름으로 쿼리를 만드는 상황을 가정해보자. 가령 findUsersBy()라는 메서드가 있고, User 목록을 조회하지만, 이 때 Game도 함께 조인해서 가져오고 싶다면 어떻게 해야할까? 그냥 JPQL을 새로 짜서 fetch join을 해도되지만, @EntityGraph(attributePaths = {"game"}) 어노테이션을 붙여주면 된다.

@EntityGraph(attributePaths = {"game"})
List<User> findUsersBy();
    @Test
    @DisplayName("엔티티 그래프 테스트")
    public void findUsersTest() throws Exception{

        //given
        Game game = Game.builder().name("game A").build();
        em.persist(game);
        //유저
        List<User> list = new ArrayList<>();
        list.add(new User("김", 10, game));
        list.add(new User("나", 20, game));
        list.add(new User("박", 30, game));
        list.add(new User("이", 10, game));
        list.add(new User("정", 20, game));
        list.add(new User("윤", 10, game));
        list.add(new User("강", 20, game));
        userRepository.saveAll(list);

        em.flush();
        em.clear();
        //when
        List<User> result = userRepository.findUsersBy();

        //then
        assertThat(result.size()).isEqualTo(7);
        //Game의 클래스 이름이 Proxy가 아니면 페치조인에 성공한 것이다.
        assertThat(result.get(0).getGame().getClass().getName()).isEqualTo(Game.class.getName());
    }
    select
        user0_.user_id as user_id1_3_0_,
        game1_.game_id as game_id1_0_1_,
        user0_.age as age2_3_0_,
        user0_.game_id as game_id4_3_0_,
        user0_.name as name3_3_0_,
        game1_.name as name2_0_1_ 
    from
        user user0_ 
    left outer join
        game game1_ 
            on user0_.game_id=game1_.game_id

 

즉, JPA에서는 직접 JPQL을 '모두' 작성해야 했지만, Spring Data JPA에서는 fetch join을 하고싶을 때 @EntityGraph로 쉽게 해결이 가능하다.

 

단, EntityGraph는 join의 형태(Inner/Outer)를 조절할 수 없으며 LEFT OUTER JOIN으로 실행되고, 복잡한 페치 조인을 사용해야할 때는 그냥 JPQL을 직접 작성해서 하는 것이 훨씬 간편하다.

 

# Reference

김영한님의 인프런 강의 - 실전! 스프링 데이터 JPA

 

실전! 스프링 데이터 JPA - 인프런 | 학습 페이지

지식을 나누면 반드시 나에게 돌아옵니다. 인프런을 통해 나의 지식에 가치를 부여하세요....

www.inflearn.com

반응형

'Framework > JPA (Hibernate)' 카테고리의 다른 글

[JPA] Querydsl 기본  (0) 2021.11.20
[JPA] OSIV (Open Session In View)  (2) 2021.09.19
[JPA] 벌크 연산 (Bulk Operation)  (0) 2021.08.05
[JPA] 페치 조인 (FETCH JOIN)  (0) 2021.08.05
[JPA] Cascade / OrphanRemoval  (0) 2021.07.25
Comments