Reference. 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online
이전 글
더보기
- 1. @Query 사용하기 -
JPA Repository 조건에 문제점
- JPA Repository에 메소드로 조건을 여러개 설정하는 경우 이름 길어지는 단점이 발생
BookRepository.java
@Repository public interface BookRepository extends JpaRepository<Book, Long> { ... List<Book> findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(String name, LocalDateTime createdAt, LocalDateTime updatedAt); }
BookRepositoryTest.java
@SpringBootTest class BookRepositoryTest { ... @Test void queryTest(){ System.out.println("findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual : " + bookRepository.findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual( "JPA 초격자 패키지", LocalDateTime.now().minusDays(1L), LocalDateTime.now().minusDays(1L) )); } }
날짜 관련 정보 및 default 설정 (data.sql)
BaseEntity.java
에data.sql
을 통해created_at
,updated_at
컬럼 값 넣는 방법data.sql
에created_at
,updated_at
값이 null로 저장 (JPA save EntityListener로 저장)
insert into book(`id`, `name`, `publisher_id`, `deleted`) values(1, 'JPA 초격자 패키지', 1, false); insert into book(`id`, `name`, `publisher_id`, `deleted`) values(2, 'Spring', 1, false); insert into book(`id`, `name`, `publisher_id`, `deleted`) values(3, 'Spring Security', 1, true);
- 필수 입력 값을 넣는 2가지 방법
BaseEntity
에@Column(nullable = false)
를 통해 필수 입력 값으로 체크하도록 수정
data.sql
쿼리 수정 (`created_at`, `updated_at`
)- 매번
created_at
,updated_at
을 추가해줘야하는 불편함 발생
insert into book(`id`, `name`, `publisher_id`, `created_at`, `updated_at`) values(1, 'JPA 초격자 패키지', 1, false, now(), now()); insert into book(`id`, `name`, `publisher_id`, `created_at`, `updated_at`) values(2, 'Spring', 1, false, now(), now()); insert into book(`id`, `name`, `publisher_id`, `created_at`, `updated_at`) values(3, 'Spring Security', 1, true, now(), now());
- 매번
@Column
속성에columnDefinition
이용 (현업은AutoDDL
을 안쓰므로 잘 안쓰는 속성)@Data @MappedSuperclass @EntityListeners(value = AuditingEntityListener.class) public class BaseEntity implements Auditable { @CreatedDate @Column(columnDefinition = "datetime(6) default now(6)", nullable = false, updatable = false) private LocalDateTime createdAt; @LastModifiedDate @Column(columnDefinition = "datetime(6) default now(6)", nullable = false) private LocalDateTime updatedAt; }
columnDefinition
은 지정 값으로 표시 이후 설정 값은 이어서 표시 (타입 삭제 됌)//columnDefinition = "default now(6)" created_at default now(6) not null, updated_at default now(6) not null, //@Column(columnDefinition = "datetime(6) default now(6) null", nullable = false, updatable = false) created_at datetime(6) default now(6) null not null, //@Column(columnDefinition = "datetime(6) default now(6) comment '수정시간'", nullable = false) updated_at datetime(6) default now(6) comment '수정시간' not null,
Ejb3Column.java (@Column 구현체)
//550라인 (sqlType을 치환) if ( col.columnDefinition().isEmpty() ) { sqlType = null; } else { sqlType = normalizer.applyGlobalQuoting( col.columnDefinition() ); }
MySQL
에 일시를 표시하는 함수now()
,current_timestamp()
now(3)
,now(6)
은 밀리세컨즈 자릿수를 의미 (최대: 6)
SELECT now(), now(3), now(6), current_timestamp(); now() : 2021-08-11 23:57:04, now(3) : 2021-08-11 23:57:04.103, now(6) : 2021-08-11 23:57:04.103821, current_timestamp() : 2021-08-11 23:57:04
1. JPQL을 사용해 긴 메소드 문제 해결
BookRepositoryTest.java
@Test
void queryTest(){
List<Book> findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual(String name, LocalDateTime createdAt, LocalDateTime updatedAt);
@Query(value = "select b from Book b "
+ "where name = ?1 and createdAt >= ?2 and updatedAt >= ?3 and category is null")
List<Book> findByNameRecently(String name, LocalDateTime createdAt, LocalDateTime updatedAt);
}
//---------------------------------------------------------------
//동일한 결과 출력
findByCategoryIsNullAndNameEqualsAndCreatedAtGreaterThanEqualAndUpdatedAtGreaterThanEqual : [Book(super=BaseEntity(createdAt=2021-08-13T05:41:42.728762, updatedAt=2021-08-13T05:41:42.728762), id=1, name=JPA 초격자 패키지, category=null, deleted=false)]
findByNameRecently : [Book(super=BaseEntity(createdAt=2021-08-13T05:41:42.728762, updatedAt=2021-08-13T05:41:42.728762), id=1, name=JPA 초격자 패키지, category=null, deleted=false)]
- @Query에 사용된 쿼리 문법을 JPQL이라고 부름 (데이터베이스 쿼리 X)
- JPA 엔티티를 기반으로 하는 쿼리
- Book은 데이터베이스가 아닌 엔티티를 사용
- JPQL에 쿼리는 Dialect(방언)을 통해 데이터베이스별 쿼리가 생성
- JPQL에 동적으로 파라미터를 설정하는 방법 2가지
- 물음표와 숫자 기반에 파라미터 매핑 (
ORDINAL
)- Java에서는 순서의 의존성을 가진 파라미터는 지양 (파라미터 순서가 바뀌면 결과 변경)
- 예) ?1, ?2, ?3와 같이 파라미터 입력 순서로 지정
//순차적일 필요는 없음 //아래와 같은 쿼리도 가능(createdAt, updatedAt, name순으로 할당) //@Query(value = "select b from Book b " // + "where name = ?2 and createdAt >= ?3 and updatedAt >= ?1 and category is null") @Query(value = "select b from Book b " + "where name = ?1 and createdAt >= ?2 and updatedAt >= ?3 and category is null") List<Book> findByNameRecently(String name, LocalDateTime createdAt, LocalDateTime updatedAt);
- 네임 기반에 파라미터 매핑
@Param
과:
을 사용하여 값을 매핑
- 순서와 상관이 없어 파라미터의 변경여부와 상관없이 결과값 동일
- 예)
@Param("name")
로 선언 된 파라미터와:name
이 연결 상태
@Query(value = "select b from Book b " + "where name = :name and createdAt >= :createdAt and updatedAt >= :updatedAt and category is null") List<Book> findByNameRecently( @Param("name") String name, @Param("createdAt") LocalDateTime createdAt, @Param("updatedAt") LocalDateTime updatedAt);
- 물음표와 숫자 기반에 파라미터 매핑 (
2. 엔티티에 연결되지 않은 상태에서 쿼리 조회
- 엔티티에 복잡한 값을 모두 가져오지 않고 필요한 값만 조회하고 싶은 경우 사용
Interface
,DTO
,Tuple
사용
2.1 Tuple 사용
BookRepository.java
@Query(value = "select b.name as name, b.category as category from Book b")
List<Tuple> findBookNameAndCategory();
BookRepositoryTest.java
bookRepository.findBookNameAndCategory().forEach(tuple -> {
System.out.println(tuple.get(0) + " : " + tuple.get(1));
});
2.2 Interface 사용
BookRepository.java
@Query(value = "select b.name as name, b.category as category from Book b")
List<BookNameAndCategory> findBookNameAndCategory();
BooknameAndCategory.java
public interface BookNameAndCategory {
String getName();
String getCategory();
}
BookRepositoryTest.java
bookRepository.findBookNameAndCategory().forEach(b -> {
System.out.println(b.getName() + " : " + b.getCategory());
});
2.3 DTO 사용
BookRepository.java
@Query(value = "select new com.example.bookmanager.repository.dto.BookNameAndCategory(b.name, b.category) from Book b")
List<BookNameAndCategory> findBookNameAndCategory();
BookNameAndCategory.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BookNameAndCategory {
private String name;
private String category;
}
BookRepositoryTest.java
bookRepository.findBookNameAndCategory().forEach(b -> {
System.out.println(b.getName() + " : " + b.getCategory());
});
3. JPQL을 사용해서 Paging처리
Repository
에findAll()
과 같은 기능을 제공 (Pagable
파라미터 필요)
BookRepository.java
@Query(value = "select new com.example.bookmanager.repository.dto.BookNameAndCategory(b.name, b.category) from Book b")
List<BookNameAndCategory> findBookNameAndCategory(Pageable pageable);
BookRepositoryTest.java
/*
select 조회 결과
JPA 초격자 패키지 : null
Spring : null
*/
bookRepository.findBookNameAndCategory(PageRequest.of(1, 1)).forEach(
bookNameAndCategory -> System.out.println(bookNameAndCategory.getName() + " : " + bookNameAndCategory.getCategory()));
//결과: Spring : null
bookRepository.findBookNameAndCategory(PageRequest.of(0, 1)).forEach(
bookNameAndCategory -> System.out.println(bookNameAndCategory.getName() + " : " + bookNameAndCategory.getCategory()));
//결과: JPA 초격자 패키지 : null
- 2. Native 쿼리 사용 -
@Query
어노테이션에서nativeQuery
속성을true
로 설정하면 사용 가능
JPQL
쿼리와 다르게Entity
속성을 사용할 수 없음
- JPA에
Dialect
(방언)을 사용할 수 없음
- 테스트(
h2
)와 운영(MySQL
)에서 이종 데이터베이스를 사용할 경우 문제 발생
BookRepository.java
@Query(value = "select * from book", nativeQuery = true)
List<Book> findAllCustom();
BookRepositoryTest.java
@Test
void nativeQueryTest() {
bookRepository.findAll().forEach(System.out::println); // JPA
bookRepository.findAllCustom().forEach(System.out::println); //native Query
}
실행결과
//JPA Repositry 사용
//쿼리에 alias가 자동생성
//Entity 설정에 Where 설정으로 인해 조건이 생성
Hibernate:
select
book0_.id as id1_2_,
book0_.created_at as created_2_2_,
book0_.updated_at as updated_3_2_,
book0_.category as category4_2_,
book0_.deleted as deleted5_2_,
book0_.name as name6_2_,
book0_.publisher_id as publishe7_2_
from
book book0_
where
(
book0_.deleted = 0
)
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.941847, updatedAt=2021-08-14T08:34:13.941847), id=1, name=JPA 초격자 패키지, category=null, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.948709, updatedAt=2021-08-14T08:34:13.948709), id=2, name=Spring, category=null, deleted=false)
//native Query 사용
//작성한 native Query만 실행
Hibernate:
select
*
from
book
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.941847, updatedAt=2021-08-14T08:34:13.941847), id=1, name=JPA 초격자 패키지, category=null, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.948709, updatedAt=2021-08-14T08:34:13.948709), id=2, name=Spring, category=null, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T08:34:13.954006, updatedAt=2021-08-14T08:34:13.954006), id=3, name=Spring Security, category=null, deleted=true)
Native 쿼리를 사용하는 이유
1. 성능에 대한 문제를 해결하는데 이용 (대표적으로 update
)
delete
는 한번에 삭제하는 메소드를 제공deleteAllInBatch
: 조건 없는 삭제 (delete from book;
)
deleteInBatch
:findAll
한 후 레코드 하나하나를 id값으로delete
update
는 한개씩 처리하는 방법만 제공 (saveAll
도findAll
후id
값으로 수정)
BookRepository.java
JPA Repository
를 사용하지 않으므로insert
,update
,delete
에는@Transactional
이 필요@Transactional
은interface
보다구체 클레스
에서 사용하기를 권고interface
는interface-base-proxies
경우에 사용 (JpaRepository
는proxies
)
Repository
에 사용하지 않은 경우 각 클레스마다 설정해야하는 불편함과 문제 발생
@Modifying
을 통해insert
,update
,delete
임을 명시하고 실행 결과를 int로 반환 가능
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
...
@Transactional
@Modifying
@Query(value = "update book set category = 'IT전문서'", nativeQuery = true)
int updateCategories();
}
BookRepositoryTest.java
@Test
void nativeQueryTest() {
System.out.println("affected rows : " + bookRepository.updateCategories());
bookRepository.findAllCustom().forEach(System.out::println);
}
실행결과
//bookRepository.updateCategories()
Hibernate:
update
book
set
category = 'IT전문서'
affected rows : 3
//bookRepository.findAllCustom()
Hibernate:
select
*
from
book
Book(super=BaseEntity(createdAt=2021-08-14T09:05:52.431619, updatedAt=2021-08-14T09:05:52.431619), id=1, name=JPA 초격자 패키지, category=IT전문서, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T09:05:52.438086, updatedAt=2021-08-14T09:05:52.438086), id=2, name=Spring, category=IT전문서, deleted=false)
Book(super=BaseEntity(createdAt=2021-08-14T09:05:52.442247, updatedAt=2021-08-14T09:05:52.442247), id=3, name=Spring Security, category=IT전문서, deleted=true)
2. JPA에서 제공하지 않는 기능을 사용하는 경우
show tables;
,show databases;
과 같은 기능은 JPA에서 제공하지 않음BookRepository.java
@Query(value = "show tables", nativeQuery = true) List<String> showTables();
BookRepositoryTest.java
@Test void nativeQueryTest() { System.out.println(bookRepository.showTables()); } //실행결과 Hibernate: show tables [address, author, book, book_and_author, book_review_info, publisher, review, user, user_history]
- 3. Converter 사용 -
- JPA ORM은 일종에 interface
- 데이터베이스의 레코드를 자바에 객체화 시켜주는 역할
자바의 객체화와 DB 데이터의 형식이 다른 경우
- JPA에서 Enum을 사용하는 경우
Convert
를 사용OrdinalEnumValueConverter
구현체를 통해JPA는 Enum
,DB엔 Integer
로 표현
Enum
,인베디드 데이터
를 못쓰는 경우 발생 → 레거시 시스템, 다른 시스템과 연동, Integer 코드로 존재하는 경우- ORM과 같이 Convert를 하기 위해 필요한 class가
AttributeConverter
- ORM과 같이 Convert를 하기 위해 필요한 class가
Converter 사용 예시
1. Convert
할 대상인 BookStatus.java
를 생성
@Data
public class BookStatus {
private int code;
private String description;
public BookStatus(int code){
this.code = code;
this.description = parseDescription(code);
}
public boolean isDisplayed(){
return code == 200;
}
private String parseDescription(int code) {
switch (code) {
case 100 :
return "판매종료";
case 200:
return "판매중";
case 300:
return "판매보류";
default:
return "미지원";
}
}
}
2. DB-Entity에 Covnert
역할을 하는 BookStatusConverter.java
를 추가
AttributeConverter
interface 구현이 필요- AttributeConverter<
BookStatus
,Integer
>는 Convert할 Entity와 DB타입을 표시
convertToDatabaseColumn
는 Entity → 데이터베이스로 변환할 메소드
convertToEntityAttibute
는 데이터베이스 → Entity로 변환할 메소드
- AttributeConverter<
@Converter
어노테이션 추가, Convert할 클래스임을 표시
@Converter
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {
@Override
public Integer convertToDatabaseColumn(BookStatus attribute) {
return attribute.getCode();
}
@Override
public BookStatus convertToEntityAttribute(Integer dbData) {
return dbData != null ? new BookStatus(dbData) : null;
}
}
3. Book Entity
에 Convert
할 속성인 BookStatus
를 추가
@Convert(converter = BookStatusConverter.class)
는 어떤 class를 통해 Convert 할지 지정
public class Book extends BaseEntity{
...
@Convert(converter = BookStatusConverter.class)
private BookStatus status; // 판매상태
}
4. 결과 확인
data.sql
- insert문에
status
값을 추가
insert into book(`id`, `name`, `publisher_id`, `deleted`, `status`) values(1, 'JPA 초격자 패키지', 1, false, 100);
insert into book(`id`, `name`, `publisher_id`, `deleted`, `status`) values(2, 'Spring', 1, false, 200);
insert into book(`id`, `name`, `publisher_id`, `deleted`, `status`) values(3, 'Spring Security', 1, true, 100);
BookRepository.java
- DB변환 된
status
(Integer
) 확인을 위해native쿼리
사용, JPA에선BookStatus
객체 반환
@Query(value = "select * from book order by id desc limit 1", nativeQuery = true)
Map<String, Object> findRowRecord();
BookRepositoryTest.java
@Test
void convertTest(){
bookRepository.findAll().forEach(System.out::println);
Book book = new Book();
book.setName("또 다른 전문서적");
book.setStatus(new BookStatus(200));
bookRepository.save(book);
System.out.println(bookRepository.findRowRecord().values());
}
//결과
//bookRepository.findAll().forEach(System.out::println);
Book(super=BaseEntity(createdAt=2021-08-14T12:08:05.490965, updatedAt=2021-08-14T12:08:05.490965), id=1, name=JPA 초격자 패키지, category=null, deleted=false, status=BookStatus(code=100, description=판매종료))
Book(super=BaseEntity(createdAt=2021-08-14T12:08:05.497410, updatedAt=2021-08-14T12:08:05.497410), id=2, name=Spring, category=null, deleted=false, status=BookStatus(code=200, description=판매중))
//System.out.println(bookRepository.findRowRecord().values());
[4, 2021-08-14 12:08:06.090323, 2021-08-14 12:08:06.090323, null, false, 또 다른 전문서적, 200, null]
Converter를 사용할 때 나올 수 있는 문제점
- 레거시 시스템의 경우
convertToEntityAttribute
만 구현하여 조회만 하는 로직을 만든 경우- 조회만하는 경우에도
convertToDatabaseColumn
를 모두 구현해야 문제가 발생하지 않음
- 영속성 컨텍스트가 구현되지 않은 메소드를 통해 변경감지로 인식 (update실행)
사용 예시
BookStatusConverter.java
- 조회만 하는 용도로써
convertToDatabaseColumn
를 구현하지 않음
@Override public Integer convertToDatabaseColumn(BookStatus attribute) { return null; }
BookService.java
- 조회하는 용도의 쿼리를 생성
@Transactional public List<Book> getAll(){ List<Book> books = bookRepository.findAll(); books.forEach(System.out::println); return books; }
BookServiceTest.java
bookService
,bookRepository
를 이용해 2번의 데이터 조회
@Test void converterErrorTest() { bookService.getAll(); bookRepository.findAll().forEach(System.out::println); }
결과
- 영속성 컨텍스트가 구현되지 않은 컨버터를 통해 변경감지를 하고 null로 update 실행
//bookService.getAll(); Book(super=BaseEntity(createdAt=2021-08-14T12:28:55.083990, updatedAt=2021-08-14T12:28:55.083990), id=1, name=JPA 초격자 패키지, category=null, deleted=false, status=BookStatus(code=100, description=판매종료)) Book(super=BaseEntity(createdAt=2021-08-14T12:28:55.092185, updatedAt=2021-08-14T12:28:55.092185), id=2, name=Spring, category=null, deleted=false, status=BookStatus(code=200, description=판매중)) //bookRepository.findAll().forEach(System.out::println); Hibernate: update book set updated_at=?, category=?, deleted=?, name=?, publisher_id=?, status=? where id=? Hibernate: update book set updated_at=?, category=?, deleted=?, name=?, publisher_id=?, status=? where id=? Book(super=BaseEntity(createdAt=2021-08-14T12:28:55.083990, updatedAt=2021-08-14T12:28:55.778718), id=1, name=JPA 초격자 패키지, category=null, deleted=false, status=null) Book(super=BaseEntity(createdAt=2021-08-14T12:28:55.092185, updatedAt=2021-08-14T12:28:55.812716), id=2, name=Spring, category=null, deleted=false, status=null)
- 조회만하는 경우에도
Convert에 autoApply속성
@Converter(autoApply = true)
를 지정하면 객체 타입을 통해 자동 매핑IntegerConvert
,StringConvert
등 많은 곳에서 사용하는 경우 문제 발생할 확률이 높음
- 용도가 명확한 클레스
(BookStatus)
를 사용하는 경우에 사용하는 것을 권장
사용 예시
BookStatusConverter.java
@Converter(autoApply = true)
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {
...
}
Book.java
// @Convert(converter = BookStatusConverter.class)
private BookStatus status; // 판매상태
'백엔드 > JPA' 카테고리의 다른 글
[ JPA ] 10. JPA 트러블슈팅 (0) | 2021.08.25 |
---|---|
[ JPA ] 9. 임베디드 타입 활용 (0) | 2021.08.18 |
[ JPA ] 7. 영속성 전이 (Cascade) (0) | 2021.08.12 |
[ JPA ] 6-4. 트랜잭션 매니저 (TransactionManager) (0) | 2021.08.08 |
[ JPA ] 6-3. Entity 생명주기 (1) | 2021.08.08 |