Notice
Recent Posts
Recent Comments
Link
«   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
Archives
Today
Total
관리 메뉴

개발자되기 프로젝트

@Converter 본문

JPA/Custom Query

@Converter

Seung__ 2021. 7. 10. 17:46

1. Converter란?


   entity와 DB사이에서 동작하며 말 그래도 converter의 역할을 한다.

 

  즉 어떤 정보를 Entity에 저장하는 type과 DB에 저장하는 type다를 경우 중간에서 변환하는 기능을 제공한다.

반대도 쌉가능

 

2. 준비


 

책의 상태를 추가해보자. 

 

DB에는 int(코드)로 status를 저장하고 entity는 boolean타입으로 관리한다고 해보자.

 

예를들어 DB에 어떤 책의 status가 200이라고 하면, 이는 현재 미판매(전시)되어있고 isDisplayed()는 true를 반환한다.

 

<Book> 

private int status; //판매상태
public boolean isDisplayed(){

return status == 200;

}

<data.sql>

insert into book(id, name, publisher_id, `deleted`, `status`) values (1, '탐사수수수수', 1, false, 100);

insert into book(id, name, publisher_id, `deleted`, `status`) values (2,'삼다수', 1, false, 200);

insert into book(id, name, publisher_id, `deleted`, `status`) values (3, '백산수', 1, true, 100);

<Test>

@Test
void converterTest(){

bookRepository.findAll().forEach(System.out::println);

}

 

이처럼 숫자 코드로 이루어진 DATA를 직접 사용하는 것은 ORM 측면에서 맞지 않다.

 

숫자 코드를 객체로 변환하여 사용해야 한다.

 

 

3. DB column --> Entity


 

위에서 작성한 book의 status부분을 별도 class로 만들자.

 

<BookStatus class>

@Data
public class BookStatus {

  private int code;
  private String description;

  public boolean isDisplayed(){
 	 return code == 200;
	}
}

<Book class> book에서는 status관련 내용을 삭제하는 대신 BookStatus 객체를 만들어준다.

//    private int status; //판매상태
//    public boolean isDisplayed(){
//
//        return status == 200;
//
//    }

    private BookStatus bookStatus;

그런데 에러가 발생한다. BookStatus는 entity속성으로 추가할 수 없다.

 

BookStatusConverter class를 만들어주자.

 

해당 class는 AttributeConverter를 implements한다.

 

AttributeConverter<x,y>
attributeConverter interface를 구현한 class는
entity attribute를 database의 column representation으로 양방향으로 변환이 가능하다.
x : entity attribute의 type           y : DB column의 type

 

<BookStatusConverter>

@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;
        //삼항연산자
    }
}

book 의 경우 @NotNull을 지정하지 않아 column들이 nullable한 상태이다. 

 

따라서null point exception이 발생할 수 있음.

 

특히 converter에서 발생하는 exception은 DB에 접근되어있는 로직에서 발생하는 예외기 때문에,

 

최대한 발생하지 않도록 막아야함. 따라서 삼항연산자를 통해 간단히 처리해줌.

 

 

 

강의를 듣다가 사망?이라 들려서 뭔가해서 찾아봤더니....나만 그런건 아닌 것 같다ㅋㅋㅋㅋㅋㅋㅋㅋㅋ

 

사망 연산자 ㅋㅋ - 후니넷

모르는 사람이 들으면 그렇게 들릴 수도 있겠네.. 삼항 연산자 -> 사망 연산자 ㅋㅋ

www.hooni.net

삼항연산자 아냐? 응 : 아니

dbData != null ? new BookStatus(dbData) : null;

--> dbData가 notnull이야 ? 응  : 아니

 

 

 

그리고 해당 class는 converter기 때문에 @Converter annotation도 붙여주자.

 

그리고 BookStatus객체를 사용하는 Book class는 BookStatus 객체에 @Convert annotation을 붙여줘야 한다.

해당 객체 이름은 DB에서 사용하는 column명과 매칭이 가능해야 한다.

@Convert(converter = BookStatusConverter.class)//어떤컨버터 사용?
private BookStatus status;

 

실행한 결과, DB의 status에 대한 숫자 코드값이 BookStatus 객체로 변환되었다.

 

Book(super=BaseEntity(createdAt=2021-07-10T16:45:38.195144,
	updatedAt=2021-07-10T16:45:38.195144), id=1, name=탐사수수수수, authorId=null, 
    category=null, deleted=false, status=BookStatus(code=100, description=판매종료))
    
Book(super=BaseEntity(createdAt=2021-07-10T16:45:38.197412, 
	updatedAt=2021-07-10T16:45:38.197412), id=2, name=삼다수, authorId=null, 
    category=null, deleted=false, status=BookStatus(code=200, description=판매중))

 

4. Entity --> DB column


반대로 객체에서 코드로 변환하여 DB에 의도대로 들어갔는지 확인해 보자.

 

save를 통해 DB에 반영되면 jpa는 DB에 있는 값 그대로를 읽어오지 못한다.

 

따라서 native query를 통해 직접 조회해보자.

 

<BookRepository>

@Query(value = "select * from book order by id desc limit 1", nativeQuery = true)
Map<String, Object> findRawRecord();

<Test>

  @Test
    void converterTest(){

        bookRepository.findAll().forEach(System.out::println);

        Book book = new Book();
        book.setName("프론트");
        book.setStatus(new BookStatus(200));

        bookRepository.save(book);

        //DB에 잘 들어갔는지 보고싶은데....?DB값을 보고싶은데..?--> native query
        System.out.println(bookRepository.findRawRecord().values());

}

