SpringBoot JPA 예제(@OneToMany, 단방향)

 

이번에도 JPA입니다. @OneToMany 어노테이션을 이용해서 Entity간의 관계를 맺어봅시다. 이 포스팅에서는 단방향 관계(unidirectional relationships)만 다루겠습니다.

1. 단방향 or 양방향

포스트 처음에 언급했지만 Entity간의 관계를 맺을 때에는 방향이 있습니다. 예를 들면 아래와 같은 두 테이블이 있다고 가정합니다.

CREATE TABLE `member` (
	`seq` INT(10) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(50) NULL DEFAULT NULL,
	PRIMARY KEY (`seq`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1;
CREATE TABLE `phone` (
	`seq` INT(10) NOT NULL AUTO_INCREMENT,
	`member_id` INT(10) NULL DEFAULT NULL,
	`no` VARCHAR(50) NULL DEFAULT NULL,
	PRIMARY KEY (`seq`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=1;

외래키(foreign key) 제약 조건은 걸지 않았습니다. DB는 MariaDB입니다.

위의 테이블은 각각 회원(member)과 핸드폰(phone)을 가리킵니다. 회원들은 핸드폰을 가지지 않거나 여러 개를 가질 수 있습니다. 핸드폰에서는 자신의 주인이 누구인지 회원 번호를 가지고 있습니다.

여기서 방향이라 하는 것은 해당 Entity가 다른 Entity를 가질 수 있느냐로 보시면 됩니다. 예를 들어 위의 회원 Entity에서 핸드폰 Entity를 멤버 변수로 가지고 있다면 회원 -> 핸드폰이라는 방향성을 가집니다. 이렇게 한쪽의 방향만 가지면 단방향(unidirectional)이라고 하죠.

만약, 위와 같은 상황에서 핸드폰 Entity에서도 회원 Entity를 가진다면 회원 <-> 핸드폰이라는 관계가 성립됩니다. 이런것을 양방향(bidirectional) 관계라고 합니다.

물론 회원 <- 핸드폰 관계도 가능합니다. 이때는 단방향 관계를 가지지만 아까와 방향이 다를 뿐입니다.

2. @OneToMany

이제 회원과 핸드폰 테이블을 기반으로 해서 설명을 진행합니다. 회원은 기본적으로 핸드폰이 없거나 1개 이상을 소지할 수 있습니다. 그러면 회원 하나에 핸드폰을 여러개 가지니 1:N 관계가 됩니다. 이것을 JPA 어노테이션으로는 @OneToMany라고 표현합니다.

2.1. @OneToMany 속성

@OneToMany 속성에는 다음과 같은 것들이 있습니다.

  • targetEntity
  • cascade
  • fetch
  • mappedBy
  • orphanRemoval

2.1.1. targetEntity

관계를 맺을 Entity Class를 정의합니다.

2.1.2. cascade

현 Entity의 변경에 대해 관계를 맺은 Entity도 변경 전략을 결정합니다.

속성값에는 CascadeType라는 enum에 정의 되어 있으며 enum값에는 ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH가 있습니다.

2.1.3. fetch

관계 Entity의 데이터 읽기 전략을 결정합니다.

FetchType.EAGER, FetchType.LAZY로 전략을 변경 할 수 있습니다. 두 전략의 차이점은 EAGER인 경우 관계된 Entity의 정보를 미리 읽어오는 것이고 LAZY는 실제로 요청하는 순간 가져오는겁니다.

2.1.4. mappedBy

양방향 관계 설정시 관계의 주체가 되는 쪽에서 정의합니다.

2.1.5. orphanRemoval

관계 Entity에서 변경이 일어난 경우 DB 변경을 같이 할지 결정합니다. cascade와 다른것은 cascade는 JPA 레이어 수준이고 이것은 DB레이어에서 처리합니다. 기본은 false입니다.

2.2. 단방향 @OneToMany 예제

실제로 예제를 보면 더 이해가 빠를듯 합니다. 예제를 진행합시다.

2.2.1. Entity

회원 Entity 및 핸드폰 Entity를 만듭니다.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

2.2.1.1. Member Entity
 
  1. package jpa3;
  2.  
  3. import java.util.ArrayList;
  4. import java.util.Collection;
  5. import java.util.List;
  6.  
  7. import javax.persistence.CascadeType;
  8. import javax.persistence.Column;
  9. import javax.persistence.Entity;
  10. import javax.persistence.FetchType;
  11. import javax.persistence.GeneratedValue;
  12. import javax.persistence.GenerationType;
  13. import javax.persistence.Id;
  14. import javax.persistence.JoinColumn;
  15. import javax.persistence.OneToMany;
  16.  
  17. @Entity
  18. public class Member {
  19.  
  20. @Id
  21. @Column(name="seq")
  22. @GeneratedValue(strategy=GenerationType.AUTO)
  23. private int seq;
  24.  
  25. @Column(name="name")
  26. private String name;
  27.  
  28. @OneToMany(fetch=FetchType.EAGER, cascade = CascadeType.ALL)
  29. @JoinColumn(name="member_id")
  30. private Collection<Phone> phone;
  31.  
  32. public Member(){}
  33.  
  34. public Member(String name){
  35. this.name = name;
  36. }
  37.  
  38. public int getSeq() {
  39. return seq;
  40. }
  41.  
  42. public void setSeq(int id) {
  43. this.seq = id;
  44. }
  45.  
  46. public String getName() {
  47. return name;
  48. }
  49.  
  50. public void setName(String name) {
  51. this.name = name;
  52. }
  53.  
  54. public Collection<Phone> getPhone() {
  55. return phone;
  56. }
  57.  
  58. public void setPhone(List<Phone> phone) {
  59. this.phone = phone;
  60. }
  61.  
  62. public void addPhone(Phone p){
  63. if( phone == null ){
  64. phone = new ArrayList<Phone>();
  65. }
  66. phone.add(p);
  67. }
  68.  
  69. @Override
  70. public String toString() {
  71. String result = "["+seq+"] " + name;
  72. for( Phone p : phone ){
  73. result += "\n" + p.toString();
  74. }
  75. return result;
  76. }
  77. }

28번 라인에서 @OneToMany 어노테이션을 정의하고 있습니다. 그리고 멤버 변수로 핸드폰 Entity를 가리키고 있습니다. 따라서 회원 -> 핸드폰이라는 단방향 1:N 관계를 만든 것입니다. 또한 FetchType.EAGER 전략을 취해 항상 Phone 목록을 가져오게 되어 있습니다. 불필요한 조회를 막으려면 FetchType.LAZY를 설정해야 합니다.

2.2.1.2. Phone Entity
package jpa3;

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

@Entity
public class Phone {

	@Id
	@Column(name="seq")
	@GeneratedValue(strategy=GenerationType.AUTO)
	private int seq;

	@Column(name="member_id")
	private int memberId;

	@Column(name="no")
	private String no;

	public Phone() {}
	public Phone(String no){
		this.no = no;
	}

	public int getSeq() {
		return seq;
	}

	public void setSeq(int seq) {
		this.seq = seq;
	}

	public int getMemberId() {
		return memberId;
	}

	public void setMemberId(int memberId) {
		this.memberId = memberId;
	}

	public String getNo() {
		return no;
	}

	public void setNo(String no) {
		this.no = no;
	}

	@Override
	public String toString() {
		String result = "[phone_"+seq+"] " + no;
		return result;
	}
}

2.2.2. Repository

Repository는 간단하게 코드만 보고 넘어가겠습니다.

package jpa3;

import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Integer>{}
package jpa3;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PhoneRepository extends JpaRepository<Phone, Integer> {}

2.3. Application

실제로 위의 코드를 돌려보는 프로그램을 작성합니다.

 
  1. package jpa3;
  2.  
  3. import java.util.List;
  4.  
  5. import javax.transaction.Transactional;
  6.  
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.boot.CommandLineRunner;
  9. import org.springframework.boot.SpringApplication;
  10. import org.springframework.boot.autoconfigure.SpringBootApplication;
  11.  
  12. @SpringBootApplication
  13. public class Jpa3Application implements CommandLineRunner{
  14.  
  15. @Autowired
  16. private MemberRepository mr;
  17.  
  18. @Autowired
  19. private PhoneRepository pr;
  20.  
  21. public static void main(String[] args) {
  22. SpringApplication.run(Jpa3Application.class, args);
  23. }
  24.  
  25. @Override
  26. public void run(String... args) throws Exception {
  27.  
  28. Member first = new Member("Jung");
  29. first.addPhone(new Phone("010-XXXX-XXXX"));
  30. first.addPhone(new Phone("010-YYYY-YYYY"));
  31.  
  32. Member second = new Member("Dong");
  33. second.addPhone(new Phone("010-ZZZZ-ZZZZ"));
  34.  
  35. Member third = new Member("Min");
  36.  
  37. mr.save(first);
  38. mr.save(second);
  39. mr.save(third);
  40.  
  41. List<Member> list = mr.findAll();
  42.  
  43. for( Member m : list ){
  44. System.out.println(m.toString());
  45. }
  46.  
  47. mr.deleteAll();
  48. }
  49. }
[106] Jung
[phone_97] 010-XXXX-XXXX
[phone_98] 010-YYYY-YYYY
[107] Dong
[phone_99] 010-ZZZZ-ZZZZ
[108] Min

28~35번째 라인은 새로운 회원을 생성하고 해당 회원들의 핸드폰을 정의했습니다. 그리고 37~39번째 라인에서 해당 정보를 DB에 저장합니다. 41~45번째 라인은 DB에 저장된 정보를 다시 읽어서 해당 정보를 출력합니다. 최종적으로 47번째 라인에서 모든 정보를 삭제합니다. 삭제할 때 @OneToMany 설정시 CascadeType.ALL 을 취했기 때문에 관련 Entity의 내용도 같이 삭제가 됩니다.

결과 출력은 제가 수많은(?) 테스트 이후 복사한 것이기 때문에 seq 값들이 커진 상태입니다. :)

2.3.1. 참조

만약 Member Entity의 @OneToMany 항목중 fetch 타입을 FetchType.LAZY로 바꾸면 어떻게 될까요? 바로 org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role 오류가 나올겁니다.

해당 오류는 데이터가 영속성(persistence)을 잃었을 경우 생깁니다. 영속성이란 데이터를 생성한 프로그램이 실행이 종료되더라도 사라지지 않는 특징을 말합니다. 따라서 영속성을 잃었다는 것은 데이터를 생성한 프로그램이 종료되면 해당 데이터는 더 이상 추가 적재 및 갱신이 안됩니다. 메모리에만 남아있을 뿐입니다.

따라서, 현재 LAZY 전략을 취한 경우 Member Entity의 멤버 변수는 살아 있는것처럼 보이지만 @OneToMany로 묶인 Phone Entity는 호출한 이후 영속성을 잃었기 때문에 LazyInitializationException 오류를 내는겁니다.

이를 해결하려면 전략을 다시 EAGER로 바꿔주면 됩니다. 하지만 이러한 방법은 경우에 따라 불필요한 조회를 하게 만들고 볼륨이 큰 데이터일 경우 더욱 성능을 나쁘게 만듭니다.

다른 방법은 @Transactional 어노테이션을 메소드에 설정하면 됩니다. 만약 현재 포스트에 있는 예제를 사용한다면 다음처럼 바뀝니다.

 
  1. @SpringBootApplication
  2. public class Jpa3Application implements CommandLineRunner{
  3. ...
  4. @Override
  5. @Transactional
  6. public void run(String... args) throws Exception {
  7. ...
  8. }
  9. }

위 코드는 Application 항목의 코드를 가져온 겁니다.

run 메소드가 실행되는 동안은 영속성을 잃지 않게 해줍니다. 따라서 오류 없이 처리 가능하게 됩니다. 다만 주의할 것은 @Transactional 어노테이션이 설정된 구간 내에서 모델 객체에 변경이 일어나면 변경된 상태로 DB에 저장됩니다. 이것은 Repository에서 꺼내온 객체라도 동일하게 적용되기 때문에 주의해야 합니다.

JPA Web MVC 애플리케이션이라면 보통 Controller - Service(or implement) - Repository의 구성을 가지게 됩니다. 이 경우 Service 클래스에서 LAZY 전략이 가미된 Entity를 사용한다면 해당 메소드에 @Transactional 어노테이션을 정의해주면 됩니다. 아니면 Service 클래스 자체에 @Transactional을 줘도 됩니다. 상황에 맞게 쓰시면 되요.

3. 마무리

@OneToMany 단방향 예제에 대해 알아봤습니다. 다음번엔 @ManyToOne에 대해 알아보아요. :)

 

[출처] http://jdm.kr/blog/141

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
224 JPA 다대다 관계 모델 @OneToMany로 구현하여 Column 추가하기(@IdClass 사용) file 졸리운_곰 2018.05.18 94
223 Spring Data JPA 연관관계 매핑하는 방법 졸리운_곰 2018.05.18 21
222 공유된 FK(Foreign Key) JPA 연관 관계 매핑 하기 file 졸리운_곰 2018.05.18 666
221 JPA - 05. 연관관계 매핑 기초 졸리운_곰 2018.05.18 31
220 SpringBoot JPA 예제(1:N, 양방향) 졸리운_곰 2018.05.18 28
» SpringBoot JPA 예제(@OneToMany, 단방향) 졸리운_곰 2018.05.18 27
218 JPA / Hibernate One to Many Mapping Example with Spring Boot file 졸리운_곰 2018.05.18 88
217 The best way to map a @OneToMany relationship with JPA and Hibernate file 졸리운_곰 2018.05.18 177
216 (JPA) Embedded Type file 졸리운_곰 2018.05.18 33
215 스프링 데이터 JPA 레퍼런스 번역 file 졸리운_곰 2018.05.14 340
214 UML: 클래스 다이어그램과 소스코드 매핑 file 졸리운_곰 2018.04.30 170
213 lombok에 대해서 알아보자 file 졸리운_곰 2018.04.24 59
212 lombok을 잘 써보자! (2) 졸리운_곰 2018.04.24 151
211 lombok을 잘 써보자! (1) 졸리운_곰 2018.04.24 77
210 Maven 기초 사용법 졸리운_곰 2018.04.15 103
209 [JAVA] Java 와 Mysql 연동 및 DB 사용 졸리운_곰 2018.02.14 88
208 json을 파싱해보자 졸리운_곰 2018.02.12 58
207 [JAVA] json형식의 문자열을 json객체로 parsing하기 졸리운_곰 2018.02.12 90
206 [Java] Quartz (쿼츠)를 사용하여 자바 스케줄링(scheduling) 하기 졸리운_곰 2018.02.12 245
205 스프링(Spring) 프레임워크 기본 개념 강좌 (7) - Patterns 졸리운_곰 2017.10.02 92
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED