JPA 다양한 Join 방법 정리 (N+1, queryDSL, fetch join)

web/Spring|2019. 11. 4. 20:31

JPA를 사용하다 보면 join을 할 때가 많아진다. join을 어떠한 방법으로 하느냐에 따라서 수행되는 쿼리가 달라지고 성능에 문제가 발생하는 경우도 종종있다.

 

그래서 다양한 방식의 join 방식을 알아보고 방식에 따라 작업을 진행해 보자.

우선 사용될 entity 두 개를 설명하면 다음과 같다.

@Getter
@Entity
@Table(name = "wedul_classes")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Builder
public class WedulClasses extends CommonEntity {

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

    @OneToMany(mappedBy = "wedulClasses", fetch = FetchType.LAZY)
    private Set<WedulStudent> wedulStudentList = new LinkedHashSet<>();

    private String classesName;

    private String classesAddr;

}

@Getter
@Entity
@Table(name = "wedul_student")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Builder
public class WedulStudent extends CommonEntity {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "wedul_classes_id")
    @JsonBackReference
    private WedulClasses wedulClasses;

    private String studentName;

    private int studentAge;

    @Enumerated(value = EnumType.STRING)
    private StudentType studentType;

}

이 두 엔티티는 class와 student의 관계로 1대 N의 관계를 가지고 있다.

이 과정에서 사용될 데이터는 임의로 추가했고 다음과 같다.

 

wedul_classe 테이블의 데이터

wedul_student 테이블의 데이터

사용 쿼리 툴) tadpole docker version

 

 

그럼 이 entity를 이용해서 데이터를 조인하여 조회해보자.

 

1. 단순 조회

우선 첫 번째로 JpaRepository 인터페이스 사용 시 기본적으로 제공하는 findAll을 사용해보자.

@Repository
public interface WedulClassesRepository extends JpaRepository<WedulClasses, Long> {

    Optional<WedulClasses> findByClassesName(String classesName);

}

이를 사용하여 데이터를 조회해보면 사용 되는 쿼리는 다음과 같다.

-- classes 목록을 조회하는 쿼리
select
    wedulclass0_.wedul_classes_id as wedul_cl1_0_
    ,wedulclass0_.create_at as create_a2_0_
    ,wedulclass0_.update_at as update_a3_0_
    ,wedulclass0_.classes_addr as classes_4_0_
    ,wedulclass0_.classes_name as classes_5_0_
  from
    wedul_classes wedulclass0_
;


-- 아래 쿼리들은 wedul_classes_id 개수별로 조회되는 쿼리
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;

쿼리를 자세히 보면 알겠지만 wedul_classes를 조회하는 쿼리와 그 wedul_classes 개수만큼 쿼리가 실행되는것을 볼 수 있다.

많이 들어 봤을 법한 N+1 문제가 발생한 것이다.

이 방식으로 쿼리 수행 시 N번의 쿼리가 발생해야 하기에 데이터 수만큼 쿼리가 실행되는 안좋은 부담을 안고 가야해서 좋지 않다.

 

2. left fetch join

위의 1번의 N+1 문제 해결로 고안된 방법 중 하나가 fetch join이다. 나는 left join을 하고자 하기에 left fetch join을 시도해보자. 우선 사용된 코드는 다음과 같다.

@Repository
public interface WedulClassesRepository extends JpaRepository<WedulClasses, Long> {
    @Query(value = "select DISTINCT c from WedulClasses c left join fetch c.wedulStudentList")
    List<WedulClasses> findAllWithStudent();
}

distinct가 붙은 이유는 카티션곱에 의해서 여러개의 결과값이 발생해 버리기 때문에 추가하였다.

그럼 사용된 쿼리도 확인해보자.

select
    distinct wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id

left join을 해서 한번에 데이터를 가져올 수 있는 걸 확인 할 수 있지만 아쉽게도 Lazy로 데이터를 가져오지 못하고 Eager로 가져와야 한다.

 

3. EntityGraph

이제 3번째 방식으로 entity graph를 사용하여 실행시켜보자. 코드는 아래와 같다.

@EntityGraph(attributePaths = "wedulStudentList")
@Query("select c from WedulClasses c")
Page<WedulClasses> findEntityGraph(Pageable pageable);

실행되는 쿼리는 다음과 같아서 2번과 동일하다. (page를 사용한 것만 차이)

select
    wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id
  order by
    wedulclass0_.update_at desc;

 

4. QueryDSL

Querydsl은 정적 타입을 이용해서 SQL과 같은 쿼리를 사용할 수 있도록 해주는 프레임워크로 HQL쿼리를 실행하게 도와준다.

설정 방식은 gradle 5 기준으로 다음과 같다.

plugins {
    id 'org.springframework.boot' version '2.2.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    id 'java'
}

group = 'com.wedul'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
    maven { url "https://plugins.gradle.org/m2/" }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compile group: "org.flywaydb", name: "flyway-core", version: '5.2.4'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.0'
    annotationProcessor 'org.projectlombok:lombok'
    testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // query dsl
    compile("com.querydsl:querydsl-apt")
    compile("com.querydsl:querydsl-jpa")
}

// querydsl 적용
def querydslSrcDir = 'src/main/generated'

querydsl {
    library = "com.querydsl:querydsl-apt"
    jpa = true
    querydslSourcesDir = querydslSrcDir
}

compileQuerydsl{
    options.annotationProcessorPath = configurations.querydsl
}

configurations {
    querydsl.extendsFrom compileClasspath
}

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', querydslSrcDir]
        }
    }
}

