
executeCommit(entityManager, () -> {
String jpql1 = "select c from Coffee c";
// 1. 객체 전체를 조회
// List resultList = entityManager.createQuery(jpql1).getResultList();
// 클래스 타입을 지정해주지 않으면 Object 타입으로 결과를 반환한다.
List<Coffee> coffeeList = entityManager.createQuery(jpql1, Coffee.class).getResultList();
// 2. 특정 필드만 조회
String jpql2 = "select c.name from Coffee c";
List<String> coffeeNameList = entityManager.createQuery(jpql2, String.class).getResultList();
// 스트링 타입으로 스트링 배열을 받을 수 있게 된다.
로그에 출력되는 SQL문을 확인해보면, SQL 쿼리 이전에 JPQL이 찍히는 것을 알 수 있다.
JPA를 이용해서 .find() 를 한다거나 하면 ActionQueue에 쌓아뒀다가 트랜잭션 커밋 시 SQL를 날리는데, 그 때 SQL를 바로 보내는 것이 아니라 JPQL을 만들어두고 이것을 SQL로 번역해서 보내는 것이다.
executeCommit(entityManager, () -> {
String jpql1 = "select c from Coffee c where c.name=:name";
String jpql2 = "select c from Coffee c where c.name= ?1";
TypedQuery<Coffee> query1 = entityManager.createQuery(jpql1, Coffee.class);
query1.setParameter("name", "Americano");
TypedQuery<Coffee> query2 = entityManager.createQuery(jpql2, Coffee.class);
query2.setParameter(1, "Americano");
Coffee americano1 = query1.getSingleResult();
// resultList.get(0);
Coffee americano2 = query2.getSingleResult();
jpql2처럼 위치 기반으로 바인딩해줄 수도 있다. .setParameter(...) 을 사용한다.createQuery는 Query를 반환한다. 특정 클래스 타입을 넣어줬을 경우, TypedQuery가 반환된다.getSingleResult()는 결과를 하나만 반환한다. 하지만, 결과가 하나도 없을 때는 예외가 발생한다. getResultList()는 결과 전체를 리스트로 반환한다. 결과가 하나도 없을 때 비어있는 리스트를 반환한다. null 이 담기는 것이다.executeCommit(entityManager, () -> {
/* 1번 방법
String jpql = "select c.name, c.price from Coffee c";
List<Object[]> resultList = entityManager.createQuery(jpql).getResultList();
for (Object[] row : resultList) {
String name = (String) row[0];
Integer price = (Integer) row[1];
log.info("name = {}", name);
log.info("price = {}", price);
CoffeeDto coffeeDto = new CoffeeDto(name, price);
log.info("coffeeDto = {}", coffeeDto);
}*/
// 2번 방법
String jpql = "select new io.silver.domain.eg4._1.CoffeeDto(c.name, c.price) from Coffee c";
List<CoffeeDto> resultList = entityManager.createQuery(jpql, CoffeeDto.class).getResultList();
String.class 이렇게 통일해서 받아올 수가 없다. CoffeeDto와 같은 필요한 컬럼만을 담고 있는 Data Transfer Object (DTO)를 생성해줄 수 있다. @Entity가 아니기 때문에 JPA가 자동으로 매핑해주지 못한다.Object[] 오브젝트 배열의 리스트로 받아올 수 있다. 하지만, 타입변환을 수동으로 해줘야 한다는 번거로움이 있다.
JPQL에서 직접 new 키워드로 DTO를 생성해줄 수 있다. new 키워드를 통해 생성자 기반 매핑을 해줘야 하고, 전체 경로를 다 지정해줘야 한다.
executeCommit(entityManager, () -> {
String jpql = "select c from Coffee c";
TypedQuery<Coffee> query = entityManager.createQuery(jpql, Coffee.class);
query.setFirstResult(5); // OFFSET
query.setMaxResults(5); // LIMIT
List<Coffee> resultList = query.getResultList();
assertThat(resultList.size()).isEqualTo(5);
});
OFFSET 설정으로 몇 번째 레코드부터 조회할지, LIMIT 설정으로 몇 개까지만 조회할지 설정할 수 있다.SQL에서는 쿼리문에서 지정해줄 수 있지만, JPQL에서는 .setFirstResult, .setMaxResults 메서드를 통해 OFFSET과 LIMIT를 정해줄 수 있다.@Slf4j
@Repository
@Transactional
@RequiredArgsConstructor
public class HibernateItemRepository {
private final EntityManager entityManager;
@Transactional 을 붙여주면 엔티티매니저에서 트랜잭션을 받아오고 시작한다. 이전 실습에서 getTransaction(), try/catch해서 커밋과 롤백을 진행한 이 반복되는 횡단관심사를 @Transactional 어노테이션이 대신 해주는 것이다. readOnly = true 라는 옵션으로 쓰기를 막아줄 수 있다. (조회만 가능)spring:
jpa:
properties:
hibernate:
show_sql: true
format_sql: true
hibernate:
ddl-auto: create
show_sql, format_sql를 통해 SQL구문이 전달되는 것을 로그에서 확인할 수 있다.Items item1 = Items.builder()
.itemCode(TestUtils.genRandomItemCode())
.price(TestUtils.genRandomPrice())
.build();
Items saved = repository.save(item1);
assertThat(saved.getId()).isNotNull();
Id 값을 넣어주지 않았지만, persist -> commit (영속성 컨텍스트에서 엔티티 인스턴스로 영속화되어 있다가 커밋)될 때 Id가 자동으로 들어가게 된다.Items item2 = Items.builder()
.price(TestUtils.genRandomPrice())
.build();
assertThatThrownBy(
() -> {
repository.save(item2);
}
).isInstanceOf(DataIntegrityViolationException.class);
nullable=false인 itemCode를 빼고 객체를 생성하고 repository.save(item2)를 실행하면 INSERT문을 날리는 것을 로그에서 확인할 수 있다.persist에서는 문제가 없었고, Commit할 때 DB의 NOT NULL 제약조건 때문에 오류가 난 것이다. persist) 시점에는 null이어도 문제가 없다. 여기서 에러는 DB 스키마에 설정된 NOT NULL 제약조건 때문에 오류가 나는 것이다. 그렇기 때문에 @Column에 설정해 둔 nullable, unique 제약조건은 유효성 검사가 아니다. Q. 그렇다면 nullable, unique 설정은 왜 해주는 걸까?
A. @Column(nullable = false)는 JPA 수준의 힌트로, 주로 DDL 생성 시점에 사용된다. DDL 생성 시 DB 컬럼에 NOT NULL 제약을 걸도록 유도하는 메타정보인 것이다.
public List<Items> saveAll(List<Items> items) {
// 1번 방법
// for (Items item : items) {
// entityManager.persist(item);
// }
// 2반 빙밥 : buffer
int batchSize = 50;
for( int i=0; i < items.size(); i++){
entityManager.persist(items.get(i));
if(i % batchSize == 0 && i > 0){
entityManager.flush(); // actionQueue에 있는 거 DB에 반영시키는 것임.
entityManager.clear();
}
}
entityManager.flush();
entityManager.clear();
return items;
}
clear()도 안 했기 때문에 1차 캐시에 엔티티가 계속 남아있어서 GC도 못하게 된다.batchSize를 정해두어 쿼리가 batchSize만큼 쌓이면 커밋하겠다고 설정하는 것이다.batchSize만큼 쌓였을 때, persist()를 하고, flush()와 clear() 도 해준다.flush()를 안 하면 메모리에 엔티티가 너무 많이 쌓여서 엔티티 컨텍스트에 과부하가 온다. 그래서 flush()로 쿼리를 DB에 전송해주고, clear()로 캐시까지 비우는 것이다.public interface DataJpaItemRepository extends JpaRepository<Items, Long> {
// Query Method
Optional<Items> findByItemCode(String itemCode);
}
JpaRepository 를 상속받아주면 된다. 그렇게 되면, Spring Data JPA가 이 인터페이스를 보고, findAll, save, delete 등 CRUD 메서드 구현체를 자동으로 생성해준다 ! 또, 이 구현체는 런타임 시 Spring Bean으로 등록되어 @Autowired 등으로 주입이 가능해진다.
기본 구현해주는 메서드를 제외하고 다른 메서드를 구현하고 싶을 때 (PK가 아닌 UNIQUE 키로 데이터를 조회하고 싶을 때 등) Query Method를 이용할 수 있다.
select i from Items i where i.itemCode = :itemCode와 같은 쿼리를 사용해서 결과를 받아오고, 반환타입도 바꿔서 Optional로 쓸 수 있게 되는 것이다. // DataJpaOrderRepository.java
@Query("select o from Orders o where o.orderCode=:orderCode")
Optional<Orders> findByOrderCode(String orderCode);
// DataJpaOrderItemsRepository.java
@Query("select oi from OrderItems oi where oi.orders.orderCode = :orderCode")
List<OrderItems> findAllByOrderCode(String orderCode);
위와 같이, @Query 어노테이션을 사용해 JPQL을 직접 작성해줄 수도 있다. 쿼리 메서드의 매개변수와 JPQL의 :파라미터명을 일치시켜줘야 자동 바인딩돼서 DB에 전달한다.
또, 두 번째의 경우 oi.orders.orderCode처럼 엔티티 간의 연관관계를 따라 들어갈 수 있다. JPA에서는 이렇게 객체 그래프 탐색을 통해 엔티티의 필드도 쿼리에서 자유롭게 사용할 수 있다.
@Repository
@RequiredArgsConstructor
public class DataJpaOrderRepositoryCombine {
private final DataJpaOrderRepository orderRepository;
private final DataJpaOrderItemsRepository orderItemsRepository;
Order 과 OrderItems는 생명주기가 같다. 그래서 Combine된 Repository class를 만들어서 각 레포지토리를 주입받아 사용한다. @Transactional을 걸어야 한다.@Transactional을 가지고 있다. 또, 테스트가 끝나면 자동 롤백되어 DB가 원상복구된다.QueryDSL은 정적 타입을 이용한 Query생성에 특화된 언어(Domain Specific Language)의 특징을 갖는 라이브러리이다.
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

clean -> build 를 수행한다.
Q클래스란
QueryDSL을 사용할 때, Entity 기반으로 자동 생성되는 클래스이다. Q 클래스는 Entity 필드들을 타입 안전하게 쿼리로 작성할 수 있도록 도와주는 DSL 객체이다. 즉, 문자열로 JPQL을 쓰는 대신, Q클래스의 필드와 메서드를 통해 자바 코드로 쿼리를 작성하는 것이다.
@Configuration
public class QueryDslConfig {
@Bean
public JPAQueryFactory queryFactory(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
}
JPAQueryFactory는 QueryDSL에서 사용하는 쿼리 생성 객체이다. JPAQueryFactory는 내부적으로 이 EntityManager를 사용해서 쿼리를 실행한다.@Slf4j
@Repository
@Transactional
@RequiredArgsConstructor
public class QueryDslItemRepository {
private final JPAQueryFactory queryFactory;
public Optional<Items> findByItemCode(String itemCode) {
Items findItem = queryFactory.selectFrom(items)
.where(items.itemCode.eq(itemCode))
.fetchFirst();// 오류가 안 남. 없으면 Null 1개만 리턴
return Optional.ofNullable(findItem);
}
}
selectFrom 로 대상 객체를 정해준다. where로 조건을 지정한다..fetchFirst() 로 결과를 1개만 반환하도록 한다. QueryDSL은 엔티티가 달라지면 clean() -> build()를 다시 해야 한다.
또한, QueryDSL은 조회에 특화되어 있는 기술이기 때문에, 따로 저장이나 수정, 삭제 작업을 원한다면 EntityManager을 사용해야 한다.
오늘은 굉장히 무난하게 ! (중간에 헷갈리는 부분들은 있었지만) 재미있게 ! 수업을 들었다. 어제 N시간동안 스터디 정리한 게 파사삭 날라간 이슈로 다시 다하고 새벽 4시에 잤더니 살짝 피곤했다. 근데 잠은 안오고 그냥 기운이 좀 없었다. 그런 거치고 집중은 되게 잘됐어서 다행이다..
아니 세상에 오늘 TIL 적는데 뭐라고 벌써 day30 TIL이라고..? 와우.. 대박이다잉.. = 이 프로그램의 30%는 했다는 건데.. 짱 신기 짱 뿌듯하다 ! ˃̵͈̑ᴗ˂̵͈̑ 그리고~~Spring Data JPA까지 진도가 다 나갔다!!! 이것도 너무 신기해... 큰 챕터로 보자면 이제 Spring Security밖에 안 남은 거잖아. 이게 맞냐구. 대박이야 어머어머
근데 나 보안쪽으로는 아는 게 더더욱 없는데.. 또 어렵겠지..? ㅎ.. ㅎ 와아.. ㅎ 그렇지만 모르는만큼 겁없이 들어주마 덤벼. 막이래
내일도 힘내서 해보자구요 파이팅 !!! 30일 수고했다 !!!