[JPA]
Java에서 사용하는 ORM (Object-Relational Mapping) 기술의 표준 사양.
이는 Java 인터페이스로 사양이 정의되어 있기에 JPA라는 표준 사양을 구현한 구현체는 따로 있다. = eg.Hibernate ORM
[데이터 액세스 계층의 구조]
JPA는 데이터 엑세스 계층에서의 상단에 위치하며, 데이터 저장, 조회 등의 작업은 JPA를 거쳐
JPA의 구현체인 Hibernate ORM을 통해 이루어지며 내부적으로 JDBC API를 이용해 데이터 베이스에 접근하게 된다.
[영속성 컨텍스트 (Persistence Context) ]
JPA는 테이블과 매핑되는 엔티티 객체 정보를 영속성 컨텍스트 라는 곳에 보관하며,
이렇게 보관된 엔티티 정보는 DB 테이블에 저장, 수정, 조회, 삭제하는 데 활용된다.
영속성 컨텍스트 공간에는 1차 캐시, 쓰기 지연 SQL 저장소 라는 영역이 있고, 이는 Entity Manager에 의해 관리된다.
[코드로 JPA API 사용하기]
1. JPA API 의존 라이브러리 설정.
build.gradle 파일의 Dependencies 안에 하기 코드 추가.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // (1)
2. JPA 설정. (application.yml)
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
jpa:
hibernate:
ddl-auto: create # (1) 스키마 자동 생성
show-sql: true # (2) SQL 쿼리 출력
(1) : JPA에서 사용하는 엔티티 클래스를 정의하고, 애플리케이션 실행 시, 엔티티와 매핑된 테이블을 DB에서 자동 생성.
(2) : JPA의 동작 과정을 이해하기 위해 JPA API를 통해서 실행되는 SQL 쿼리를 로그로 출력해준다.
3. 샘플 코드 실행을 위한 Configuration 클래스 생성.
// (1)
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
// (2)
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
// (3) 이 곳에 학습할 코드를 타이핑합니다.
};
}
}
(1) : @Configuration 애너테이션을 붙이면 Spring에서 해당 클래스를 Bean 검생 대상으로 간주한다.
(2) : @Bean 메서드를 검색하여 리턴하는 객체를 Spring Bean으로 등록한다.
4. 영속성 컨텍스트에 저장할 엔티티 클래스 생성.
@Getter
@Setter
@NoArgsConstructor
@Entity // (1)
public class Member {
@Id // (2)
@GeneratedValue // (3)
private Long memberId;
private String email;
public Member(String email) {
this.email = email;
}
}
(1) : @Entity 애너테이션을 설정하여 엔티티 클래스임을 명시.
(2) : 엔티티 클래스와 매핑할 테이블의 식별자가 될 변수에 @Id, @GeneratedValue 추가
5. JPA 영속성 컨텍스트에 등록된 객체를 테이블에 저장
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
// (1)
this.tx = em.getTransaction();
return args -> {
example02();
};
}
private void example02() {
// (2)
tx.begin();
Member member = new Member("hgd@gmail.com");
// (3)
em.persist(member);
// (4)
tx.commit();
// (5)
Member resultMember1 = em.find(Member.class, 1L);
Member resultMember2 = em.find(Member.class, 2L);
System.out.println(resultMember2 == null);
}
}
(2) : 트랜잭션을 시작하겠다 의미.
(3) em.persist() : 영속성 컨텍스트에 등록. (1차 캐시 O, 쓰기 지연 SQL 저장소 O)
(4) tx.commit(); : 영속성 컨텍스트에 등록한 데이터를 DB로 저장. (1차 캐시 O, 쓰기 지연 SQL 저장소 X)
(5) em.find(Member.class, 1L); : 입력한_엔티티클래스(member)에서 식별자1 을 가진 데이터를 가져온다.
6. 영속성 컨텍스트와 테이블에 엔티티 업데이트
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
example04();
};
}
private void example04() {
tx.begin();
em.persist(new Member("hgd1@gmail.com")); // (1)
tx.commit(); // (2)
tx.begin();
Member member1 = em.find(Member.class, 1L); // (3)
member1.setEmail("hgd1@yahoo.co.kr"); // (4)
tx.commit(); // (5)
}
}
(1). 영속성 컨텍스트에 Member 객체 (이메일)로 생성하여 등록함.
(2) : DB 테이블에 저장.
(3) : 영속성 컨텍스트에 저장된 데이터 중 조회할 엔티티 클래스타입, 식별자 번호의 데이터를 불러와서 member1 에 할당.
(4) : member1을 setEmail()메서드를 활용해서 이메일 주소를 변경함.
(5) : setter 메서드로 이메을을 변경한 member1을 commit처리하여 DB에 저장함.
7. 영속성 컨텍스트와 테이블에 엔티티 삭제
@Configuration
public class JpaBasicConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
example05();
};
}
private void example05() {
tx.begin();
em.persist(new Member("hgd1@gmail.com")); // (1)
tx.commit(); //(2)
tx.begin();
Member member = em.find(Member.class, 1L); // (3)
em.remove(member); // (4)
tx.commit(); // (5)
}
}
(1) : Member 클래스 객체를 만들어서 이메일로 데이터 생성 후 영속성 컨텍스트에 등록.
(2) : 등록한 Member 객체 DB에 저장.
(3) : Member 엔티티 클래스에 식별자1번을 가진 데이터를 호출하여 member 변수에 할당함.
(4) : em.remove(member) : member라는 변수를 EntityManager에 의해 삭제함.
(5) : 삭제 이후 다시 commit함.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
JPA를 이용해 DB 테이블과 상호작용 (저장, 수정, 조회, 삭제)를 위해
제일 필요한 것은 DB 테이블과 엔티티 클래스간의 매핑 작업이다.
[매핑 작업]
1. 엔티티 클래스와 테이블간의 매핑
2. 기본키 매핑
3. 객체 필드와 테이블 컬럼의 매핑
4. 엔티티 클래스들의 연관 관계 매핑
[매핑작업] - 1. 엔티티 객체와 테이블 간의 매핑
package com.codestates.entity_mapping.single_mapping.table;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity // (1)
@Table // (2)
public class Member {
@Id
private Long memberId;
}
(1) : @Entity 매핑 애너테이션 사용. 옵션으로 @Entity(name="원하는_엔티티_이름")
(2) : @Table은 옵션 이며, 추가하지 않을 경우, 클래스 이름을 테이블 이름으로 사용한다.
(3) : @NoArgsConstructor, 파라미터가 없는 기본 생성자는 습관적으로 추가하기.
> @Table은 옵션, 하지만 @Entity + @Id 애너테이션은 필수
[매핑작업] - 2. 기본키 매핑
A. 기본키 직접 할당
A-1 : @Entity 클래스의 필드 변수에 @Id 애너테이션 지정으로 기본키 직접 할당
A-2 : tx.bein()
em.persist(new Member(1L));
tx.commit()
영속성 컨텍스트에 저장할 때 객체 생성하며 기본키 직접 할당.
B. 기본키 자동 생성
B-1 : IDENTITY : 테이블에 데이터를 저장한 후, 기본키 값이 자동으로 생성되어 할당됨.
@NoArgsConstructor
@Getter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // (1)
private Long memberId;
public Member(Long memberId) {
this.memberId = memberId;
}
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) 사용.
B-2 : SEQUENCE : 엔티티를 영속성 컨텍스트에 저장하기 전에 DB에서 시퀀스 값(자동번호생성)을 조회하여 할당.
@NoArgsConstructor
@Getter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE) // (1)
private Long memberId;
public Member(Long memberId) {
this.memberId = memberId;
}
}
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE) 사용.
B-3 : AUTO : JPA가 DB의 Dialect에 따라서 적절한 전략을 자동으로 선택.
* Dialect : 표준 SQL 등이 아닌 특정 DB에 특화된 고유한 기능.
package com.codestates.entity_mapping.single_mapping.id.sequence;
@NoArgsConstructor
@Getter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // (1)
private Long memberId;
public Member(Long memberId) {
this.memberId = memberId;
}
}
[매핑작업] - 3. 객체 필드와 테이블 컬럼의 매핑
@Column : 앤티티 클래스 필드와 테이블 컬럼을 매핑.
@Column의 Attribute
1. @Column (nullable = true/false) : 컬럼에 빈 값(null) 허용 여부, default : true
>> 필드 타입이 String, int, char, long 등과 같이 원시 타입이면 nullable = false 설정할 것.
2. @Column (updatable = true/false) : 컬럼이 수정의 허용 여부, default : true
3. @Column (unique = true/false) : 컬럼이 중복 값 허용 여부, default : false
4. @Column (length = 0~255) : 컬럼에 빈 값(null) 허용 여부, default : 255
5. @Column (name = ) : 컬럼에 별도의 이름을 지정해 필드명과 테이블 컬럼명을 다르게 설정.
@Transient
추가된 필드는 테이블의 컬럼에 포함시키지 않겠다 라는 의미.
[매핑작업] - 4. 엔티티 클래스들의 연관 관계 매핑
A. 참조하는 방향성 기준.
A-1 : 단방향 연관 관계
A-2 : 양방향 연관 관계
B. 참조할 수 있는 객체 수 기준.
B-1 : [1:N 관계]
B-2 : [N:N 관계]
B-3 : [N:1 관계]
B-4 : [1:1 관계]
A. 참조하는 방향성 기준.
A-1 : [단방향 연관 관계] : 한쪽 엔티티 클래스에서만 상대 엔티티 클래스의 참조 객체를 가지고 있는 관계
OR
A-2 : [양방향 연관 관계] : 양쪽 엔티티 클래스가 서로의 참조 객체를 가지고 있어 서로 참조가 가능한 관계
1:N 관계
1클래스 : N클래스의 객체를 List/Set <N클래스명> 타입으로 필드를 가진다.
N클래스 : 1클래스의 객체를 필드로 가진다.
B. 참조하는 객체 수 기준
B-1 : [1:N 관계] = 단방향 연관 관계
1 클래스가 N클래스를 List/Set <N클래스명> 필드를 가진다.
하지만 테이블의 관계에서는 1테이블의 PK를 N테이블에서 FK로 가진다.
즉 클래스 관계에서의 필드와 테이블의 컬럼간에 관계가 맞지 않으므로 단독의 1:N 매핑은 사용되지 않는다.
B-2 : [N:1 관계]
N클래스가 1클래스의 객체를 필드로 가지는 관계.
<코드로 보는 N:1 연관 관계 매핑> // Member : Order 중 N에 해당하는 Order 클래스
@NoArgsConstructor
@Getter
@Setter
@Entity(name = "ORDERS")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long orderId;
@ManyToOne // (1)
@JoinColumn(name = "MEMBER_ID") // (2)
private Member member;
public void addMember(Member member) {
this.member = member;
}
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;
@Column(nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(nullable = false, name = "LAST_MODIFIED_AT")
private LocalDateTime modifiedAt = LocalDateTime.now();
public enum OrderStatus {
ORDER_REQUEST(1, "주문 요청"),
ORDER_CONFIRM(2, "주문 확정"),
ORDER_COMPLETE(3, "주문 완료"),
ORDER_CANCEL(4, "주문 취소");
@Getter
private int stepNumber;
@Getter
private String stepDescription;
OrderStatus(int stepNumber, String stepDescription) {
this.stepNumber = stepNumber;
this.stepDescription = stepDescription;
}
}
}
(1) : @ManyToOne : N:1 관계임을 나타내며, N클래스에서 1클래스 객체 필드에 사용한다.
(2) : @JoinColumn(name = "") : N테이블에서의 FK 컬럼명 (=1테이블의 PK 컬럼명)
<N:1 연관 관계 매핑을 이용한 회원과 주문 정보 저장>
<Configuration 클래스 생략..>
@Configuration
public class JpaManyToOneUniDirectionConfig {
private EntityManager em;
private EntityTransaction tx;
@Bean
public CommandLineRunner testJpaManyToOneRunner(EntityManagerFactory emFactory) {
this.em = emFactory.createEntityManager();
this.tx = em.getTransaction();
return args -> {
mappingManyToOneUniDirection();
};
}
private void mappingManyToOneUniDirection() {
tx.begin();
// (1)
Member member = new Member("hgd@gmail.com", "Hong Gil Dong",
"010-1111-1111");
// (2)
em.persist(member);
// (3)
Order order = new Order();
// (4)
order.addMember(member);
// (5)
em.persist(order);
// (6)
tx.commit();
// (7)
Order findOrder = em.find(Order.class, 1L);
// (8)
System.out.println("findOrder: " + findOrder.getMember().getMemberId() +
", " + findOrder.getMember().getEmail());
}
}
(1) : 회원 객체 member 생성.
(2) : 생성한 회원 객체 member, 영속성 컨텍스트에 등록
(3) : 신규 주문 객체 order 생성
(4) : 주문 객체 order에 생성한 회원 객체 member 추가.
= 주문 정보에 특정 회원의 정보를 부여함으로써 주문에 회원정보가 포함된다.
(5) : 주문 객체 영속성 컨텍스트에 추가.
(6) : DB에 저장.
(7) : 저장된 데이터중 Order 엔티티 클래스타입과 식별자 1을 가진 데이터를 Order findOrder로 할당.
(8)
findOrder.getMember().getMemberId() : findOrder객체에서 Member 객체를 찾아서 포함된 MemberId를 가져오기.
findOrder..getMember().getEmail() : findOrder객체에서 Member 클래스에 Email이라는 변수값을 가져오기.
<N:1 연관 관계 매핑에 1:N 매핑 추가> Member : Order
: 현재 주문은 회원의 정보를 조회할 수 있지만, 회원은 주문정보를 조회할 수 없다.
추가하는 1:N 매핑에서는 B-1과 같이 1클래스에서 N클래스 객체를 List/Set<N클래스>로 가진다.
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
// (1)
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
@Column(nullable = false, updatable = false, unique = true)
private String email;
@Column(length = 100, nullable = false)
private String name;
@Column(length = 13, nullable = false, unique = true)
private String phone;
@Column(nullable = false)
private LocalDateTime createdAt = LocalDateTime.now();
@Column(nullable = false, name = "LAST_MODIFIED_AT")
private LocalDateTime modifiedAt = LocalDateTime.now();
public Member(String email) {
this.email = email;
}
public Member(String email, String name, String phone) {
this.email = email;
this.name = name;
this.phone = phone;
}
public void addOrder(Order order) {
orders.add(order);
}
}
(1) @OneToMany(mappedBy = "")
N:1 매핑 후 1:N 추가의 경우에 1클래스에 가지는 N클래스의 필드에 사용.
mappedBy = ""에는 N클래스에서 @ManyToOne 애너테이션 가지고있는 필드 명.
<N:1 연관 관계 매핑에 1:N 매핑 추가하여 주문 정보 조회> Member : Order
<Configuration 클래스 생략..>
private void mappingManyToOneBiDirection() {
tx.begin();
// (1)
Member member = new Member
("hgd@gmail.com", "Hong Gil Dong", "010-1111-1111");
// (2)
Order order = new Order();
// (3)
member.addOrder(order);
// (4)
order.addMember(member);
// (5)
em.persist(member);
em.persist(order);
// (6)
tx.commit();
// (7)
Member findMember = em.find(Member.class, 1L);
findMember
// (8)
.getOrders()
.stream()
.forEach(findOrder -> {
System.out.println("findOrder: " +
// (9)
findOrder.getOrderId() + ", "
+ findOrder.getOrderStatus());
});
}
}
(1) : 회원 정보 생성
(2) : 주문 정보 생성
(3) : 회원 정보에 주문 정보 추가
(4) : 주문 정보에 회원 정보 추가
(5), (6) : 회원, 주문 정보 저장
(7) : DB에서 회원 타입의 데이터중 식별자1을 가진 데이터를 findMember에 할당.
(8) : findMember.getOrders()로 findMember 변수에 포함되는 주문 정보를 불러온다.
(9) : 불러온 주문 정보에서 OrderId, OrderStatus 정보를 불러온다.
B-3 : [N:N 관계]
테이블의 관계에서는 <중간 객체>를 생성하여 1:N, N:1 관계로 만든다.
<중간 객체>는 양쪽의 객체를 FK로 가지고 있으므로, 테이블을 기반으로
엔티티 클래스간의 매핑을 진행하면된다.
1. N:1 관계 먼저 매핑
N클래스
@ManyToOne : N클래스에서 1클래스 객체를 필드로 작성 = N:1 관계 명시
@JoinColumn(name="1 테이블의 PK 컬럼명")
1클래스
@OneToMany (mappedby="N클래스에서 가지고있는 1클래스의 필드명")
N:1 관계에서 1클래스에서 작성하며, N클래스를 List/Set 형태로 필드 선언 후 애너테이션 추가.
2. N:1 매핑이 종료된 이후, 1쪽에서 N쪽의 정보를 참조할 기능이 필요하다면 1:N 추가하여 양방향 처리.
1클래스
@OneToMany (mapped ="N클래스에서 필드로 선언한 1클래스의 객체명")
N클래스를 List/Set<N클래스명> 형태로 필드 선언
B-4 : [1:1 관계]
양쪽의 엔티티 클래스 필드에 상대 클래스 객체 선언.
@OneToOne(mappedby="상대클래스 필드에 선언된 본인 클래스명", cascade = CascadeType.PERSIST)
@JoinColumn(name="상대1클래스_테이블의_PK컬럼명)
상대_1클래스_필드
'백엔드 학습 과정 > Section 3 [Spring MVC, JDBC, JPA, RestDo' 카테고리의 다른 글
#7. Spring MVC 트랜잭션 (0) | 2023.01.11 |
---|---|
#6. Spring Data JPA (0) | 2023.01.11 |
JPA에서 em을 활용한 회원과 주문 정보 저장 및 호출 흐름 (0) | 2023.01.01 |
Spring JPA 기능 코드 (0) | 2023.01.01 |
Spring Data JDBC - Service 클래스 기능별 코드 (0) | 2022.12.31 |