Query DSL게시글은 대부분 인프런의 김영한님의 강의인 '실전! Query DSL' 기반으로 내용을 정리했습니다.
예제 도메인 모델
예제는 익숙하고도 간단한 Member와 Team이다. Member의 컬럼은 id, username, age, team이 있고 Team의 컬럼은 id, name, members가 있다. 둘의 연관 관계는 Member의 입장에서 보면 Member와 Team은 다대일이고, Team의 입장에서 보면 Team과 Member는 일대다이다.
Member
클래스 어노테이션
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of={"id", "username", "age"})
public class Member {
Member 엔티티에는 위와 같은 어노테이션이 필요하다.
- @Entity : JPA를 사용하려면 필수인 어노테이션이다. 해당 클래스를 JPA가 Entity로 관리할 수 있도록 해준다.
- @Getter/@Setter : Lombok에서 제공하는 어노테이션으로 해당 클래스의 필드에 대한 getter, setter를 자동으로 만들어 준다.
- @NoArgsContructor : Lombok에서 제공하는 어노테이션으로 기본 생성자를 자동으로 만들어주는 어노테이션으로, access 속성을 이용해 해당 생성자의 접근 제한자를 설정할 수 있다. 위 예제에서는 PROTECTED로 설정되어있다.
- 기본 생성자를 꼭 써야하나? 싶은데 JPA를 사용하고 싶을 땐 기본 생성자를 필수로 만들어줘야 한다. 그래서 기본 생성자를 public으로 열어두지 말고 그냥 protected 이상으로 막아둔다.
- @ToString : 해당 클래스에 대한 toString()을 자동으로 생성해준다. 주의할 점은 연관 관계가 맺어진 필드를 제외한 나머지 내부 필드만을 지정해야 한다. 연관 관계가 맺어진 필드를 설정하면 오버플로우를 맞이하게 될 것이다.
필드
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "team_id")
private Team team;
Member에는 세 가지 내부 필드와 한 가지의 연관 관계 필드가 있다. id의 경우 기본키이므로 @Id와 기본키 값이 자동으로 증가되는 @GeneratedValue를 붙여주고, @Column으로 기본키 컬럼명을 알려준다.
그리고 team은 Team의 클래스를 데이터 타입으로 지정한다. 그리고 제일 처음에 말했다시피 Member의 입장에서 Team을 바라볼 땐 다대일이므로 @ManyToOne을 사용하고, fetch 설정을 LAZY로 준다. 그리고 join 할 대상 컬럼명을 @JoinColumn으로 지정해주면 된다.
연관 관계 편의 메서드
public Member(String username, int age, Team team) {
this.username = username;
this.age = age;
if (team != null) {
changeTeam(team);
}
}
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
연관 관계를 편하게 맺어주기 위해 연관 관계 편의 메서드를 작성한다. 연관 관계 편의 메서드가 뭔지 모르면 아래 게시글을 참고하거나 JPA 강의를 들으면 된다.
Team
클래스 어노테이션
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
Team 엔티티에는 Member에서 사용한 어노테이션들을 사용해주면 된다.
필드
@Id @GeneratedValue
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList();
Team에는 두 개의 내부 필드와 한 개의 연관관계 필드가 존재한다. members는 List로 정의하며 null 값을 방지하기 위해 new ArrayList()를 붙여준다. 그리고 Team의 입장에서 Member를 바라볼 땐 일대다이므로 @OneToMany를 사용하고, 연관 관계의 주인이 아니므로 mappedBy로 상대의 연관 관계 컬럼(필드명)을 적어주면 된다. 연관 관계의 주인에 대해 잘 모른다면 위 연관 관계 편의 메서드에 링크된 게시글을 참조하면 알 수 있다.
테스트(순수 JPA)
클래스 어노테이션
@SpringBootTest
@Transactional
class MemberTest {
- @SpringBootTest : 스프링 부트에서 테스트 코드에 필수로 적어야하는 어노테이션이다. 애플리케이션 테스트에 필요한 거의 모든 의존성들을 제공한다.
- @Transactional : 스프링에서 트랜잭션 처리를 지원하는 방법 중 하나이며 선언적 트랜잭션이라 부른다. 테스트 환경에서 이 어노테이션을 사용하면 해당 테스트가 종료될 때 자동으로 롤백된다.
의존 관계 주입
@Autowired
EntityManager em;
EntityManager를 스프링으로부터 주입 받는다. 최신 버전의 스프링 부트에서부터는 @Autowired로 주입받을 수 있다. 또 @PersistenceContext로도 주입받을 수 있다. 만약 스프링 부트가 아닌 다른 프레임워크에서 JPA를 사용한다면 @PersistenceContext를 사용하자.
테스트 데이터 생성
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
em.flush();
em.clear();
간단한 테스트를 위해 데이터를 생성해보자. Team은 teamA와 teamB 두 개를 만들고 em.persist()로 영속성 컨텍스트에 저장한다. 뒤이어 member1, member2, member3, member4를 만들고 이 엔티티들 또한 em.persist()로 영속성 컨텍스트에 저장한다. 그러고 em.flush()를 이용해 영속성 컨텍스트에 쌓여있는 쿼리들을 DB로 전송하고, em.clear()로 영속성 컨텍스트를 초기화시킨다. 만약 영속성 컨텍스트가 뭔지 모르겠다면 아래 게시글을 참고하거나 JPA 강의를 들으면 된다.
JPA
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
for (Member member : members) {
System.out.println("member = " + member);
System.out.println("-> member.team : " + member.getTeam());
}
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.username as username3_1_
from
member member0_
member = Member(id=3, username=member1, age=10)
select
team0_.team_id as team_id1_2_0_,
team0_.name as name2_2_0_
from
team team0_
where
team0_.team_id=?
-> member.team : Team(id=1, name=teamA)
member = Member(id=4, username=member2, age=20)
-> member.team : Team(id=1, name=teamA)
member = Member(id=5, username=member3, age=30)
select
team0_.team_id as team_id1_2_0_,
team0_.name as name2_2_0_
from
team team0_
where
team0_.team_id=?
-> member.team : Team(id=2, name=teamB)
member = Member(id=6, username=member4, age=40)
-> member.team : Team(id=2, name=teamB)
출력된 로그를 보면 SQL이 제대로 날라간걸 확인할 수 있다. QueryDSL로 변경하는 부분은 ㄷㅏ음에...