[JPA] EntityManager 테스트하기

merci·2023년 3월 19일
1

JPA

목록 보기
3/5

EntityManager 테스트

이번에는 EntityManager를 이용한 레파지토리를 만들고 테스트를 해보자

yml 설정은 이전 포스팅처럼 한다.

  jpa:
    hibernate:
      ddl-auto: create
    show-sql: true
    properties:
      hibernate:
        '[format_sql]': true

엔티티 만들기

@Setter @Getter
@NoArgsConstructor
@Entity
@Table(name="user_tb")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    @JsonIgnore
    private String password;
    private String email;
    @CreationTimestamp
    private Timestamp createdAt;

    @Builder
    public User(Long id, String username, String password, String email, Timestamp createdAt) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.createdAt = createdAt;
    }

    public void update(String password, String email){
        this.password = password;
        this.email = email;
    }
}

참고로 @ToString을 사용하지 않는 이유는 JPA에서 사용했을때 순환참조를 피하게 위함이다.

@Setter @Getter
@NoArgsConstructor
@Entity
@Table(name="board_tb")
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne // FK 키 설정을 자동적으로 해준다
    // @ManyToOne(fetch = FetchType.LAZY) // lazy 전략 - 필요할때만 사용
    private User user; // hibernate 를 이용하면 오브젝트를 넣어주면 FK로 인식한다.
    private String title;
    private String content;
    @CreationTimestamp
    private Timestamp createdAt;

    @Builder
    public Board(Long id, User user, String title, String content, Timestamp createdAt) {
        this.id = id;
        this.user = user;
        this.title = title;
        this.content = content;
        this.createdAt = createdAt;
    }

    public void update(String title, String content){
        this.title = title;
        this.content = content;
    }

    @Override
    public String toString() {
        return "Board [id=" + id + ", user=" + user + ", title=" + title + ", content=" + content + ", createdAt="
                + createdAt + "]";
    }
}

ManyToOne

Java 애플리케이션에서 두 엔티티 클래스 간의 다대일 관계를 정의하는 데 사용되는 JPA 주석이다.

예를 들어 작가가 여러 책을 썼다면 책의 속성은 다음과 같을 것이다.

   @ManyToOne
   private User author;

반대로 유저의 속성은 다음과 같을 것이다.

   @OneToMany(mappedBy = "author")
   private Set<Post> posts;

하지만 한쪽에서만 접근이 가능한 단방향 관계일 경우 ManyToOne만 사용되는 경우가 있다.
다른테이블과 1:1로 연결이 된다면 OneToOne주석을 사용한다.
@JoinColumn 주석을 추가하여 외래 키 열 이름을 명시적으로 지정하고 데이터베이스 스키마의 일관성을 보장하는 것이 좋지만 위처럼 생략이 될 경우도 있다.


서버를 실행하면 자동적으로 엔티티에 맞는 DB가 h2 에 만들어지게 된다.

Hibernate: 

    create table board_tb (
       id bigint generated by default as identity,
        content varchar(255),
        created_at timestamp,
        title varchar(255),
        user_id bigint,
        primary key (id)
    )
Hibernate: 
    
    create table user_tb (
       id bigint generated by default as identity,
        created_at timestamp,
        email varchar(255),
        password varchar(255),
        username varchar(255),
        primary key (id)
    )


레파지토리 만들기

EntityManager를 이용하는 레파지토리를 만든다.
제네릭으로 하나의 레파지토리만 이용하도록 만들어 봤다.
insert, update는 save() 하나로 처리한다.

import javax.persistence.EntityManager;
import org.springframework.stereotype.Repository;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Repository
public class MyRepository<T, D> {
    private final EntityManager em;

    public T findById(Class<T> clazz, D d){
        return em.find(clazz, d);
    }

    public List<T> findAll(Class<T> clazz){
        return em.createQuery("select u from "+clazz.getSimpleName()+" u", clazz).getResultList();
    }

	@Transactional
    public T save(T entity){
        // if(ObjectUtils.isEmpty(entity.getId())){
        //     em.persist(entity);
        // }else{
        //     em.merge(entity);
        // }
        if (em.contains(entity)) { // DB 에서 해당 entity 객체가 있는지 영속상태를 확인해준다.
            em.merge(entity);
        } else {
            em.persist(entity);
        }
        return entity;
    }
    
    @Transactional
    public void delete(T entity){ // 삭제할때도 오브젝트를 넣어서 찾는다 !!
        em.remove(entity);
    }
}

EntityManager는 findAll 메소드를 따로 제공하지 않는다.
엔티티마다 필요한 필드가 다르고 검색하는 조건도 다르기 때문에 개발자가 직접 적을수 있도록 createQuery를 제공한다.
createQuery를 사용할때는 *을 이용하지 않고 별칭을 만들어서 조회를 한다.

테스트코드 준비

@DataJpaTest // 이녀석은 JpaRepository 를 구현한 인터페이스만 힙에 띄워준다.
@Import(MyRepository.class) // 따라서 EntityManager를 의존한 레파지토리는 임포트를 해줘야 한다.
@Transactional
public class BoardRepositoryTest extends MyDummyEntity{

    @Autowired 
    private MyRepository<Board, Long> boardRepository;
    @Autowired 
    private MyRepository<User, Long> userRepository;
    @Autowired
    private EntityManager em;
    
    // 테스트 코드 작성
}

@DataJpaTest 를 이용했는데 jpa테스트에 사용된다.
주의점은 JpaRepository를 구현한 인터페이스만 힙에 띄워주는데 이번에는 해당 인터페이스를 구현하지 않고 EntityManager를 이용한 레파지토리를 사용할 예정이므로 @Import(MyRepository.class)를 이용해서 EntityManager를 레파지토리를 힙에 띄운다.

