이번에는 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 + "]";
}
}
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();
}
}
@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]
@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가 되었다.
@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을 같이 사용하지 않는편이 좋다.
@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);
}
}