그리고 QueryDsl 사용을 위해 QueryDslRepositorySupport를 상속받아서 사용할 수 있는데 마지막에 distinct를 사용한 것은 2번 fetch 조인의 이유와 동일하다.

@Repository
public class WedulClassesQueryDsl extends QuerydslRepositorySupport {

    public WedulClassesQueryDsl() {
        super(WedulClasses.class);
    }

    public List<WedulClasses> findAllWithStudent() {
        QWedulClasses wedulClasses = QWedulClasses.wedulClasses;
        QWedulStudent wedulStudent = QWedulStudent.wedulStudent;

        return from(wedulClasses)
            .leftJoin(wedulClasses.wedulStudentList, wedulStudent)
            .fetchJoin()
            .distinct()
            .fetch();
    }

}

그럼 마찬가지로 실행되는 쿼리를 확인해보자.

select
    distinct wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id

애도 2번, 3번과 동일한 쿼리가 작성되는 걸 확인할 수 있다.

기본적으로 단순하게 다대일 데이터를 가져오려고 하면 N+1 문제가 발생할 수 있기 때문에 조심해야하고 이를 해결하기 위해서는 다양한 방식의 문제 해결 방식이 있는걸 확인할 수 있었다.

무엇이 가장 좋은지는 본인이 판단하거나 상황에 맞게 사용하면 좋을 거 같다.

댓글()

DDD. 애거리루트 정리

DDD|2019. 5. 16. 22:53

애그리거트는 관련된 객체를 하나의 군으로 묶어주는 것 으로 상위수준에서 모델을 조망하는 방법 중 하나이다.

애그리거트는 비슷한 속성을 가진 객체를 묶어놓은 것을 의미한다.
예를 들어 주문 시스템에 주문 관련 애그리거트는 Order, Receiver, OrderLine.. 등이 있고 회원정보에는 Member, MemberInfo등으로 나눌 수 있다. 각 애그리거트에 연관된 객체를 담고 있으며 유사하고 동일한 라이프 사이클을 보유하고 있다.

애거리거트 루트


애그리거트에서 가장 핵심이 되는 주체 즉, 애그리거트 전체를 관리하고 책임지는 주체를 애그리거트의 루트 엔티티라고 한다. 애그리거트내에 존재하는 모든 엔티티는 루트 엔티티와 직간접적으로 연결되어있다.

애거리거트 루트의 핵심 역할은 애거리거트의 일관성을 유지하는 것이다. 그렇기 때문에 모든 애거리거트의 주요 기능은 애거리거트 루트 엔티티에 구현되어야 한다. 다시말하면 애거리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안된다. 

예를 들어 상품에 대한 애그리거트가 있고 루트 엔티티로 Product가 있을 때 상품에 대한 정보를 보유한 ProductInfo의 price를 할인율 반영없이 단순히 변경하면 모델에 일관성을 깨트릴 수 있다.

ProductInfo productInfo = product.getProductInfo();
productInfo.setPrice(price);

또한 이렇게 변경하면서 중간에 할인율을 검사하도록 할 수 있지만 매번 중복된 코드가 만들어 질 수 있다.

ProductInfo productInfo = product.getProductInfo();
price = priceCalculator(price);
productInfo.setPrice(price);

 

트랜잭션의 범위


트랜잭션의 크기는 작을 수록 좋다. 하나의 트랜잭션에서 두 개 이상의 애거리거트(주문, 사용자정보등등)를 수정하게 되면 충돌이 발생할 가능성이 크다.

예를 들어 주문을 하면서 입력한 배송지를 사용자의 기본 배송지로 설정하는 경우에는 주문정보와 사용자 정보를 동시에 수정하는 경우가 발생 할 수 있다.

이런 경우에는 Order라는 애거리거트 루트에서 값을 두개 모두 수정하지 말고 OrderService라는 곳에서 값을 수정하는것이 옳다. 간단하게 보면 다음과 같이 OrderService에서 수정하는 것이 옳다.

public class OrderService {
  public void order(ShipInfo ship, isNewShippingAddr) {
    if(isnewShippingAddr) custermer.setAddr(ship.getAddr());
  }
}

 

애거리거트 필드 참조


애거리거트에서 다른 애거리거트를 필드로써 참조할 수 있다. 아래 예처럼 학교라는 School 클래스에 Student라는 애거리거트를 필드로써 참조 할 수있다.

public class School {
    private Student student;
}

이럴경우 ORM을 통해서 데이터를 함께 가져올 수 있는데 이럴 경우 단점이 있다.

1. 편한 탐색 오용
- 편하게 필드로써 애그리거트에서 다른 애그리거트를 접근할 수 있으므로 다른 애그리거트내에서 다른 애그리거트의 값을 수정할 수 있는 문제를 야기할 수 있다.
2. 성능에대한 문제
- 이는 사용에 따라서 lazy와 eager 둘 중의 하나로 선택해서 진행할 수 있다.
3. 확장이 어려움
- 만약 확장해서 student를 다른 도메인으로 빼고 싶을경우에 연관성이 깊어지다보면 분리하기 어려워서 확장이 여려워진다.

이에 대한 해결방법으로 School 내부에 Student라는 애그리거트를 포함하지 말고 student의 id인 studentId만을 포함하고 있는 것이다. 이럴경우 School에서 Student를 수정할 문제도 방지할 수 있고 lazy, eager를 고민할 필요도 dbms 확장도 자유롭게 할 수 있다.

 

애거리거트에 연관을 ID로 했을 시 상황


만약 School 내부에 Student가 List형태로 있다고 가정했을 때, 모든 학생정보를 가지고 오기 위해서 학생들 정보를 하나씩 다 가져와야한다.

