[hibernate] 단방향/양방향 연관성 구현 (OneToMany, ManyToOne)

개요

Object Relational Mapping(ORM)을 사용하기 위해 도메인 주도 설계(Domain Driven Design)를 공부하던 중에 단방향/양방향 연관성에 대해 알게 되었고, 그것을 구현하려면 OneToOne, OneToMany, ManyToOne, ManyToMany를 사용해야 한다.
그러나 막상 모델을 저장하려 했더니 원하는 테이블과 컬럼에 저장하는 것이 쉬운일이 아니였다. 예제를 보면서 확인해보자.

예제

먼저 예제는 부서인 Department 안에 속하는 사람들을 Member 로 표현하겠다.


테이블 구조


 tbl_dept

 dept_no


 tbl_member

 member_no

dept_no 


목표 : 객체 간의 양방향/단방향 레퍼런스를 위 테이블 구조로 저장


예제1 : 양방향 (Department <-> Member)


Department.java

package com.tistory.jekalmin.domain;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

    @OneToMany(mappedBy="dept")
    private Collection<Member> members;


}

mappedBy 이후에 들어오는 문자열은 Member.java에서 Department를 가리키는 변수명과 일치해야한다.

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

    @ManyToOne
    @JoinColumn(name="dept_no")
    private Department dept;

}

JoinColumn은 명시하지 않아도 알아서 ID를 찾지만, 그럴 경우 컬럼 명이 원하는 대로 생성되지 않기 때문에 JoinColumn을 이용해 명시해주었다.



예제2 : 단방향 (Department <- Member)


이번 예제에서는 Member에서만 Department를 참조하는 단방향 예제이다.

Department.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

}

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

    @ManyToOne
    @JoinColumn(name="dept_no")
    private Department dept;

}

양방향과 다른 점이라면 Department에서는 Member를 더이상 가지고 있지 않다.



예제3 : 단방향 (Department -> Member)


이번에는 Department에서만 Member를 참조하는 단방향 예제이다.

Department.java

package com.tistory.jekalmin.domain;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

    @OneToMany
    @JoinColumn(name="dept_no")
    private Collection<Member> members;

}

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

}

이전 예제들과 약간 다른 점이라면, 이전에는 JoinColumn을 썼던 이유가 원하는 컬럼명을 명시하기 위해서 였는데, 이번 예제에서는 JoinColumn을 사용하지 않으면 전혀 다른 테이블 구조가 생성이 된다. 그리고 Department.java의 JoinColumn 안에 들어가는 컬럼명은 tbl_member 에서 tbl_dept를 참조할 컬럼명이다.

JoinColumn을 제외하고 실행하면 아래와 같은 테이블이 생성된다.

create table tbl_dept (dept_no integer generated by default as identity (start with 1), primary key (dept_no));

create table tbl_dept_members (tbl_dept_deptNo integer not null, members_memberNo integer not null);

create table tbl_member (member_no integer generated by default as identity (start with 1), primary key (member_no));

tbl_dept_members라는 원하지 않았던 테이블이 생기고, tbl_member는 dept_no를 가지고 있지 않은 구조로 된다.

그래서 JoinColumn을 이전 두 예제에서는 생략해도 컬럼명만 바뀌지만, 이 경우엔 테이블 구조가 예상치 못한 결과가 나왔다.



결론

OneToMany, ManyToOne 어노테이션은 위에 달아만 주면 알아서 구조를 만들어 주기는 하지만, 그 구조가 내가 생각했던 테이블과 컬럼 구조 인지는 잘 확인해 보시길 바란다. 위 예제는 전부 같은 테이블 구조를 생성한다.


[spring-data-jpa] 기본 예제

소개

spring-data-jpa는 jpa 기반 저장소를 쉽게 구현하도록 만들어준다. 이중에서도 눈에 띄는 특징들은

  • type-safe한 쿼리가 가능하다.
  • 공통된 저장소 기능들(CRUD)을 제공해준다.
  • @Query 어노테이션을 제공한다.

그 외에도 querydsl의 predicate 제공이나 xml 기반의 entity 매핑등의 특징들도 보인다.

예제

