JPA ORM은 일종에 interface
-> 데이터베이스의 레코드를 자바에 객체화 시켜주는 역할
자바의 객체화와 DB 데이터의 형식이 다른 경우
-> @Converter를 통해 데이터를 가져오는 즉시 정보를 변경하여 마치 Entity에서는 원래 이런 값이 있던 것 처럼 핸들링 가능
JPA에서 Enum 데이터를 가져오는 경우 -> Converter를 사용
-> OrdinalEnumValueConverter 구현체를 통해 JPA는 Enum
, DB엔 Integer
로 표현
Enum, 임베디드 데이터를 못쓰는 경우 발생 → 레거시 시스템, 다른 시스템과 연동, Integer 코드로 존재하는 경우
ORM과 같이 Convert를 하기 위해 필요한 class가 AttributeConverter
@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 "미지원";
}
}
}
AttributeConverter interface 구현이 필요
AttributeConverter<BookStatus, Integer>는 Convert할 Entity와 DB타입을 표시
convertToDatabaseColumn
는 Entity → 데이터베이스로 변환할 메소드convertToEntityAttibute
는 데이터베이스 → Entity로 변환할 메소드@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;
}
}
@Convert(converter = BookStatusConverter.class)는 어떤 class를 통해 Convert 할지 지정
public class Book extends BaseEntity{
...
@Convert(converter = BookStatusConverter.class)
private BookStatus status; // 판매상태 표시
}
data.sql 에 column 추가
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);
DB변환 된 status (Integer) 확인을 위해 native쿼리사용, JPA에선 BookStatus객체 반환
@Query(value = "select * from book order by id desc limit 1", nativeQuery = true)
Map<String, Object> findRowRecord();
@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]
레거시 시스템 에서 경우 convertToEntityAttribute
만 구현하여 조회만 하는 로직을 만든 경우
조회만하는 경우에도 convertToDatabaseColumn를 모두 구현해야 문제가 발생하지 않음
영속성 컨텍스트가 구현되지 않은 메소드를 통해 변경감지로 인식 (update실행)
조회만 하는 용도로써 convertToDatabaseColumn를 구현하지 않음
@Override
public Integer convertToDatabaseColumn(BookStatus attribute) {
return null;
}
조회하는 용도의 쿼리를 생성(@Transactional 사용 주목)
@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);
}
영속성 컨텍스트가 해당 엔티티 값 중 변경된 내용이 있는지 없는지 체크를 하고 만약 변경된 내용이 있다면 그 데이터를 DB에 다시 영속화 하게 됨
-> update 구문 발생
-> converter를 덜 구현이 되어 조회했을 때와 다시 DB에 확인하는 값이 서로 다르게 되어 영속성 컨텍스트 입장에서는 값이 변경된것 처럼 감지하였음
Book(super=BaseEntity(createdAt=2022-11-23T17:01:04, updatedAt=2022-11-23T17:01:04), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false, status=BookStatus(code=100, description=판매 종료))
Book(super=BaseEntity(createdAt=2022-11-23T17:01:04, updatedAt=2022-11-23T17:01:04), id=2, name=스프링 시큐리티 패키지, category=null, authorId=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=?
// status가 null로 프린트 됨
Book(super=BaseEntity(createdAt=2022-11-23T17:01:04, updatedAt=2022-11-23T17:01:05.063945), id=1, name=JPA 초격차 패키지, category=null, authorId=null, deleted=false, status=null)
Book(super=BaseEntity(createdAt=2022-11-23T17:01:04, updatedAt=2022-11-23T17:01:05.067392), id=2, name=스프링 시큐리티 패키지, category=null, authorId=null, deleted=false, status=null)
Convert에 autoApply속성
@Converter(autoApply = true) 를 지정하면 객체 타입을 통해 자동 매핑
IntegerConvert, StringConvert 등 많은 곳에서 사용하는 경우 문제 발생할 확률이 높음
용도가 명확한 클레스(BookStatus)를 사용하는 경우에 사용하는 것을 권장
개발자가 만든 타입에만 적용하자
, 예를 들어 String에 autoapply를 하면 다양한 자료형들이 만든 컨버터를 타게 될 것!
@Converter(autoApply = true)
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {
...
}
// @Convert(converter = BookStatusConverter.class)
private BookStatus status; // 판매상태