이는 결국 School과 내부에 있는 학생들 수인 N개를 합쳐서 N + 1번 조회하게 된다고 해서 N+1문제라고 한다. 이를 속도가 엄청 느려지기 때문에 해결하기 위해서 조인을 사용해야 한다. 이런문제는 JPA에서 쿼리를 직접적으로 실행할 수 있는 기능등을 사용해서 해결하거나 이부분만 mybatis를 사용하는 등의 방법으로 해결할 수 있다.

 

출처 : DDD Start 도메인 주도 설계 구현과 핵심 개념 익히기 (출판 지앤선, 저자 최범균)

댓글()

스프링 부트에서 사용하는 JPA 기능 정리

web/JPA|2018. 11. 4. 23:53

스프링 프레임워크에서 제공하는 JPA는 별도의 구현 클래스 없이 인터페이스만을 사용할 수 있도록 제공한다. 제공되는 인터페이스 JpaRepository는 실행시점에 자동으로 인터페이스 내용을 연결하는 엔티티에 맞게 자동으로 구현해준다. 만약 스프링 JPA 인터페이스에서 제공하지 않는 기능을 사용하고 싶을 때는 메서드명을 특정한 규칙대로 만들어서 사용하면 인터페이스가 알아서 그 이름에 맞는 JPQL을 만들어서 실행해준다.


스프링 JPA 인터페이스는 Mysql같은 RDBMS 뿐만 아니라 Mongodb, Redis와 같은 NoSQL에도 동일한 인터페이스를 사용해서 기능을 사용할 수 있도록 제공해준다. 공통으로 사용할 수 있기에 아주 편리하다.


우선 스프링 부트에 JPA를 사용하기 위해서 Gradle에 라이브러리를 넣자.

1
compile ('org.springframework.boot:spring-boot-starter-data-jpa')
cs

 

기본적인 구조

JpaRepository를 상속받아 구현하고자 하는 인터페이스를 만들고 제네릭에 구현하려는 엔티티와 엔티티의 식별자 타입을 지정하여 인터페이스를 선언한다.

1
2
3
public interface StudentRepository extends JpaRepository<Student, Long> {
}
 
cs


주요 메서드 몇개만 정리해보자.

save() : 저장하거나 업데이트한다.

delete(entity) : em.remove() 호출하여 엔티티를 제거한다.

findOne(ID) : em.find() 호출하여 엔티티를 찾는다.

findAll() : 엔티티 모두를 조회한다. 


JpaRepository 인터페이스로 부족한 기능을 구현한 PagingAndSortingRepository CrudRepository 사용해서 보완할 있다.


JPA에서 메서드 사용하는 방법

JPA에서 엔티티에 맞는 메서드를 사용하는 방법은 크게 3가지이다.

- 메서드 이름으로 쿼리 생성

- 메서드 이름으로 JPA NamedQuery 호출

- @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의


1. 메서드 이름으로 쿼리 생성

메서드 이름으로 쿼리를 생성할 있는데 정식 Document 이용하면 자세히 나와있다

https://docs.spring.io/spring-data/jpa/docs/2.1.2.RELEASE/reference/html/#jpa.query-methods


2. 메서드 이름으로 JPA NamedQuery 호출

JPA NamedQuery 쿼리에 이름을 부여해서 사용하는 방법은 다음과 같이 엔티티에 선언해주면 된다.

1
2
3
4
5
@NamedQuery(
  name="student.findByName",
  query="select s from student s where s.name = :name")
public class Student {

}
cs


위와 같이 선언하고 실제 사용할 때는 entityManager에 아래와 같이 createQuery를 사용해서 쿼리를 호출하면 된다.

1
2
3
4
5
6
7
@PersistenceContext
private EntityManager entityManager;
 
public List<Student> findByUser(String name) {
  List<Student> students = entityManager.createQuery("student.findByName", Student.class).setParameter("name""wedul").getResultList();
}
 
cs


위와 같이 EntityManager를 사용할 수 있지만 스프링 JPA를 사용하여 간단하게 메소드 이름만으로 호출이 가능하다. 이렇게 호출하면 레포지토리에서 Student.쿼리메소드 형태로 쿼리를 찾는다. 만약 실행할 쿼리가 없으면 메서드 이름으로 쿼리를 자동으로 바꿔 동작한다.

1
2
3
4
5
public interface StudentRepository extends JpaRepository<Student, Long> {
  
  List<Student> findByUserName(@Param("name") String name);
  
}
cs


3. @Query 어노테이션을 사용해서 레포지토리 인터페이스에 쿼리 직접 정의

2번에서는 @Entity 클래스에서 정의한 쿼리를 레포지토리에서 메소드 형태로 접근하여 사용하였다.  이번에는 레포지토리에서 직접적으로 쿼리를 만들어서 조회하는 방식을 확인해보자.


@Query("select s from Student s where s.name = ?1")

Student findByName(String name);


인터페이스에 정의하는 메소드에 @Query 어노테이션을 붙혀서 정의하고 사용하면 된다. 바인딩 값은 1부터 시작한다. 스프링 데이터 JPA에서는 ?1 ?2 같은 위치기반 파라미터와 :name 같은 이름 기반 방식을 모두 사용가능하다.


페이징과 정렬

스프링 데이터 JPA에서 쿼리 메서드에 페이징과 정렬 기능을 사용할 있다. 파라미터로 Pageable 인터페이스를 사용할 경우에는 Page 또는 List 반환 받을 있다.

1
2
3
4
public interface StudentRepository extends JpaRepository<Student, Long> {
 
