SMALL

View

데이터베이스에서 View는 가상 테이블이다.

View를 사용하는 이유로는 여러 테이블을 조인하는 경우와 같이 복잡한 SQL을 간소화시키기 위해서입니다.

 

이러한 View를 Spring JPA에서 어떻게 사용하는지 알아보도록 합시다.

 

@Table

 

View 생성

CREATE OR REPLACE VIEW ROLE AS 
SELECT u.user_id,
       ut.user_id AS u_id
FROM   USER u
       JOIN user_team ut
         ON u.id = ut.user_id;

 

Entity 생성

@Entity
@Immutable
@Table(name = "role")
@Getter
public class Role {

    private String userId;
    
    @Id
    private UUID uId;

    private UUID teamId;

}

@Table, @Immutable Annotation을 추가하여 Entity 생성

@Table에는 View를 생성한 name을 작성

@Immutable - 해당 Entity에 매핑되는 것은 View이기 때문에 조회만 하고 수정은 존재하지 않는다.

 

이렇게 SQL View에 매핑되는 Entity를 작성할 수 있다.

추가적으로, @Subselect Annotation을 통해서도 가능하다.

 

@Subselect

해당 Annotation은 SQL View를 생성하지 않고 View를 생성할 때 사용하는 Join query를 Annotation에 설정하는 방식이다.

 

@Entity
@Immutable
@Subselect("SELECT u.user_id,\r\n" + 
        "       ut.user_id AS u_id,\r\n" + 
        "FROM   USER u\r\n" + 
        "       JOIN user_team ut\r\n" + 
        "         ON u.id = ut.user_id")
@Getter
public class Role {

    private String userId;
    
    @Id
    private UUID uId;

    private UUID teamId;

}

 

복합키 View ( @IdClass + @Table )

추가적으로, View를 생성하면 여러 테이블을 join 하여 생성하므로 일반적인 Entity와 같이 PrimaryKey가 하나 이상일 경우가 존재한다.

이러한 경우에는 @IdClass Annotation을 사용하여서 여러 Column을 조합하여 Primary Key를 설정할 수 있다.

 

View 생성

CREATE OR REPLACE VIEW ROLE AS 
SELECT u.user_id,
       ut.user_id AS u_id,
       tpr.team_id,
       tpr.page_id
FROM   USER u
       JOIN user_team ut
         ON u.id = ut.user_id
       JOIN uteam t
         ON t.id = ut.team_id
       JOIN team_page_role tpr
         ON t.id = tpr.team_id;

 

Entity 생성

@Entity
@Immutable
@Table(name = "role")
@Getter
@IdClass(RoleId.class)
public class Role {

    private String userId;
    private UUID uId;

    @Id
    private UUID teamId;

    @Id
    private UUID pageId;

}

이러한 식으로 Entity에 @Id를 한 개 이상 추가할 수 있다.

@IdClass Annotation 추가 해당 class 생성

 

 

@IdClass

@NoArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class RoleId implements Serializable {

    private static final long serialVersionUID = 1122593031541339037L;

    @EqualsAndHashCode.Include
    UUID teamId;

    @EqualsAndHashCode.Include
    UUID pageId;
}

@IdClass를 사용하는 자세한 내용은 아래의 포스팅을 통해 확인할 수 있다. ( JPA 복합키 생성 )

https://joomn11.tistory.com/105

 

LIST
SMALL

Entity를 생성하다 보면 PK(primary key)가 여러 개가 되는 경우가 발생한다.

이러한 경우에 어떠한 방식으로 Entity를 작성해야 하는지 알아보도록 하자.

 

JPA에서는 두 가지 방식을 제공한다.

@IdClass, @EmbeddedId

각각의 사용법과 특징을 알아보도록 하자.

 

@IdClass

@Getter
@NoArgsConstructor
@Entity
@IdClass(MemberId.class)
public class Member {
	@Column
	private String name;
	
	@Id
	@Column
	private String teamId;
	
	@Id
	@Column
	private String teamName;
}

복합키가 존재하는 Entity에 @IdClass Annotation을 추가해주고, 복합키를 설정할 class명을 정해준다.

또한, 복합키로 지정할 컬럼에 @Id Annotation을 추가한다.

 

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@NoArgsConstructor
public class MemberId implements Serializable {