먼저 Spring Project로 프로젝트를 생성하고 dependency를 추가하자.

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>1.7.0.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.3.2</version>
</dependency>


스프링 프로젝트가 기본 dependency를 등록해주므로 우리는 두개만 더 등록해주자.

다음 Article.java 라는 엔티티를 만들어보자.


Article.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Article {

    @Id        // primary key
    @GeneratedValue        // auto increment
    private int articleNo;
    private int memberNo;
    private String title;
    private String content;
    /* 컬럼명은 필드명이랑 동일하게 된다. 따로 명시해주고 싶으면 @Column을 사용하자 */

    /**
     * 하이버네이트가 파라미터 없는 생성자를 호출하기 때문에, 파라미터 있는 생성자를 생성했을 경우
     * 꼭 파라미터 없는 생성자를 만들어 줘야 한다.
     */
    public Article(){}

    public Article(int memberNo, String title, String content) {
        this.memberNo = memberNo;
        this.title = title;
        this.content = content;
    }

    public int getArticleNo() {
        return articleNo;
    }
    @SuppressWarnings("unused")
    private void setArticleNo(int articleNo) {
        this.articleNo = articleNo;
    }
    public int getMemberNo() {
        return memberNo;
    }
    public void setMemberNo(int memberNo) {
        this.memberNo = memberNo;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "Article [articleNo=" + articleNo + ", memberNo=" + memberNo
                + ", title=" + title + ", content=" + content + "]";
    }


}


그리고 Article 엔티티의 저장소를 만든다.


ArticleRepository.java

package com.tistory.jekalmin.repository;

import org.springframework.data.repository.CrudRepository;

import com.tistory.jekalmin.domain.Article;

public interface ArticleRepository extends CrudRepository<Article, Integer> {

}


이제 applicationContext 설정만 하면 된다.


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
        http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <context:component-scan base-package="com.tistory.jekalmin"/>
    <context:annotation-config/>

    <jdbc:embedded-database id="dataSource" type="HSQL"></jdbc:embedded-database>

    <!-- spring data jpa 설정 -->

    <!-- jpa repository가 위치한 패키지 경로 등록 -->
    <jpa:repositories base-package="com.tistory.jekalmin.repository" entity-manager-factory-ref="entityManagerFactory"/>

    <!-- 하이버네이트의 SessionFactory 에 상응하는 jpa의 EntityManagerFactory 등록 -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop> <!-- Hsql 형식에 맞게 변환해주는 클래스 -->
                <prop key="hibernate.connection.pool_size">1</prop>
                <prop key="hibernate.connection.shutdown">true</prop> <!-- hsql에 있는 마지막 연결이 끊어지면 데이터베이스 shutdown 하는 플래그 -->
                <prop key="hibernate.show_sql">true</prop> <!-- SQL 출력 -->
                <prop key="hibernate.hbm2ddl.auto">create</prop> <!-- 테이블 자동 생성 -->
            </props>
        </property>
        <!-- 엔티티 정의된 클래스들이 있는 패키지 등록 -->
        <property name="packagesToScan" value="com.tistory.jekalmin.domain"/>
    </bean>
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"></property>
    </bean>
    <!-- spring data jpa 설정 끝 -->



</beans>


설정 부분이 쉽지 않은데, 약간의 주석들이 도움이 되길 바란다.

이제 테스트할 클래스만 만들고 테스트하면 된다.


ArticleRepositoryTest.java

package com.tistory.jekalmin.test;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.tistory.jekalmin.domain.Article;
import com.tistory.jekalmin.repository.ArticleRepository;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/application-config.xml")
public class ArticleRepositoryTest {

    @Resource
    ArticleRepository articleRepository;

    @Test
    public void write(){

        articleRepository.save(new Article(11, "제목", "내용"));

        System.out.println(articleRepository.findAll());

    }


}


spring-data-jpa의 가장 강력한 기능중 하나가 Repository의 기본 기능을 제공하는 것 같다. 다만 인터페이스만 선언한 만큼, 나만의 메소드를 추가하는 방법이 조금 까다롭다. 메소드를 추가하는 방법은 다음에 작성하겠다.