  Page<Student> findByNameStartingWith(String name, Pageable pageable);
}
cs


실제 사용할 때는 Pageable 인터페이스이기 때문에 구현체인 PageRequest 객체를 사용해서 사용한다.

// 파라미터 순서대로 페이지 번호, 사이즈, 정렬 기준 등으로 사용한다.

1
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Sort.Direction.DESC, "name"));
cs


반환되는 값인 Page에서 제공하는 다양한 메소드를 사용해서 편하게 페이징과 소트 기능을 사용할 있다. 

컨트롤러에서 사용자 요청에게 전달되는 페이징 정보를 받기 위해서는 다음과 같이 Pageable 인터페이스를 받으면 되고 받을 속성값은 page, size, sort 사용해서 받는다. (/student?page=0&size=20&sort=name,desc&sort=address.city)


1
2
3
4
5
6
7
@GetMapping("/student")
public String list(Pageable pageable, Model model) {
  Page<Student> page = studentRepository.findByNameStartingWith("dbsafer", pageable);
 
  return "dfdfdf";
}
 
cs


참고 싸이트

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html


출처. : 자바 ORM 표준 JPA 프로그래밍


댓글()

JPA 상속관계 매핑 전략

web/JPA|2018. 10. 31. 00:58

객체 지향으로 데이터베이스 중심 매핑을 변경하기 위해서 가장 애매한게 상속이다. 이런 상속관계속에서 테이블로 구현할 3가지 방법을 선택할 있다.


1) 각각의 테이블로 변환 : 각각을 모두 테이블로 만들고 조회할 조인을 사용.

2) 통합 테이블로 변환 : 테이블을 하나만 사용해서 통합 

3) 서브타입 테이블로 변환 : 서브 타입마다 하나의 테이블을 만드는 방식.



순서대로 하나씩 정리해보자.




각각의 테이블로 변환 (조인전략)

- 부모와 각각의 자식 엔티티를 모두 각자의 테이블로 만들고 부모의 기본키와 자식의 외래키를 사용하여 조인하여 사용한다.

-  자식 엔티티의 타입을 구별하기 위한 DTYPE 컬럼을 구분컬럼으로 추가하여 사용한다. (없어도 무관)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Culture {
  
  @Id @GeneratedValue
  @Column(name = "CULTURE_ID")
  private Long id;
  
  private String name;
  private int price;
  
}
 
cs

-> 부모 추상 클래스 Culture 선언된 애노테이션에 대한 설명은 다음과 같다.


1
2
3
4
5
6
7
8
9
10
11
12
@Inheritance(strategy = InheritanceType.JOINED) : 조인전략 사용을 의미
@DiscriminatorColumn(name = "DTYPE") : 부모 클래스에 구분 컬럼으로 써 자식 테이블을 구분할 때 사용할 키로 사용된다. 기본값은 DTYPE이며 바꿔서 사용가능 (해당 기능을 사용하지 않아도 무관하다.)
 
 
@Entity
@DiscriminatorValue("M")
public class Movie {
 
  private String artiest;
  private String genre;
 
}
cs


기본적으로 사용하는 자식클래스 형식이다. 기본적으로 부모 테이블의 ID 컬럼명을 승계받아 사용 하지만 만약 변경하고 싶은 경우 클래스 위에 @PrimaryKeyJoinColumn(name = "MOVIE_ID")처럼 정의해서 사용할 있다.


조인전략의 장점은 저장공간을 효율적으로 관리하거나 테이블의 정규성이 지켜진다는 점이 있지만 조회할 조인이 많아지고 조회 쿼리시 귀찮아지며 수정이 발생하면 부모와 자식 테이블 두번을 해주어야한다.

 


 통합 테이블로 변환 (단일 테이블 전략)

부모의 속성과 자식의 속성을 하나의 테이블로 사용하는 것이다. 그리고 구분 컬럼 DTYPE 추가하여 해당 테이블이 어떤 자식 엔티티를 기반으로 만들어진 테이블인지 구분한다. (DTYPE 어떤자식인지 구분하기 위해 사용한다. 만약 속성이 같은 자식인 경우 구분이 안되기 때문이다.) 해당 전략의 단점으로는 자식 엔티티에 추가된 필드는 필수가 아니기 때문에 null 입력 되어도 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Culture {
 
  @Id @GeneratedValue
  @Column(name = "CULTURE_ID")
  private Long id;
 
  private String name;
  private int price;
 
}
 
cs

-> Inheritance(strategy = InheritanceType.SINGLE_TABLE) 조인 타입을 단일 테이블 전략으로 지정한다. 


1
2
3
4
5
6
7
@Entity
@DiscriminatorValue("M")
public class Movie extends Culture {
 
  private String genre;
 
}
cs


 하나의 테이블로 지정되기 때문에 조인전략과 다르게 자식클래스를 구분할 있는 DTYPE 무조건 추가되어야 한다. 방식은 하나로 묶여있어 쿼리가 단순하다는 장점이 있지만 자식 엔티티의 필드의 null 무조건 허용해줘야 한다는 것과 자식마다 성격이 모두 달라도 테이블에 필요없는 필드까지 추가되어 있어야 하는 문제점이 있다.



- 서브타입 테이블로 변환 (구현 클래스마다 테이블 전략)

 자식 엔티티 마다 별도의 모든 테이블을 만들어 주는 방식이다. 부모와 자식을 키로 묶어서 사용하는 조인 방식과 다르게 자식 마다 부모의 속성과 자식의 속성을 모두 포함한 엔티티를 개별적으로 모두 만드는 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Culture {
 
  @Id @GeneratedValue
  @Column(name = "CULTURE_ID")
  private Long id;
 
  private String name;
  private int price;
 
}
cs


