일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Diary 해우소
- #스파르타내일배움캠프TIL
- 회고록
- Github_token
- KPT
- TiL_1st_0419
- 클래스
- 스레드
- Java
- 객체지향 언어
- #스파르타내일배움캠프
- 생성자
- Java의 이점
- 성장기록
- 인스턴스
- diary
- JVM
- 스파르타내일배움캠프TIL
- 내일배움캠프
- 메서드
- #내일배움캠프
- 포맷은 최후의 보루
- 스파르타내일배움캠프
- static
- 해우소
- 변수의 다양성
- 감사기록
- GitHub
- Token
- Git
- Today
- Total
몬그로이
Spring Data JPA 본문
Cascade (영속성 전이)
- 사용 조건
- 양쪽 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
예를들어, 게시글이 삭제되면 첨부파일도 같이 삭제 되어야 한다. - 대상 엔티티로의 영속성 전이는 현재 엔티티에서만 전이 되어야 한다. (다른곳에서 또 걸면 안됨)
예를들어, 첨부파일을 게시글이 아닌 다른곳에서 영속성 전이를 하면 안된다.
- 양쪽 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
- 옵션 종류
- ALL : 전체 상태 전이
- PERSIST : 저장 상태 전이
- REMOVE : 삭제 상태 전이
- MERGE : 업데이트 상태 전이
- REFERESH : 갱신 상태 전이
- DETACH : 비영속성 상태 전이
orphanRemoval (고아 객체 제거)
- 사용법
- Cascade.REMOVE 와 비슷한 용도로 삭제를 전파하는데 쓰인다.
- 부모 객체에서 리스트 요소삭제를 했을경우 해당 자식 객체는 매핑정보가 없어지므로 대신 삭제해준다.
요건 DB 에서는 절대 알 수 없는 행동이다. (부모가 자식의 손을 놓고 버리고 간 고아 객체)
Cascade.REMOVE 와 orphanRemoval 차이점?
Cascade.REMOVE의 경우 일에 해당하는 부모 엔티티를 em.remove를 통해 직접 삭제할 때,
그 아래에 있는 다에 해당하는 자식 엔티티들이 삭제되는 것입니다.
orphanRemoval=true는 위 케이스도 포함하며,
일에 해당하는 부모 엔티티의 리스트에서 요소를 삭제하기만 해도
해당 다에 해당하는 자식 엔티티가 delete되는 기능까지 포함하고 있다.
즉, orphanRemoval=true 는 리스트 요소로써의 영속성 전이도 해준다는 뜻
영속성 전이 최강 조합 : orphanRemoval=true + Cascade.ALL
위 2개를 함께 설정하면 자식 엔티티의 라이프 사이클이 부모 엔티티와 동일해지며, 직접 자식 엔티티의 생명주기를 관리할 수 있게 되므로 자식 엔티티의 Repository 조차 없어도 된다. (따라서, 매핑 테이블에서 많이 쓰임)
Fetch (조회시점)
- 사용 위치
- Entity 에 FetchType 으로 설정할 수 있다
@ElementCollection, @ManyToMany, @OneToMany, @ManyToOne, @OneToOne - Query 수행시 fetch Join 을 통해서 LAZY 인 경우도 즉시 불러올 수 있다.
- Entity 에 FetchType 으로 설정할 수 있다
- 사용법
- 기본 LAZY를 설정한 뒤에 필요할때만 fetch Join 을 수행한다.
- 항상 같이 쓰이는 연관관계 일 경우만 EAGER 를 설정한다.
- 옵션(FetchType)
- EAGER : 즉시 로딩 (부모 조회 시 자식도 같이 조회)
- LAZY : 지연 로딩 (자식은 필요할때 따로 조회)
Raw JPA 작동 방식
persist(), merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태)
> commit() > (DB에 쿼리가 반영된 상태)
Raw JPA 작동 방식으로 인해
SpringDataJpa 사용시 고려할 점
*SpringDataJpa 는 Raw JPA를 추상화한 것
1. @Transactional
메서드1 : save()
- persist() 까지 수행하고 (업데이트일 경우 merge() )
- save() 메소드에 달린 내부적인 @Transactional 어노테이션에 의해 flush, commit 까지 수행됩니다.
메서드2 : saveAndFlush()
- persist(), flush() 까지 수행하고
- saveAndFlush() 메소드에 달린 내부적인 @Transactional 어노테이션에 의해 commit() 까지 수행됩니다.
이는 Repository 기본 구현체인 SimpleRepository 클래스의 save 메소드에
@Transactional 어노테이션이 달려있기 때문이므로
flush 와 commit 사이 즉, DB에 쿼리가 전송된 상태에서 추가동작이 필요한경우에는
SimpleRepository 대신 Repository 를 개별로 구현하여
@Transactional 이 달리지 않은 saveAndFlush() 를 구현할 것
2. 지연 로딩(LAZY)를 우선적으로 사용한다.
- N+1 에러 방지를 위해 지연 로딩 (LAZY) 모드로 사용을 하고
- 성능 최적화가 필요한 부분에서는 Fetch 조인을 사용한다.
N+1 문제란?
특정 엔티티를 조회할 때 연관된 엔티티를 추가로 조회하면서 발생하는 비효율적인 쿼리가 실행되는 것
10명의 Member 엔티티가 데이터베이스에 있다고 가정
첫 번째 쿼리로 10명의 Member 엔티티를 가져옴 (LAZY 설정으로 인해 team 정보 가져오지 않음)
for 문 등으로 따로 각 member에 대한 team 을 조회하려고 한다면
10명의 Member 엔티티에 대해 각각의 Team 엔티티를 조회하기 위해 10번의 쿼리가 추가로 실행되어야만 함
따라서, 총 1 + 10 = 11개의 쿼리가 실행
따라서 LAZY 가 설정되어 있을때 각 member 에 대한 팀 조회까지 필요한 경우 fetchJoin을 이용할 것
3. OneToMany 매핑은 양방향으로 해준다.
- 단방향으로 매핑하면 자식 엔티티가 관리하는 외래 키가 다른 테이블에 있음
=> 작업한 Entity가 아닌 다른 Entity에서 쿼리문이 나가는 경우가 있어 헷갈림
=> 양방향으로 하면 외래 키가 같은 테이블에 생김 - 단방향으로 매핑하면 불필요한 쿼리문이 발생(update 등)
=> 양방향으로 하면 연관관계 정보를 통해 한번에 쿼리 수행
4. save() 이후에는 반환된 인스턴스를 사용해야 한다.
- save 할때 넘겨준 객체는 save 이후부터는 EntityManager가 관리하고 있는 대상이 아니기 때문에(준영속성 상태)
이 객체의 값을 바꿔봐야 쿼리에 영향을 미치지 않는다. - save 메소드 응답값으로 오는 객체가 영속성 상태의 객체이니 이것을 사용해야 한다.
5. 테스트 코드에서 @Transactional 을 사용할때는 주의해야한다.
- 실제 런타임시에는 @Transactional 이 적용되지 않아서 테스트와 다른 환경에서 다른 동작이 발생할 수 있음
- 따라서, @Transactional 이 테스트 대상 환경일 경우만 적용하여 사용한다.
JpaRepository 쿼리 기능
1. Repository ~ JpaRepository 까지는 @NotRepositoryBean 이 붙어있는 인터페이스
2. JpaRepository<Entity,ID> 붙이면 알맞은 프로그래밍 된 SimpleJpaReository 구현체 빈이 등록된다.
*@EnableJpaRepositories 의 JpaRepositoriesRegistrar 를 통해서 빈이 등록된다.
(@EnableJpaRepositories 는 @SpringBootApplication 을 통해 자동으로 붙는 어노테이션)
(JpaRepositoriesRegistrar 는 ImportBeanDefinitionRegistrar 의 구현체)
(ImportBeanDefinitionRegistrar 는 프로그래밍을 통해 빈을 주입해줌)
ImportBeanDefinitionRegistrar 를 사용해 직접 프로그래밍 해보기
// MyRepository.java
@Setter
public class MyRepository {
private HashMap<Long, String> dataTable; // DB 테이블을 의미
public String find(Long id) {
return dataTable.getOrDefault(id, "");
}
public Long save(String data) {
var newId = Long.valueOf(dataTable.size());
this.dataTable.put(newId, data);
return newId;
}
}
// MyRepositoryRegistrar.java
public class MyRepositoryRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
// 주입할 빈에 대해 프로그래밍 하는 부분!!
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(MyRepository.class);
beanDefinition.getPropertyValues().add("dataTable", Map.of(1L, "data"));
// 여기까지!
registry.registerBeanDefinition("myRepository", beanDefinition);
}
}
// TestApplication.java
@Import(MyRepositoryRegistrar.class) // 빈 주입!
@SpringBootTest
public class MyRepositoryTest {
@Autowired
MyRepository myRepository;
@Test
void myRepositoryTest() {
// given
var newData = "NEW DATA";
var savedId = myRepository.save(newData);
// when
var newDataList = myRepository.find(savedId);
// then
System.out.println(newDataList);
}
}
JpaRepository 쿼리 사용법
규칙
리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)[(And|Or){프로퍼티 표현식}(조건식)](OrderBy{프로퍼티}Asc|Desc) (매개변수...)
접두어 | Find, Get, Query, Count, .. |
도입부 | Distinct, First(N), Top(N) |
프로퍼티 표현식 | Person.Address.ZipCode -> find(Person)ByAddress_ZipCode(...) |
조건식 | IgnoreCase, Between, LessThan, GreaterThan, Like, Contains, ... |
정렬조건 | OrderBy(프로퍼티)Asc|Desc |
리턴 타입 | E, Optional<E>, List<E>, Slice<E>, Stream<E> |
매개변수 | Pageable, Sort |
// 페이징
Page<User> findByName(String name, Pageable pageable); // Page 는 카운트쿼리 수행됨
Slice<User> findByName(String name, Pageable pageable); // Slice 는 카운트쿼리 수행안됨
List<User> findByName(String name, Sort sort);
List<User> findByName(String name, Pageable pageable);
Pageable vs. Sorting
Pageable
- Pageable 인터페이스를 구현한 PageRequest 객체를 만들거나 얻습니다.
- PageRequest 객체를 repository 메소드에 우리가 의도한대로 인자로 전달합니다.
- PageRequest 객체는 요청된 페이지 숫자와 페이지 사이즈를 넘김으로서 만듭니다.
(페이지 숫자는 0부터 시작)
// 첫 페이지 (페이지 사이즈 = 2)
Pageable firstPageWithTwoElements = PageRequest.of(0, 2);
// 두번째 페이지 (페이지 사이즈 = 5)
Pageable secondPageWithFiveElements = PageRequest.of(1, 5);
// 페이지 사용
List<Product> allTenDollarProducts = productRepository.findAllByPrice(10, secondPageWithFiveElements);
- findAll(Pageable pageable) 메소드는 기본적으로 Page 객체를 리턴합니다.
- 그러나, 우리는 또한 커스텀 메소드를 통해 페이지네이션된 데이터를
Page , Slice 또는 List 의 타입으로 받을 수 있습니다. - Page 인스턴스는 Product 의 목록 뿐 아니라 페이징할 수 있는 전체 목록의 숫자도 알고 있습니다.
이를 실행하기 위해 추가적으로 쿼리 작업이 들어갑니다. - 이러한 작업에 대한 비용을 방지하기 위해, 우리는 대신 Slice나 List로 반환 받을 수 있습니다.
Slice 는 단지 다음 slice가 가능한지 아닌지만 알고 있습니다.
Sorting
- 유사하게, 우리의 쿼리 결과를 정렬하기 위해선 Sort 객체를 메소드에 전달하면 됩니다.
- 만약, 정렬과 페이지네이션을 둘다 하고 싶다면
정렬에 대한 디테일 정보를 PageRequest 객체에 전달하면 됩니다. - 페이징 없이 정렬만 하려는 경우
위에 언급한 findAll(Sort sort) 메서드와 같이 Sort 객체만 파라미터로하는 메서드를 작성하면 된다.
Pageable sortedByName = PageRequest.of(0, 3, Sort.by("name"));
Pageable sortedByPriceDesc = PageRequest.of(0, 3, Sort.by("price").descending());
Pageable sortedByPriceDescNameAsc = PageRequest.of(0, 5, Sort.by("price").descending().and(Sort.by("name")));
Page<Product> allProductsSortedByName = productRepository.findAll(Sort.by("name").accending());
JpaRepository 효율적 사용법
Optional 제거하기
Spring Data JPA의 findByXX 메서드는 기본적으로 Optional을 반환한다. 이로 인해 비즈니스 로직에서 Optional 처리를 위한 추가적인 작업이 필요하게 되는데, 이럴 때 default 메서드를 활용하면 이 문제를 우아하게 해결할 수 있다.
public interface UserRepository extends JpaRepository<User, Long> {
// Default 메소드를 사용하여 findById의 Optional을 내부적으로 처리
default User findUserById(Long id) {
return findById(id).orElseThrow(() -> new DataNotFoundException("User not found with id: " + id));
}
}
메서드명 간소화하기
Spring Data JPA를 사용하다 보면 복잡한 쿼리 때문에 메서드명이 길어져 가독성을 해치는 경우가 있다. 이럴 때도 default 메서드를 활용하면 긴 메서드명을 간결하고 명확하게 표현할 수 있다.
public interface ProductRepository extends JpaRepository<Product, Long> {
// 기존의 긴 쿼리 메소드
List<Product> findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(String category, BigDecimal minPrice, BigDecimal maxPrice);
// Default 메소드를 사용하여 간결한 메소드명 제공
default List<Product> findProductsByCategoryAndPriceRange(String category, BigDecimal minPrice, BigDecimal maxPrice) {
return findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(category, minPrice, maxPrice);
}
}
비즈니스 로직 통합
여러 기본 제공 메서드를 하나의 고차 작업으로 결합할 수도 있다. 다만 Spring Data JPA의 Repository는 Data Access Layer의 일부로, 데이터베이스와의 상호작용만을 담당하는 것이 일반적이기 때문에 이 부분은 서비스 레이어에서 처리하는 것이 일반적이다.
public interface UserRepository extends JpaRepository<User, Long> {
// 사용자 ID로 사용자를 찾고, 존재할 경우 연락처 정보를 업데이트하는 메소드
default void updateUserContact(Long userId, String newContact) {
findById(userId).ifPresent(user -> {
user.setContact(newContact);
save(user);
});
}
}
'Organizing Docs > Java Docs' 카테고리의 다른 글
JPQL - 복잡한 쿼리를 수동으로 작성하고 실행하는 방법 (0) | 2024.06.30 |
---|---|
ORM (0) | 2024.06.28 |
페이징 (0) | 2024.06.28 |
MyBatis (0) | 2024.06.27 |
데이터베이스Driver >> JDBCTemplate (0) | 2024.06.26 |