Simple JPA Test
단순한 JPA의 OneToMany, ManyToOne 관계에 대한 테스트를 해보자.
1. JPA테스트 :
- JPA에서 OneToMany와 ManyToOne을 양방향 연결 관계로 설정하고, 이를 운용하는 테스트를 진행해보았다.
- 여기서는 CascadeType.ALL의 설정과 orphanRemove = true에 대해서 알아보고자 한다.
- 또한 Unique 설정에서 해당 처리가 어떻게 수행되는지 알아 볼 것이다.
2. Teacher Entity 객체 생성하기.
package kr.co.unclebae.jpa.domain;
import com.google.common.collect.Lists;
import org.springframework.util.CollectionUtils;
import javax.persistence.*;
import java.util.List;
/** * Created by UncleBae on 15. 9. 14.. */@Entity@Table(name = "TEACHER")
public class Teacher {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "TEACHER_ID")
private Long id;
private String name;
private String subject;
@OneToMany(mappedBy = "teacher" ,fetch = FetchType.LAZY,
cascade = CascadeType.ALL, orphanRemoval = true)
private List<Student> students = Lists.newArrayList();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
if (students == null) {
return;
}
for(Student student : students) {
addStudent(student);
}
}
public void addStudent(Student student) {
if (students.contains(student)) {
return;
}
this.students.add(student);
student.setTeacher(this);
}
public void removeStudent(Student student) {
if (student == null) {
return;
}
students.remove(student);
student.setTeacher(null);
}
public void removeAllStudents() {
if (CollectionUtils.isEmpty(students)) {
return;
}
for(Student student : students) {
removeStudent(student);
}
}
@Override public String toString() {
return String.format("Teacher ID %s, name %s,
subject %s, students %s", id, name, subject,
createStudentsString());
}
private String createStudentsString() {
StringBuffer sb = new StringBuffer();
sb.append("[");
if (!CollectionUtils.isEmpty(students)) {
for (Student student : students) {
sb.append("{");
sb.append(student.toString());
sb.append("},");
}
}
sb.append("]");
return sb.toString();
}
@Override public boolean equals(Object obj) {
return super.equals(obj);
}
}
-
Teacher : Student = 1 : N관계 즉 OneToMany관계이다.
- 여기서
mappedBy를 이용하여 연관관계의 주인이 아님을 설정한다. (연관관계의 주인은 보통 Many쪽에 지정한다. )
-
cascade = CascadeType.ALL 을 지정하였다.
이로 인해 Teacher의 영속성 변화에 대해서 자식인 Student에게 전이를 시켜준다. (즉, 부모가 영속화 되면 자식도 영속화 되며, 부모에서 제거되면 자식도 제거되는 처리를 수행한다.)
-
orphanRemove = true 를 지정하였다.
이렇게 지정하면 Teacher객체를 제거하면, 자식인 Student의 객체들도 함께 제거된다.
또한 Student에 대해서 student.setTeacher(null); 이라고 지정하면 해당 자식은 제거 된다.
-
addStudent, removeStudent, removeAllStudents와 같은 편의 메소드를 부모쪽에 지정하여 자식 객체에 대한 연관관계를 함께 추가 및 제거해주도록 처리한다.
3. Student Entity 객체 생성하기.
package kr.co.unclebae.jpa.domain;
import javax.persistence.*;
/** * Created by UncleBae on 15. 9. 14..
*/@Entity@Table(name = "STUDENT")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private String grade;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEACHER_ID")
private Teacher teacher;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Teacher getTeacher() {
return teacher;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
if (teacher == null) {
return;
}
if ( !teacher.getStudents().contains(this)) {
teacher.getStudents().add(this);
}
}
@Override public String toString() {
return String.format("Student id %s, name %s,
grade %s", id, name, grade );
}
}
-
ManyToOne 를 지정하여 자식 객체에서 부모와의 연관을 맺어준다.
- ManyToOne에서는 cascade를 사용하지 말것. - 만약 cascade = CascadeType.ALL을 지정했을때 테스트를 해보면 자식객체에서 부모 객체를 제거하면, Teacher객체 역시 함께 제거 되어 버린다. (주의)
4. 리포지토리 생성하기
TeacherRepository.java
package kr.co.unclebae.jpa.repository;
import kr.co.unclebae.jpa.domain.Teacher;
import org.springframework.data.jpa.repository.JpaRepository;
/** * Created by Uncle Bae on 15. 9. 14.. */
public interface TeacherRepository
extends JpaRepository<Teacher, Long> {
}
StudentRepository.java
package kr.co.unclebae.jpa.repository;
import kr.co.unclebae.jpa.domain.Student;
import org.springframework.data.jpa.repository.JpaRepository;
/** * Created by naver on 15. 9. 14.. */
public interface StudentRepository
extends JpaRepository<Student, Long> {
}
- SpringData JPA를 이용하였다.
- JpaRepository<T, ID>를 상속받은 인터페이스를 생성하면, 실행시점에 해당 구현체를 만들어준다.
5. 테스트 케이스 작성하기.
package kr.co.unclebae.jpa.service;
import com.google.common.collect.Lists;
import kr.co.unclebae.jpa.domain.Student;
import kr.co.unclebae.jpa.domain.Teacher;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import java.util.List;
/** * Created by Uncle Bae on 15. 9. 14.. */
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration@ContextConfiguration(
{"classpath:hibernateConfig.xml"})
public class TeacherTest {
@Autowired
private TeacherService teacherService;
@Test
public void TeacherInsertTest() {
Teacher teacher = createTeacherFixture();
Teacher savedTeacher = teacherService.save(teacher);
System.out.println(String.format("Saved Val : %s", savedTeacher));
}
@Test public void TeacherInsertAndSutdentUpdate() {
// 일반적인 CascadeType.ALL 테스트
Teacher teacherFixture = createTeacherFixture();
Student student1 = createStudentFixture("STUDENT1", "B-");
Student student2 = createStudentFixture("STUDENT2", "A+");
teacherFixture.addStudent(student1);
teacherFixture.addStudent(student2);
Teacher teacherSaved = teacherService.save(teacherFixture);
System.out.println( String.format("[GENERAL CascadeType.ALL]
SAVED TEACHER %s", teacherSaved.toString() ) );
// Delete And Insert And Update
List<Student> students = Lists.newArrayList();
Student student3 = createStudentFixture("STUDENT3", "A-");
students.add(student3);
Student student4 = createStudentFixture("STUDENT1", "F");
student4.setId(student1.getId());
// Update가 되려면 ID를 동일하게 세팅해야한다.
students.add(student4);
teacherService.deleteAndMerge(teacherSaved.getId(), students);
Teacher teacherSelected = teacherService.findById(teacherSaved.getId());
System.out.println( String.format("[CascadeType.REMOVE
or CascadeType.ALL] Delete And Merge Result %s",
teacherSelected ) );
teacherService.removeFirstStudent(teacherSelected.getId());
Teacher teacherSelected2 = teacherService.findById(teacherSaved.getId());
System.out.println(String.format("[orphanRemove = true]
RemoveFirstStudent Result %s", teacherSelected2));
teacherService.deleteById(teacherSelected.getId());
System.out.println( String.format("[orphanRemove = true]
delete parent") );
}
private Student createStudentFixture(String name, String grade) {
Student student = new Student();
student.setName(name);
student.setGrade(grade);
return student;
}
private Teacher createTeacherFixture() {
Teacher teacher = new Teacher();
teacher.setName("Undle Bae");
teacher.setSubject("Computer Science");
return teacher;
}
}
- 위 테스트는 아래 4가지를 테스트한다.
A. 일반적인 CascadeType.ALL 동작
B. Delete And Insert Update 처리 확인
C. 리스트에서 첫번째 자식 객체를 제거한경우 처리 확인
4. 부모 제거시 동작 확인
6. 생성된 스키마 확인.
create table STUDENT (
id bigint generated by default as identity,
grade varchar(255),
name varchar(255),
TEACHER_ID bigint,
primary key (id)
)
create table TEACHER (
TEACHER_ID bigint generated by default as identity,
name varchar(255),
subject varchar(255),
primary key (TEACHER_ID)
)
alter table STUDENT
add constraint UK_3leirxifnbbkn35r06ggygpj0 unique (name)
alter table STUDENT
add constraint FK_kqtywag05gv4ixllub1d0ayp1
foreign key (TEACHER_ID)
references TEACHER
- 상기 내역을 보면 STUDENT에서 foreign key로 부모의 TEACHER_ID가 설정되었다.
7. Cascade.ALL에 의한 부모 및 자식객체 저장
/* insert kr.co.unclebae.jpa.domain.Teacher
*/ insert
into
TEACHER
(TEACHER_ID, name, subject)
values
(null, ?, ?)
/* insert kr.co.unclebae.jpa.domain.Student
*/ insert
into
STUDENT
(id, grade, name, TEACHER_ID)
values
(null, ?, ?, ?)
/* insert kr.co.unclebae.jpa.domain.Student
*/ insert
into
STUDENT
(id, grade, name, TEACHER_ID)
values
(null, ?, ?, ?)
- TEACHER 테이블에 에 인서트가 되고나서
- SUTDENT 테이블에 2건 인서트가 되었다.
select
teacher0_.TEACHER_ID as TEACHER_1_1_0_,
teacher0_.name as name2_1_0_,
teacher0_.subject as subject3_1_0_
from
TEACHER teacher0_
where
teacher0_.TEACHER_ID=?
select
students0_.TEACHER_ID as TEACHER_4_1_0_,
students0_.id as id1_0_0_,
students0_.id as id1_0_1_,
students0_.grade as grade2_0_1_,
students0_.name as name3_0_1_,
students0_.TEACHER_ID as TEACHER_4_0_1_
from
STUDENT students0_
where
students0_.TEACHER_ID=?
- 해당 내역을 출력하기 위해서 TEACHER를 먼저 호출하고, STUDENT 를 TEACHER_ID로 호출하여 2건을 가져오고 있다.
- 출력 결과 :
[GENERAL CascadeType.ALL]
SAVED TEACHER Teacher ID 1, name Undle Bae, subject Computer Science,
students [
{Student id 1, name STUDENT1, grade B-},
{Student id 2, name STUDENT2, grade A+},
]
8. Cascade.ALL, orphanRemove = true 처리 (삭제, 인서트, 업데이트)
/* insert kr.co.unclebae.jpa.domain.Student
*/ insert
into
STUDENT
(id, grade, name, TEACHER_ID)
values
(null, ?, ?, ?)
/* update
kr.co.unclebae.jpa.domain.Student */ update
STUDENT
set
grade=?,
name=?,
TEACHER_ID=?
where
id=?
/* delete kr.co.unclebae.jpa.domain.Student */ delete
from
STUDENT
where
id=?
- 우선 신규로 추가되는 STUDENT3이 인서트 된다.
- 다음으로 STUDENT1이 업데이트 된다. (ID가 같으므로 업데이트함)
- 마지막으로 STUDENT2가 삭제 된다.
# 중요한 것은 JPA는 ID값이 존재하면 업데이트를, ID가 없으면 인서트를 수행한다.
# orphanRemove를 통해서 부모와 연결이 끊어진 객체 STUDENT2는 트랜잭션이 종료 될때 제거된다.
- 처리 결과 :
[CascadeType.REMOVE or CascadeType.ALL]
Delete And Merge Result Teacher ID 1, name Undle Bae, subject Computer Science,
students [
{Student id 1, name STUDENT1, grade F},
{Student id 3, name STUDENT3, grade A-},
]
9. orphanRemove = true설정후 부모 객체 삭제
/* delete kr.co.unclebae.jpa.domain.Student */ delete
from
STUDENT
where
id=?
- students의 첫번째 엘리먼트만 제거한경우에는 트랜잭션이 종료 되면서 해당 내역을 DB에서 제거한다.
- 처리결과 :
[orphanRemove = true] RemoveFirstStudent Result Teacher ID 1, name Undle Bae, subject Computer Science, students [{Student id 3, name STUDENT3},]
/* delete kr.co.unclebae.jpa.domain.Student */ delete
from
STUDENT
where
id=?
/* delete kr.co.unclebae.jpa.domain.Teacher */ delete
from
TEACHER
where
TEACHER_ID=?
- 부모인 TEACHER객체를 제거하게 되면 부모를 잃은 자식 객체도 자동적으로 제거 된다.
- 로그 :
[orphanRemove = true] delete parent
10. 설정하기.
10.1 hibernateConfig.xml설정하기.
<?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:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
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/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<jpa:repositories base-package="kr.co.unclebae.jpa.repository" />
<tx:annotation-driven/>
<context:component-scan base-package="kr.co.unclebae.jpa.service,
kr.co.unclebae.jpa.repository"/>
<bean id="dataSource" class="org.apache.tomcat.jdbc
.pool.DataSource">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:jpashop"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>
<bean id="transactionManager" class="org.springframework
.orm.jpa.JpaTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean class="org.springframework.dao.annotation.
PersistenceExceptionTranslationPostProcessor"/>
<bean id="entityManagerFactory" class="org.springframework.
orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="packagesToScan" value="kr.co.unclebae.
jpa.domain"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.
HibernateJpaVendorAdapter"/>
</property>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.use_sql_comments">true</prop>
<prop key="hibernate.id.new_generator_mappings">true</prop>
<prop key="hibernate.hbm2ddl.auto">create</prop>
</props>
</property>
</bean>
</beans>
10.2 POM 설정하기
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>JPATest</groupId>
<artifactId>JPATest</artifactId>
<version>1.0-SNAPSHOT</version>
<name>JPATest</name>
<packaging>war</packaging>
<properties>
<java.version>1.8</java.version>
<spring-data-jpa.version>1.8.0.RELEASE</spring-data-jpa.version>
<spring-framework.version>4.1.6.RELEASE</spring-framework.version>
<querydsl.version>3.6.3</querydsl.version>
<hibernate.version>4.3.10.Final</hibernate.version>
<tomcat-jdbc.version>7.0.52</tomcat-jdbc.version>
<h2db.version>1.4.187</h2db.version>
<jsp.version>2.2</jsp.version>
<jstl.version>1.2</jstl.version>
<servlet.version>3.0.1</servlet.version>
<logback.version>1.1.1</logback.version>
<slf4j.version>1.7.6</slf4j.version>
<junit.version>4.12</junit.version>
</properties>
<dependencies>
<!-- 스프링 데이터 JPA -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${spring-data-jpa.version}</version>
</dependency>
<!-- QueryDSL -->
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mysema.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>
<!-- 스프링 MVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- JPA, 하이버네이트 -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- H2 데이터베이스 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2db.version}</version>
<scope>runtime</scope>
</dependency>
<!-- 커넥션 풀 -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
<version>${tomcat-jdbc.version}</version>
<scope>compile</scope>
</dependency>
<!-- WEB -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
<!-- 로깅 SLF4J & LogBack -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
<!-- 테스트 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-framework.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Spring Data JPA 1.8이 스프링 4.0.9에 의존관계를 가지므로 스프링 버전을 직접 관리한다. -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring-framework.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>