- 단일테이블 전략과 다르게 모든 자식 테이블이 별도로 만들어지기 때문에 필요없는 필드가 추가되지 않아서 not null조건을 넣어줄 있다. 하지만 모두 만들어야하고 자식테이블끼리 합칠 성능의 문제가 발생할 있다. 가장 비효율적인 방법으로 일반적으로 조인이나 단일 테이블 전략중에서 사용한다.


- 참고 JAVA ORM 표준 JPA 프로그래밍


댓글()

JPA 관계 유형별 엔티티 설정 방법

web/JPA|2018. 10. 31. 00:14

JPA에서 관계 유형별로 엔티티를 설정하는 방법을 정리해보자.



1. 다대일 (단방향)

-> 다쪽에 @ManyToOne 으로 설정


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
@Entity
@Table(name = "student")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
 
  @Id
  @Column(name = "STUDENT_ID")
  private String id;
 
  @Column(name = "name")
  private String name;
 
  @ManyToOne
  @JoinColumn(name = "CLASSES_ID")
  private Classes classes;
 
  public void setClasses(Classes classes) {
    // 먼저 지워준다.
    classes.getStudents().remove(this);
 
    // 그리고 반을 바꾸고 학생추가
    this.classes = classes;
    classes.getStudents().add(this);
  }
 
}
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Table(name = "classes")
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Classes {
 
  @Id
  @Column(name = "CLASSES_ID")
  private String id;
 
  @Column(name = "name")
  private String name;
}
 
cs


2. 다대일 양방향

-> 다쪽에 @ManyToOne, 일쪽에 @OneToMany 지정해서 사용

-> 다쪽인 Student에서 외래키를 가지고 있으므로 직접 조작할 있고 반대쪽은 조회만 가능

-> 이전시간에 정리한거 처럼 setClasses(), addStudent()등에 메서드에 빈곳이 없도록 해야한다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Entity
@Table(name = "student")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
 
  @Id
  @Column(name = "STUDENT_ID")
  private String id;
 
  @Column(name = "name")
  private String name;
 
  @ManyToOne
  @JoinColumn(name = "CLASSES_ID")
  private Classes classes;
 
  public void setClasses(Classes classes) {
    // 먼저 지워준다.
    classes.getStudents().remove(this);
 
    // 그리고 반을 바꾸고 학생추가
    this.classes = classes;
    classes.getStudents().add(this);
  }
 
}
 
@Table(name = "classes")
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Classes {
 
  @Id
  @Column(name = "CLASSES_ID")
  private String id;
 
  @Column(name = "name")
  private String name;
 
  @OneToMany(mappedBy = "classes")
  private List<Student> students;
 
}
cs


3. 일대다 (단방향)

-> 일대다에서 외래키가 다쪽에 존재

-> 다대일 단방향에서 일쪽에 @JoinColumn(name = "CLASSES_ID") 추가된 형태

-> 일대다보다 있으면 다대일을 사용하자.

1
2
3
4
@JoinColumn(name = "CLASSES_ID")
@OneToMany(mappedBy = "classes")
private List<Student> students;
 
cs


4. 일대다 (양방향)

-> 일대다 매핑은 존재하지 않고 다대일 양방향으로 사용해야한다.



5. 일대일

-> 양쪽이 서로 하나의 관계만 가지는 .

-> 양쪽 테이블 모두 외래키를 가질 있다.

-> @OneToOne으로 매핑하고 주인이 엔티티에 @JoinColumn 사용하고, 나머지 엔티티에는  mappedBy 옵션을 사용한다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Entity
@Table(name = "student")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
 
  @Id
  @Column(name = "STUDENT_ID")
  private String id;
 
  @Column(name = "name")
  private String name;
 
  @ManyToOne
  @JoinColumn(name = "CLASSES_ID")
  private Classes classes;
 
  @OneToOne
  @JoinColumn(name = "LOCKER_ID")
  private Locker locker;
 
  public void setClasses(Classes classes) {
    // 먼저 지워준다.
    classes.getStudents().remove(this);
 
    // 그리고 반을 바꾸고 학생추가
    this.classes = classes;
    classes.getStudents().add(this);
  }
 
}
 
@Table(name = "classes")
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Locker {
 
  @Id
  @Column(name = "LOCKER_ID")
  private String id;
 
  @Column(name = "location")
  private String location;
 
  @OneToOne(mappedBy = "locker")
  private Student student;
}
cs


6. 다대다

-> 데이터베이스에서 사용하지 않는 다대다를 사용하지 말고 중간에 중점을 두어 다대일로 구별하여 처리한다.

-> 예를 들어 학생과 책의 다대다 관계를 지칭한다 했을 중간에서 학생과 책의 관계를 담당하는 엔티티 StudentBook 있다고 가정해보자.

-> Student에는 관계 테이블에서 사용될 키가 보함된   @OneToMany(mappedBy = "student") 설정한다.

-> 관계 엔티티 에는 키를 관리하는 별도의 객체를 만들어서 @IdClass(StudentBookId.class) 설정해준다.


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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@Entity
@Table(name = "student")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
 
  @Id
  @Column(name = "STUDENT_ID")
  private String id;
 
  @Column(name = "name")
  private String name;
 
  // 다대일
  @ManyToOne
  @JoinColumn(name = "CLASSES_ID")
  private Classes classes;
 
  // 일대일
  @OneToOne
  @JoinColumn(name = "LOCKER_ID")
  private Locker locker;
 
  // 다대다(중간에 관계테이블 있는경우)
  @OneToMany(mappedBy = "student")
  private List<Book> books;
 
  public void setClasses(Classes classes) {
    // 먼저 지워준다.
    classes.getStudents().remove(this);
 
    // 그리고 반을 바꾸고 학생추가
    this.classes = classes;
    classes.getStudents().add(this);
  }
 
}
 
 
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
 
  @Id
  @Column(name = "PRODUCT_ID")
  private String id;
 
  private String name;
 
}
 