의도한 대로 DB에 200으로 반영되었다.

Hibernate: 
    select
        * 
    from
        book 
    order by
        id desc limit 1
[4, 2021-07-10 16:56:32.127908, 2021-07-10 16:56:32.127908, null, null, false, 프론트, 200, null]

 

5. Converter 사용 시 주의점 : 양방향 모두 구현해야 함.


 

아래처럼 converter에서 entity --> DB column에 대한 구현이 없다고 해보자.

@Converter
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {

    @Override
    public Integer convertToDatabaseColumn(BookStatus attribute) {
        return null; //attribute.getCode();
    }

    @Override
    public BookStatus convertToEntityAttribute(Integer dbData) {
        return dbData != null ? new BookStatus(dbData) : null;
        //삼항연산자
    }
}

<BookService class> 

getAll() 이라는 repository에서 record전부 읽어오고 각각 출력해주는 method이다.

그리고 해당 method에 @Transactional 적용!

@Transactional
  public List<Book> getAll(){

  List<Book> books = bookRepository.findAll();

  books.forEach(System.out::println);

  return books;
}

<Test>

@Test
void converterErrorTest(){
   bookService.getAll();

   bookRepository.findAll().forEach(System.out::println);
}

<결과>

 

처음에 조회한 data는 status가 정상적으로 입력되었다.

 

하지만 두 번의 update query가 실행되었고,

Book(super=BaseEntity(createdAt=2021-07-10T17:08:24.672196, updatedAt=2021-07-10T17:08:24.672196), id=1, name=탐사수수수수, authorId=null, category=null, deleted=false, 
	status=BookStatus(code=100, description=판매종료))
    
Book(super=BaseEntity(createdAt=2021-07-10T17:08:24.674221, updatedAt=2021-07-10T17:08:24.674221), id=2, name=삼다수, authorId=null, category=null, deleted=false, 
	status=BookStatus(code=200, description=판매중))

Hibernate: 
    update
        book 
    set
        updated_at=?,
        author_id=?,
        category=?,
        deleted=?,
        name=?,
        publisher_id=?,
        status=? 
    where
        id=?
Hibernate: 
    update
        book 
    set
        updated_at=?,
        author_id=?,
        category=?,
        deleted=?,
        name=?,
        publisher_id=?,
        status=? 
    where
        id=?

            book0_.deleted = 0
        )

Book(super=BaseEntity(createdAt=2021-07-10T17:08:24.672196, updatedAt=2021-07-10T17:08:27.435769), 
	id=1, name=탐사수수수수, authorId=null, category=null, deleted=false, status=null)

Book(super=BaseEntity(createdAt=2021-07-10T17:08:24.674221, updatedAt=2021-07-10T17:08:27.443016), 
	id=2, name=삼다수, authorId=null, category=null, deleted=false, status=null)

최종적으로는 status가 null이다.

 

영속성 context가 오작동을 했드아. 왜이러는겨

 

원인은

  1) @Transactional 적용

  2) Transaction이 끝나는 시점에 해당 entity에서 변경된 내용이 있는지 check

  3) 변경된 내용이 있다면 DB에 다시 영속화

  4) 이때 convert의 기능이 양방향으로 완벽히 구현되지 않음 : DB column에 null로 return

  5) 처음 DB에서 불러온 값과, converter에 의해 다시 변환된 값이 차이가 발생했다.

  6) 영속성 context는 값이 변경되었기 때문에 update 실행

  7) DB의 status에 null 입력됨.

 

즉 조회만 사용하려고 entity --> DB경우는 구현을 하지 않았지만,

converter는 완전히 구현하지 않아 data가 날라가는 경우임.

 

필요없어도 꼭 양방향으로 구현해두자.

 

6. AutoApply


AutoApply
true로 설정하면 persistence provider가 해당 객체 type이 entity에 filed로 선언되어 있으면 해당 컨버터를 통해 변환이 이루어진다. 알아서 @Convert 붙여주는 효과.
If the autoApply element is specified as true, the persistence provider must automatically apply the converter to all mapped attributes of the specified target type for all entities in the persistence unit except for attributes for which conversion is overridden by means of the Convert annotation

 

주의

일반적으로 개발자가 생성한 class type에 한해서 사용해야 한다.

 

만약 아래와 같이 entity Type을 String으로 한다면? 

public class Converter implements AttributeConverter<String, Integer> {

entity의 filed에 선언된 객체 중 타입이 String이면 무조건 convert로 들어간다.....노답임.

 

 

<BookStatusConverter class>

@Converter(autoApply = true)
public class BookStatusConverter implements AttributeConverter<BookStatus, Integer> {

    @Override
    public Integer convertToDatabaseColumn(BookStatus attribute) {
        return null; //attribute.getCode();
    }

    @Override
    public BookStatus convertToEntityAttribute(Integer dbData) {
        return dbData != null ? new BookStatus(dbData) : null;
        //삼항연산자
    }
}

<Book class> @Convert 삭제

  //@Convert(converter = BookStatusConverter.class)//어떤컨버터 사용?
    private BookStatus status;

<Test>

[4, 2021-07-10 17:42:12.583954, 2021-07-10 17:42:12.583954, null, null, false, 프론트, 200, null]

 

7. GitHub : 210710 Converter


 

 

bsh6463/BookManager

Contribute to bsh6463/BookManager development by creating an account on GitHub.

github.com

 

'JPA > Custom Query' 카테고리의 다른 글

Native Query  (0) 2021.07.10
@Query  (0) 2021.07.09
Comments