	private static final long serialVersionUID = 5735022999922182249L;

	@EqualsAndHashCode.Include
	public String teamId;
	
	@EqualsAndHashCode.Include
	public String teamName;
}

IdClass의 작성시 지켜야 할 사항들이 존재한다.

  • Serializable을 implements 받아야 한다.
  • equal, hashCode method를 구현해야 한다. 위의 예제에서는 lombok을 이용하였다.
  • 기본 생성자가 존재해야 한다.
  • public class 이여야 한다.
  • Entity Class의 필드명과 동일한 필드를 가지고 있어야 한다. 

 

public interface MemberRepository extends JpaRepository<Member, MemberId>{

}

Repository를 작성 시에는 JpaRepository의 제너릭 타입에 Entity Class와 Entity Class의 Key값을 넣는데 복합키를 사용한 경우에는 위와 같이 넣어주어야 한다.

 

@EmbeddedId

@Getter
@NoArgsConstructor
@Entity
public class Member {
	@Column
	private String name;
	
	@EmbeddedId
	private MemberId memberId;
}

 

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@NoArgsConstructor
@Embeddable
public class MemberId implements Serializable {

	private static final long serialVersionUID = 5735022999922182249L;

	@EqualsAndHashCode.Include
	public String teamId;
	
	@EqualsAndHashCode.Include
	public String teamName;
}

 

이렇게 두 가지의 형식이 존재하는데 각각의 선언하는 방식도 다르지만 실제로 쿼리를 조회한 후에 값을 조회할 때도 차이가 존재한다.

 

@IdClass의 경우에는 member.getTeamId()와 같이 사용

@EmbeddedId의 경우에는 member.getMemberId().getTeamId()와 같이 사용해야 한다. 

LIST
SMALL

Fetch Join은 기본 SQL 문법이 아니다.

JPQL( Java Persistence Query Language)에서 성능 최적화를 위해  제공하는 기능이다. 

연관 Entity or Collection을 SQL 한 번에 조회하기 위해 사용된다. 

JPQL ( Java Persistence Query Language ) : 객체 지향 쿼리 언어 
 - 테이블 대상으로 쿼리를 하는것이 아닌 객체를 대상으로 쿼리
 - SQL를 추상화한 것으로 특정 DB의 SQL에 의존하지 않는다

보통 아래와 같은 형태로 사용된다.

 

@Query("select m from Member m join fetch m.team")

 

실제 사용되는 예시를 보도록하자.

Member, Team Entity가 존재하고 Member는 하나의 Team에 속할 수 있고 Team은 여러 명의 Member를 가질 수 있다.

 

 

위와 같은 형태로 Member의 정보를 조회했을 때 해당 Member가 속한 Team 정보를 조회하고 싶은 경우에 사용될 수 있다.

 

@Getter
@NoArgsConstructor
@Entity
public class Member {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;
	
	@ManyToOne
	private Team team;
	
	@Builder
	public Member(String name) {
		this.name = name;
	}
	
	public void updateTeam(Team team) {
		this.team = team;
	}
}
@Getter
@NoArgsConstructor
@Entity
public class Team {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;
	
	@OneToMany(mappedBy = "team")
	private List<Member> members;
	
	@Builder
	public Team(String name) {
		this.name = name;
	}
}

 

기본적으로 JPA에서 제공하는 findAll 메서드를 호출하게 되면 Member List를 모두 조회해오는 Query와 각각의 Member에 매핑되는 Team을 조회하는 쿼리가 실행하게 된다. 

( 예를 들어 Member의 개수가 N 인 경우 Member List 조회 ( 1 ) + 각 Member에 매핑되는 Team 조회 ( N ) => N +1 )

 

memberRepository.findAll()

Hibernate:
    select member0_.id as id1_2_, member0_.name as name2_2_, member0_.team_id as team_id3_2_ from Member member0_
Hibernate:
    select team0_.id as id1_6_0_, team0_.name as name2_6_0_ from Team team0_ where team0_.id=?
Hibernate:
    select team0_.id as id1_6_0_, team0_.name as name2_6_0_ from Team team0_ where team0_.id=?

 

이러한 문제를 해결하기 위해서 JPQL에서 Fetch Join 기능을 제공한다.

 

public interface MemberRepository extends JpaRepository<Member, Long>{