@Entity
@IdClass(StudentBookId.class)
public class StudentBook {
 
  @Id
  @ManyToOne
  @JoinColumn(name = "STUDENT_ID")
  private Student student;
 
  @Id
  @ManyToOne
  @JoinColumn(name = "BOOK_ID")
  private Book book;
 
}
 
@EqualsAndHashCode
public class StudentBookId implements Serializable {
 
  private String student;
  private String book;
 
}
 
cs


댓글()

연관관계 매핑 (다대일 - 양방향)

web/JPA|2018. 10. 26. 00:18

바로 앞에서 다대일 관계에서 단반향으로써 학생이 반을 접근하는 방식으로 진행했으나 이번에는 반에서 학생들을 접근하는 방식을 사용해보자.


그렇게 되면 학생 -> 반에서 반 -> 학생이 추가되어 결국 반 <-> 학생 이런 양방향 연관관계가 형성된다.

 

하나의 반에는 여러 학생이 포함되어 있다. 그렇기 때문에 반 클래스에 List<Student> 객체를 추가한다.


1

2

  @OneToMany(mappedBy = "classes")

  private List<Student> students;

cs

 


@OneToMany(mappedBy = "classes")

- 일대다 매핑을 정보를 추가하고 학생쪽에서 사용되는 반 필드명을 mappedBy에 값으로 추가해준다.


조회

반에 포함되어 있는 학생들을 조회한다.

1

2

3

4

5

6

7

8

9

10

@Override

@Transactional

public void selectClasses() {

  Classes classes1_1 = entityManager.find(Classes.class"1-1");

  List<Student> students = classes1_1.getStudents();

 

  for (Student student : students) {

    print(student);

  }

}

Colored by Color Scripter

cs

 


연관관계 주인 지정


테이블은 외래키 하나로 테이블의 연관관계를 관리 있다. 예를 들면 이름이 외래키라고 했을 학생 테이블에서 외래키 이름을 추가할 수도 있고, 테이블에서 반 이름을 관리할 있다. 하지만 엔티티에서는 외래키를 관리(추가, 수정, 삭제) 있는 것은 개의 엔티티의 연관관계의 주인이 되는 엔티티만이 가능하다. 나머지 다른 엔티티는 조회만 가능하다.

 

예를 들어 저번 시간에 공부 했었던 Student 엔티티 클래스는 Classes 외래키의 주인으로써 외래키를 추가, 수정, 삭제 있다. Classes 엔티티 클래스는 외래키의 주인이 아니므로 조회만 가능하다.

 

@ManyToOne 설정이 있는 곳이 무조건 주인이다. 그리고 양방향 설정된 엔티티에서 조회가 가능하도록 하기 위해서 다른 엔티티에 @OneToMany(mappedby ="classes") 지정해 주면 주인 설정이 끝난다.

 

그럼 진짜 주인이 아닌 엔티티 Classes에서는 외래키 관리가 안되는지 확인해보자.

 

1

2

3

4

5

6

7

@Override

@Transactional

public void saveClasses() {

  Student wedul = entityManager.find(Student.class"1-1-01");

  Classes classes2_1 = new Classes("2-2""2학년2", Arrays.asList(wedul));

  entityManager.persist(classes2_1);

}

Colored by Color Scripter

cs

-> 처음 생각대로라면 id : "2-2", name : "2학년2" 반이 classes테이블에 추가되고 학생 테이블에 wedul 학생의 반이 2-2 같지만 그렇지 않다. 왜냐하면 Classes 엔티티는 주인이 아니기 때문이다. 그래서 테이블에 값만 추가된다.



주의사항

 만약 단방향 그러니까 Student 엔티티에만 @ManyToOne 해줄 경우 Classes 엔티티를 통해 반에 등록된 학생을 조회 하려 값을 받게 된다. 왜냐하면 연관관계가 맺어지지 않았기 때문이다. 그래서 무조건 이럴 경우 양방향 연관관계를 맺어 주는것이 좋다.(@OneToMany)

 

아래 코드를 보면 wedul학생에 classes2_1 반을 추가해줬지만 classes2_1에서 학생을 조회하면 wedul 학생이 없다.

1

2

3

4

5

6

Student wedul = entityManager.find(Student.class, "1-1-01");

Classes classes2_1 = new Classes("2-2", "2학년2", Collections.emptyList());

wedul.setClasses(classes2_1);

    

// 반영 되어 있지 않아서 wedul 출력되지 않음

classes2_1.getStudents();

Colored by Color Scripter


그래서 이런 문제로 버그가 발생할 있기 때문에 좋은 방법으로 setClasses() 메소드를 다음과 같이 변경해주면 좋다.

1

2

3

4

public void setClasses(Classes classes) {

  this.classes = classes;

  classes.getStudents().add(this);

}

Colored by Color Scripter

그렇지만 이렇게만 해주고 나면 또다른 버그가 발생할 있다. 다음과 같은 상황을 가정해보자.

1

2

3

4

5

6

7

8

9

Student wedul = entityManager.find(Student.class, "1-1-01");