여기서 jpa 테스트의 트랜잭션을 위해서는 조금 다른 방법을 이용해야 한다.

jpa를 이용할때 트랜잭션을 이용하면 데이터는 지워지더라도 테이블의 id는 초기화가 되지 않는데 아래코드로 @BeforeEach를 수행해서 매번 테이블의 id를 초기화 시킨다.

    @BeforeEach
    public void setUp(){
        em.createNativeQuery("ALTER TABLE user_tb ALTER COLUMN id RESTART WITH 1").executeUpdate();
        em.createNativeQuery("ALTER TABLE board_tb ALTER COLUMN id RESTART WITH 1").executeUpdate();
    }

간단하게 객체를 초기화하기 위해서 클래스를 하나 만들고 상속시켰다.
newUser()newBoard()를 호출하면 객체가 초기화된다.

public class MyDummyEntity {
    
    protected User newUser(String username){
        return User.builder()
                .username(username)
                .password("1234")
                .email("ssar@nate.com")
                .build();
    }

    protected Board newBoard(String title, User userPS){
        if(userPS.getId() == null){
            System.out.println("영속화 필요");
            return null;
        }
        return Board.builder()
                .title(title)
                .content("1234")
                .user(userPS)
                .build();
    }
}


테스트코드 작성

  • insert
    @Test
    public void save_test() throws Exception {
        // given
        User user = newUser("ssar");
        User userPS = userRepository.save(user);
    
        // given 2
        Board board = newBoard("제목1", userPS);
        
        // when
        Board boardPS = boardRepository.save(board);
        System.out.println("테스트 : "+boardPS);
    
        // then
        assertThat(boardPS.getId()).isEqualTo(1);
        assertThat(boardPS.getUser().getId()).isEqualTo(1);
    }
    // 테스트 : Board [id=1, user=User [id=1, username=ssar, password=1234, 
    // email=ssar@nate.com, createdAt=2023-03-19 16:06:49.03], title=제목1, 
    // content=1234, createdAt=2023-03-19 16:06:49.041]
  • update
    @Test
    public void update_test() throws Exception {
        // given1 - DB에 영속화
        User user = newUser("ssar");
        User userPS = userRepository.save(user);
        Board board = newBoard("제목1", userPS);
        Board boardPS = boardRepository.save(board);

        // given2 - request 데이터 만들기
        String title = "제목2";
        String content = "내용2";

        // when
        boardPS.update(title, content);
        em.flush();

        // then
        Board findBoardPS = boardRepository.findById(Board.class, 1L);
        assertThat(findBoardPS.getContent()).isEqualTo("내용2");
    }

처음 save()를 호출하면 Jpa가 영속성컨텍스트에 해당 엔티티를 관리하게 된다.
이때 update()를 하면 영속성컨텍스트에 관리하는 엔티티가 변했으므로 자동적으로 영속성컨텍스트의 객체도 변하게 된다.
따라서 그 다음 em.save()를 호출하지 않고 em.flush()만 호출하더라도 관리하는 엔티티의 정보가 최신화됐으므로 그대로 영속화가 진행된다.

테스트 결과를 확인하면 이상없이 DB에 update가 되었다.


  • delete
    @Test
    public void delete_test() throws Exception {
        // given1 - DB에 영속화
        User user = newUser("ssar");
        User userPS = userRepository.save(user);
        Board board = newBoard("제목1", userPS);
        Board boardPS = boardRepository.save(board);
        // em.clear(); // 영속성 컨텍스트를 비운다면 ?        
        // lazy 전략을 이용하면 필요한 데이터만 다시 불러와 부하를 줄일수도 있다.
        
        // given2 
        Long id = 1L;
        Board findBoardPS = boardRepository.findById(Board.class, id);

        // when
        boardRepository.delete(findBoardPS);

        // then
        Board deleteBoardPS = boardRepository.findById(Board.class, id);
        assertThat(deleteBoardPS).isNull();
    }

여기서 lazy전략을 이용하면 필요한 데이터만 DB에서 가져오게 된다.
필요할 때 가져온다는 것은 참조할 때 데이터를 가져오지 않고 필요할 때 접근하게 되는데 여기서 간단하게 확인한다고 toString을 이용하면 lazy전략을 무시하고 참조된 모든 데이터를 불러오게 되므로 의도한 전략을 무시하게 된다.
clear()를 하더라도 toString은 필요한 데이터를 다 가져오므로 lazy전략을 이용한다면 toString을 같이 사용하지 않는편이 좋다.


  • select할 경우
    @Test
    public void findById_test() throws Exception {
        // given
        User user = newUser("ssar");
        User userPS = userRepository.save(user);
        Board board = newBoard("제목1", userPS);
        Board boardPS = boardRepository.save(board);
        Long id = 1L;
        
        // when
        Board findBoardPS = boardRepository.findById(Board.class, id);
    
        // then
        assertThat(findBoardPS.getUser().getUsername()).isEqualTo("ssar");
        assertThat(findBoardPS.getTitle()).isEqualTo("제목1");
    
    }

    @Test
    public void findAll_test() throws Exception {
        // given
        User user = newUser("ssar");
        User userPS = userRepository.save(user);
        List<Board> boardList = Arrays.asList(newBoard("제목", userPS), newBoard("제목2", userPS));
        boardList.stream().forEach((board2)->{
            boardRepository.save(board2);
        });
        // when
        List<Board> userListPS = boardRepository.findAll(Board.class);
    
        // then
        assertThat(userListPS.size()).isEqualTo(2);
    }
}
profile
작은것부터

0개의 댓글