	@Query("select m from Member m join fetch m.team")
	Set<Member> findAllJoinFetch();
}

 

memberRepository.findAllJoinFetch()

Hibernate: 
    select
        member0_.id as id1_2_0_,
        team1_.id as id1_6_1_,
        member0_.name as name2_2_0_,
        member0_.team_id as team_id3_2_0_,
        team1_.name as name2_6_1_ 
    from
        Member member0_ 
    inner join
        Team team1_ 
            on member0_.team_id=team1_.id

만약 실행되는 Query의 Join 형식도 정하고 싶은 경우에는 아래와 같이 지정하면 된다.

@Query("select m from Member m left join fetch m.team")

 

 

또한 Join을 한 대상에서 한 번 더 Join을 하고 싶은 경우에는 아래와 같이 작성하면 된다.

( Team에 연관 객체로 Coach가 있다고 가정 )

@Query("select m from Member m join fetch m.team t join fetch t.coach")

 

만약 이러한 형태가 아닌 기본 제공하는 findAll을 통해서 연관 객체를 조회하고 싶은 경우에는 FetchType을 EAGER로 지정하면 된다.

@Getter
@NoArgsConstructor
@Entity
public class Team {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;
	
	@OneToMany(mappedBy = "team", fetch = FetchType.EAGER) // !!Added
	private List<Member> members;
	
	@Builder
	public Team(String name) {
		this.name = name;
	}
}

 

LIST
SMALL

JPA의 연관관계 매핑의 다중성 중에 하나인 One To Many Mapping에 관해서 알아보고 예시를 보도록 하자.

연관관계 관련 어노테이션 중에 @OneToMany, @ManyToOne이 존재한다.

언뜻 보면 두 어노테이션은 같은 기능을 할 것 같지만 그렇지 않다. 

 

예시로 학생과 수업 Entity를 살펴보자.

학생은 여러 개의 수업을 수강할 수 있지만, 수업은 한 명의 학생만 수강할 수 있다. (과외라고 생각하자)

이 관계에서 주인은 foreign key를 가지고 있는 Courses가 된다.

 


@Getter
@NoArgsConstructor
@Entity
public class Courses {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;

	@ManyToOne
	private Students student;

	@Builder
	public Courses(String name) {
		this.name = name;
	}

	public void update(String name, Students student) {
		this.name = name;
		this.student = student;
	}
}

 

@Getter
@NoArgsConstructor
@Entity
public class Students {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;

	@Builder
	public Students(String name) {
		this.name = name;
		this.course = new HashSet<>();
	}

	public void update(String name, Courses course) {
		this.name = name;
		this.course.add(course);
	}
}

이러한 형태로 Entity를 작성하게 되면, Course에 Student_ID에 해당하는 foreign key가 생성된다.

( 대부분 연관관계의 주인이 foreign key를 가지고 있다고 생각하면 된다)

지금 현재 상태에서는 Students에서 자신이 수강하고 있는 Courses의 정보를 조회하기 어렵다.

이러한 점을 보완하기 위해 양방향 매핑으로 코드를 추가해보자.

 

양방향 관계

@Getter
@NoArgsConstructor
@Entity
public class Students {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;

	@OneToMany(mappedBy = "student") //ADDED !
	private Set<Courses> course;

	@Builder
	public Students(String name) {
		this.name = name;
		this.course = new HashSet<>();
	}

	public void update(String name, Courses course) {
		this.name = name;
		this.course.add(course);
	}
}

@OneToMany 어노테이션을 가진 courseSet이 해당 매핑 관계의 주인이 아닌것을 표시하기 위해 mappedBy를 추가한다.

 

 

테스트 코드

위의 관계를 확인하는 테스트 코드를 보자.

    @Test
    public void 코스와학생매핑_OneToMany_코스가주인() {
    	
        Courses course = Courses.builder().name("METH").build();
        Courses courseEng = Courses.builder().name("ENG").build();
        Students student = Students.builder().name("Jon").build();
        
        studentsRepository.save(student);
        
        course.updateStudent(student); // Mapping Courses With Students
        courseEng.updateStudent(student); // Mapping Courses With Students
        
        coursesRepository.save(course);
        coursesRepository.save(courseEng);

        assertThat(course.getStudent().getId()).isNotNull();
    }

 

 

LIST

+ Recent posts