Classes classes2_1 = new Classes("2-2", "2학년2", Collections.emptyList());

wedul.setClasses(classes2_1);

 

classes2_1.getStudents();

 

// 학생의 반을 다른 반으로 변경할 경우 기존의 반에 들어있는 getStudents List안에서 학생을 지워줘야한다.

Classes classes3_1 = new Classes("3-1", "3학년1", Collections.emptyList());

wedul.setClasses(classes3_1);

Colored by Color Scripter

상황에서는 위에 변경해주었던 방식대로 진행하면 기존에 반이었던 classes2_1에도 wedul이 있고 classes3_1에도 wedul 있는 문제가 발생한다. 그래서 다음과 같이 바꿔주면 해결된다. 

1

2

3

4

5

6

7

8

  public void setClasses(Classes classes) {

    // 먼저 지워준다.

    classes.getStudents().remove(this);

    

    // 그리고 반을 바꾸고 학생추가

    this.classes = classes;

    classes.getStudents().add(this);

  }

Colored by Color Scripter


단방향 매핑만으로도 테이블과 객체의 연관관계 매핑이 되었지만 양방향 매핑을 통해서 더욱 편리하게 객체의 탐색이 가능하게 있다. 하지만 위에 보았듯이 양방향 매핑에서는 주의해서 관리 해줘야 포인트가 많다.


댓글()

연관관계 매핑 - 다대일 매핑 (단반향)

web/JPA|2018. 10. 23. 18:30

연관관계 매핑을 해야하는 경우가 많다. 예를 들어 학생을 가지고 학생에 소속 반을 찾거나 반을 사용해서 학생들을 찾거나 때가 있다.

 

양방향과 단방향 관계가 존재하는데 아래의 객체 형태를 보면 이해가 쉽다.

 

단방향

class Student {

 Class class;

}

 

class Class {}

 

양방향

class Student {

 Class class;

}

 

class Class {

 Student student;

}


이중에서 먼저 단방향 연관 관계에 대해 먼저 공부해보자.

 

단방향 연관관계

  • 학생과 반이있다.
  • 학생은 하나의 반에 소속된다.
  • 학생과 반은 다대일 관계이다. (학생이 , 반이 )
  • 학생 테이블을 담는 객체는 Student, 반 테이블을 담는 객체는 Classes를 사용한다.
  • 학생 테이블은 반 테이블의 키 CLASSES_ID를 외래키로 테이블과 연관 관계를 가진다.

 

학생 테이블이 다, 반 테이블이 일로 다대일 관계이기 때문에 Student 클래스에 포함된 Classes 객체에 다음과 같이 선언한다.


1
2
3
@ManyToOne
@JoinColumn(name="CLASSES_ID")
private Classes classes;
cs


@ManyToOne

- 다대일 관계라는 표현

 

 

@JoinColumn(name = "CLASSES_ID")

- 조인 컬럼은 외래키를 매핑할 때 사용한다. 이 어노테이션은 생략해도 된다.

- 만약 생략 할 경우 외래키는 다음과 규칙으로 찾는다. 필드명 + _ + 참조하는 테이블의 컬럼명 그래서 위에서는 생략할 경우 classes_CLASSES_ID의 형태의 외래키를 찾는다.


다대다 테이블 관계 매핑설정이 끝난 객체


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
32
package com.wedul.springboottest.student.dto;
 
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
 
/**
 * springboottest
 *
 * @author wedul
 * @since 23/10/2018
 **/
@Table(name = "classes")
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Classes {
 
  @Id
  @Column(name = "CLASSES_ID")
  private String id;
 
  @Column(name = "name")
  private String name;
 
}
cs
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
32
33
package com.wedul.springboottest.student.dto;
 
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
 
import javax.persistence.*;
 
/**
 * springboottest
 *
 * @author wedul
 * @since 23/10/2018
 **/
@Entity
@Table(name = "student")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
 
  @Id
  @Column(name = "STUDENT_ID")
  private String id;
 
  @Column(name = "name")
  private String name;
 
  @ManyToOne
  @JoinColumn(name = "CLASSES_ID")
  private Classes classes;
 
}
cs


삽입

그럼 이 다대다 관계를 이용해서 데이터를 넣어보자. entityManager를 데이터를 삽입하는 코드를 만들자

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
32
33
34
35
36
37
38
39
40
41
42
package com.wedul.springboottest.student.serviceImpl;
 
import com.wedul.springboottest.student.dto.Classes;
import com.wedul.springboottest.student.dto.Student;
import com.wedul.springboottest.student.service.StudentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
 
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
 
/**
 * springboottest
 *
 * @author wedul
 * @since 23/10/2018
 **/
@Service
@Slf4j
public class StudentServiceImpl implements StudentService {
 
  @PersistenceContext
  private EntityManager entityManager;
 
  public StudentServiceImpl(EntityManager entityManager) {
    this.entityManager = entityManager;
  }
 
  @Override
  @Transactional
  public void insertStudent() {
    Classes classes1_1 = new Classes("1-1""1학년 1반");
    entityManager.persist(classes1_1);
 
    Student wedul = new Student("1-1-01""wedul", classes1_1);
    entityManager.persist(wedul);
 
    Student chul = new Student("1-1-02""chul", classes1_1);
    entityManager.persist(chul);
  }
}
cs


테스트 코드를 통해 테스트해보자.

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
package com.wedul.springboottest.member;
 
import com.wedul.springboottest.member.service.MemberService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
/**
 * springboottest
 *
 * @author wedul
 * @since 03/10/2018
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest // rolleback 설정
public class MemberTest {
 
  @Autowired
  MemberService memberService;
 
  @Test
  public void jpa_test() {
    memberService.jpaService();
  }
 
}
 
cs


조회

1

2

3

4

5

6

@Override

@Transactional

public void selectStudent() {

  Student wedul = entityManager.find(Student.class"1-1-01");

  print(wedul);

}

Colored by Color Scripter

cs

 

출력 결과

아이디 : 1-1-02, 이름 : chul, 소속반: 1?? 1?

 

JPQL을 사용한 조회

1

2

3

4

5

6

7

8

9

 

  @Override

  @Transactional

  public void selectStudentWithJPQL() {

    String jpql = "select s from Student s join s.classes c where s.name=:studentName";

 

    Student student = entityManager.createQuery(jpql, Student.class).setParameter("studentName""chul").getSingleResult();

    print(student);

  }

Colored by Color Scripter

cs

 

 


연관관계제거


1

2

3

4

5

6

@Override

@Transactional

public void removeClasses() {

  Student wedul = entityManager.find(Student.class"1-1-01");

  wedul.setClasses(null);

}

Colored by Color Scripter


wedul 학생의 반 정보가 지워진것을 알 수 있다.


생각보다 어렵지 않다. 

댓글()

JPA 매핑 어노테이션 - DDL 2

web/JPA|2018. 10. 13. 01:10

엔티티 매핑에서 사용될 컬럼의 필드 유형을 설정하는 매핑 어노테이션을 정리해보자.

@Column
테이블에서 사용 되는 컬럼이라는 필드를 지정해줄때 사용하며 name, nullable(기본이 true) 등의 설정을 해줄 수 있다. 

1
2
@Column(name = "NAME", length = 10, nullable = true)
private String userName;
cs


@Enumerated
자바의 enum 타입을 매핑할 때 사용한다. 속성으로 EnumType.ORDINAL과 EnumType.STRING이 존재하는데 이름 그대로 ORDINAL은 순서를 STRING은 Enum의 이름을 저장한다

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
public class Member {
 
  @Id
  @GeneratedValue
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME"length = 10, nullable = true)
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
  @Enumerated(EnumType.STRING)
  Gender gender;
 
}
 
enum Gender {
  Men, Women;
 
  private Gender() {
 
  }
}
 
// 위와 같이 설정하면 데이터베이스에 Men으로 들어간다.
member.setGender(Gender.Men);
cs


@Temporal
java.util.Date와 java.util.Calendar 값을 매핑 할 때 사용한다.

1
2
3
4
5
6
7
8
9
10
11
// 2018-04-02 형태 (데이터베이스 DATE와 매핑)
@Temporal(TemporalType.DATE)
private Date birthDate;
 
// 12:11:11 (데이터베이스 TIME과 매핑)
@Temporal(TemporalType.TIME)
private Date birthTime;
 
// 2013-10-21 12:11:11 (데이터베이스 TIME과 매핑)
@Temporal(TemporalType.TIMESTAMP)
private Date birthTimeStamp;
cs


@LOB
데이터베이스 BLOB, CLOB 타입과 매핑 된다. CLOB(String, char[], java.sql.CLOB)은 문자, BLOB(byte[], java.sql.BLOB)은 나머지가 매핑된다.

@Transient
저장 조회에 사용되지도 않고 그냥 단순 값을 가지고 있고 싶을때 사용.

1
2
@Transient
private String tempStr;
cs


@Access
데이터베이스에 엔티티에 값이 저장될 때 필드(AccessType.FIELD)의 값을 직접 접근해서 사용할 것인가 아니면 메서드에 직접(AccessType.PROPERTY) 접근할 것 인가를 설정하는 것이다.

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
32
33
@Access(AccessType.FIELD)
public class Member {
 
  @Id
  @GeneratedValue
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME"length = 10, nullable = true)
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
  @Enumerated(EnumType.STRING)
  Gender gender;
 
  // 2018-04-02 형태 (데이터베이스 DATE와 매핑)
  @Temporal(TemporalType.DATE)
  private Date birthDate;
 
  // 12:11:11 (데이터베이스 TIME과 매핑)
  @Temporal(TemporalType.TIME)
  private Date birthTime;
 
  // 2013-10-21 12:11:11 (데이터베이스 TIME과 매핑)
  @Temporal(TemporalType.TIMESTAMP)
  private Date birthTimeStamp;
 
  @Transient
  private String tempStr;
 
}
cs


- @Access 필드를 생략하고 @Id 필드를 사용하면 AccessType.FIELD로 설정된 것과 같다.
- 나머지 필드는 @Id를 사용하여 AccessType.FIELD로 사용하고 특정 값만 AccessType.PROPERTY로 설정할 수 있다.

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
32
33
34
35
36
37
38
@Access(AccessType.FIELD)
public class Member {
 
  @Id
  @GeneratedValue
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME"length = 10, nullable = true)
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
  @Enumerated(EnumType.STRING)
  Gender gender;
 
  // 2018-04-02 형태 (데이터베이스 DATE와 매핑)
  @Temporal(TemporalType.DATE)
  private Date birthDate;
 
  // 12:11:11 (데이터베이스 TIME과 매핑)
  @Temporal(TemporalType.TIME)
  private Date birthTime;
 
  // 2013-10-21 12:11:11 (데이터베이스 TIME과 매핑)
  @Temporal(TemporalType.TIMESTAMP)
  private Date birthTimeStamp;
 
  @Transient
  private String tempStr;
 
  @Access(AccessType.PROPERTY)
  public String getFullName() {
    return "dbsafer" + this.userName;
  }
 
}
cs


댓글()