이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
관계형 데이터베이스에는 상속 관계가 없다.
대신에 관계형 데이터베이스는 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.

왼쪽이 슈퍼타입 서브타입 논리 모델이고, 오른쪽이 객체 상속 모델이다.
상속관계 매핑이란 관계형 데이터베이스의 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법을 말한다.
상속관계 매핑하는 방법에는 3가지 방법이 있다.
조인 전략: 각각 테이블로 변환
단일 테이블 전략: 통합 테이블로 변환
구현 클래스마다 테이블 전략: 서브타입 테이블로 변환
@Inheritance(strategy=InheritanceType.XXX)
JOINED : 조인 전략 (InheritanceType.JOINED)
SINGLE_TABLE : 단일 테이블 전략 (InheritanceType.JOINED)
TABLE_PER_CLASS : 구현 클래스마다 테이블 전략 (InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn(name=“DTYPE”) : 부모에서 구분 컬럼을 지정할 때 사용한다. 기본값이 DTYPE이므로 name 속성은 생략 가능하나, 작성하는 것이 협업하기 있어서 좋다.
@DiscriminatorValue(“XXX”) : 자식에서 구분 컬럼에 입력할 값을 지정할 때 사용한다. 기본값은 엔티티 이름이다.
@PrimaryKeyJoinColumn(name = "Book_ID") : 기본 값으로 자식 테이블은 부모 테이블 id 컬럼명을 그대로 사용하나, 변경 시 해당 설정값을 추가해줘야 한다.
조인 전략은 엔티티마다 모두 테이블로 만들어주고 부모의 기본키를 기본키 + 외래키로 사용한다.
그래서 조회할 때 조인을 사용한다.
다만 객체는 타입이 있는데 테이블은 타입에 개념이 없어서 따로 컬럼을 추가해줘야 한다.
@DiscriminatorColumn(name = "DTYPE")
// Item 엔티티
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item{
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
...
}
// Album 엔티티
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
...
}
// Movie 엔티티
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
...
}
// Book 엔티티
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
}
...
Item 엔티티에서 상속 매핑을 하기 위해서 @Inheritance 추가하고, 조인 전략을 사용하기 위해 InheritanceType.JOINED을 추가한다.
그리고 부모에서 구분 컬럼을 지정한다. -> @DiscriminatorColumn(name = "DTYPE")
자식들은 구분 컬럼을 어떻게 저장할지 작성한다.
Album 엔티티는 부모로부터 DTYPE에 A라는 값으로 저장된다. -> @DiscriminatorValue("A")
Movie 엔티티는 부모로부터 DTYPE에 M라는 값으로 저장된다.
Book 엔티티는 부모로부터 DTYPE에 B라는 값으로 저장된다.
자식 엔티티에는 Id가 없는데 부모 테이블에서 Id를 자식 테이블에서 그대로 사용한다.
@PrimaryKeyJoinColumn을 사용한다.// Book 엔티티
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
BOOK_ID(이전: ITEM_ID)로 된다.장점
테이블이 정규화가 된다.
외래 키 참조 무결성 제약조건을 활용할 수 있다. -> 외래 키 참조로 ITEM_ID로 ITEM 테이블만 확인해도 된다.
참조 무결성 제약조건이란 자식 릴레이션의 외래 키의 값은 참조된 부모 릴레이션의 기본 키 값과 같아야 하며, 자식 릴레이션의 값이 변경될 때 부모 릴레이션의 제약을 받는다는 조건을 말한다.
외래 키는 NULL 값이거나 또는 부모 테이블의 기본 키 or 고유 키 값과 동일해야 한다.
저장공간을 효율적으로 사용할 수 있다.
단점
조회시 조인을 많이 사용해서 성능이 저하된다.
조회 쿼리가 복잡하다.
데이터 저장시 INSERT SQL을 2번 호출한다.
단일 테이블 전략은 말 그대로 테이블을 하나로 합치는 전략이다.
구분 컬럼(DTYPE)을 통해 어떤 자식 데이터가 저장되었는지 알 수 있다.

조인 전략과 형식은 비슷한데, 부모 테이블에 @Inheritance에 (strategy = InheritanceType.SINGLE_TABLE)을 지정한다.
또한 모든 테이블이 하나로 통합되어 있기 때문에 구분 컬럼은 필수다.
// Item 엔티티
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DisciminatorColumn(name = "DTYPE")
...
// 자식 엔티티
@Entity
@DiscriminatorValue("A")
...
@Entity
@DiscriminatorValue("M")
@Entity
@DiscriminatorValue("B")
...
장점
테이블이 하나이므로 조인이 필요없기 때문에 일반적으로 조회 성능이 빠르다.
조회 쿼리가 단순하다.
단점
자식들을 모두 하나의 테이블로 만들었기 때문에, 자식들마다 컬럼이 달라서 null 값이 많을 수 있다. -> null 값 허용 예) Album 엔티티에는 ACTOR 컬럼을 쓰지 않아 null 이다.
단일 테이블에 모든 것이 저장되므로 테이블이 커질 수록 조회 성능이 오히려 느릴 수 있다.
구현 클래스마다 테이블 전략은 자식 테이블이 부모 테이블의 모든 것을 다 가지고 있는 형태이다. 그래서 자식 엔티티마다 테이블을 다 만들어준다.
// Item 엔티티
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
...
// 자식 엔티티
@Entity
@DiscriminatorValue("A")
...
@Entity
@DiscriminatorValue("M")
@Entity
@DiscriminatorValue("B")
...
@Inheritance에 (strategy = InheritanceType.TABLE_PER_CLASS)을 지정한다.장점
서브 타입을 구분해서 처리할 때 효과적이다.
not null 제약조건을 사용할 수 있다. (각각의 테이블을 가지고 있기 때문)
단점
여러 자식 테이블을 함께 조회할 때 성능이 느리다. (SQL에 UNION을 사용한다)
자식 테이블을 통합해서 쿼리하기가 어렵다.
위에서는 부모-자식 클래스 모두 데이터베이스 테이블과 상속 관계로 매핑했는데, 여기선 다르다.
객체 입장에서 부모 클래스를 자식 클래스에게 매핑 정보만 제공하고 싶을때, @MappedSuperclass 를 사용한다.
@Entity가 있는 클래스의 경우 실제 테이블과 매핑되지만, @MappedSuperclass를 사용한 클래스는 실제 테이블과 매핑되지 않고, 공통 매핑 정보가 필요할 때 사용할 목적으로만 사용된다.
아래 그림처럼 id, name과 같은 공통 매핑 정보가 필요할 때 BaseEntity 클래스를 만들어서 Member와 Seller로부터 상속받게 설계를 한다.

// BaseEntity
@MappedSuperclass
public abstract class BaseEntity{
@Id @GeneratedValue
private Long id;
private String name;
...
}
// Member 엔티티
@Entity
public class Member exteds BaseEntity{
// ID, NAME 상속
private String email;
...
}
// Seller 엔티티
@Entity
public class Seller exteds BaseEntity{
// ID, NAME 상속
private String shopName;
...
}
@MappedSuperclass 특징을 정리하면 아래와 같다.
상속관계 매핑이 아니다.
그래서 엔티티도 아니고 테이블과 매핑하지도 않는다.
부모 클래스를 상속 받은 자식 클래스에 매핑 정보만 제공해준다.
직접 생성해서 사용할 일이 없기 때문에 추상 클래스로 사용하는 것을 권장한다.
엔티티와 다르게 BaseEntity 추상 클래스에는 조회나 검색이 불가능하다.
정리하면, @MappedSuperclass는 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할이고,
주로 등록일, 수정일, 등록자, 수정자와 같은 전체 엔티티에서 공통적으로 적용하는 정보를 모을 때 사용한다.
굿프렌즈 프로젝트에서 몇몇 테이블에 등록 시간과 수정 시간이 공통적으로 필요해서, @MappedSuperclass을 사용했다.
회원, 상품, 주문, 신고 테이블에 등록 시간과 수정 시간이 필요해서 BaseTimeEntity 추상 클래스를 만들어서 상속받도록 했다.
프로필, 신고 테이블에는 등록 시간만 필요해서 BaseCreateTimeEntity 추상 클래스를 만들어서 상속받도록 했다.
굿프렌즈 프로젝트에서는 공통으로 쓰는 클래스를 global -> common 패키지에 넣어두었다.
@EntityListeners(AuditingEntityListener.class)에 대한 부분은 나중에 다른 포스팅에서 설명할 에정이다.BaseTimeEntity - 등록 시간, 수정 시간
```java package woorifisa.goodfriends.backend.global.common;
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
다대일에서 다쪽이 연관관계 주인이다.
다대일 단방향은 가장 많이 사용하는 연관관계다.
외래 키가 있는 쪽을 연관관계의 주인으로 한다.
양쪽이 서로 참조되도록 개발한다.

일대다에서 일쪽이 연관관계 주인이다.
일대다 단방향 모델은 권장하지 않는다. (실무에서 거의 쓰이지 않는다)
DB 설계상 일대다 관계는 항상 다 쪽에 외래키가 있다.
객체와 테이블의 차이 때문에 반대편 테이블의 외래 키(Member의 외래 키)를 관리하는 특이한 구조이다.
@JoinColumn을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용해야 한다. (@JoinColumn을 사용하지 않으면 TEAM_MEMBER와 같은 중간에 테이블 하나가 추가된다)
Team 엔티티 클래스에 대해 java 코드로 작성하면 아래와 같다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // Team은 일대다에 '일'에 속하므로 @One으로 시작한다.
@JoinColumn(name ="TEAM_ID") // **
List<Member> members = new ArrayList<Member>();
// 아래 부분 생략
}
일대다 단뱡향 매핑의 단점으로는 아래와 같다.
엔티티가 관리하는 외래 키가 다른 테이블에 있다.
연관관계 관리를 위해 추가로 UPDATE SQL을 실행해줘야 한다.
정리하면, 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.

일대다 양방향 매핑은 공식적으로 존재하지 않는다.
만약 다인 Member 테이블에서 조회하고 싶다면 아래와 같이 작성하면 된다.
읽기 전용 필드(@JoinColumn(insertable=false, updatable=false))을 사용해서 양방향 처럼 사용한다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable=false, updatable=false) // 읽기 전용 필드를 사용
private Team team;
// 아래 부분 생략
}
다대일 양방향 매핑을 사용하자.주 테이블이나 대상 테이블 중에 외래 키를 선택 가능하다.
둘 중 한군데에만 넣어도 상관없다.
외래 키에 데이터베이스 유니크(UNI) 제약조건을 추가한다.

다대일(@ManyToOne) 단방향 매핑과 유사하다.
@ManyToOne 에서 @OneToOne으로 바꾼 것 뿐이다.
Member 클래스
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
// 아래 부분 생략
}

다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인이다.
주인이 아닌 곳(반대편)은 mappedBy를 적용한다.
Locker 클래스
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
// 읽기 전용
@OneToOne(mappedBy = "locker") // Member 클래스에 Locker의 변수명을 의미한다.
private Member member;
// 아래 부분 생략
}
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
따라서 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어나가야 한다.
객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.

@ManyToMany 사용하고, @JoinTable로 연결 테이블을 지정한다.
다대다 매핑은 단방향, 양방향 모두 가능하다.
결론부터 말하자면, 다대다 매핑는 실무에서 사용하지 않는다.
연결 테이블이 단순히 연결만 하고 끝나지 않는다.
주문시간, 수량 같은 데이터가 들어올 수 있다.

다대다 한계를 극복하기 위해 연결 테이블용 엔티티를 추가한다. (연결 테이블을 엔티티로 승격)
@ManyToMany -> @OneToMany, @ManyToOne으로 바꿔준다.

@ManyToMany 사용하지 않는다.@JoinColumn은 외래 키를 매핑할 때 사용하는 어노테이션이다.
@ManyToOne은 다대일 관계 매핑할 때 사용하는 어노테이션이다.
다대일에서 ‘다’가 연관관계 주인이 되어야 한다.

@OneToMany은 일대다 관계를 매핑할 때 사용하는 어노테이션이다.
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다 - 조영호(객체지향의 사실과 오해)
이번 글에서는 JPA에서 외래 키로 조인을 사용하고, 객체는 참조를 사용해서 연관관계를 매핑하는 방법에 대해서 알아보자.
예를 들어, 회원과 팀이 있고,
회원은 하나의 팀에만 소속될 수 있고,
회원(다)과 팀(일)은 다대일 관계라고 시나리오를 가정해보자.
이때, 객체를 테이블에 맞추어서 모델링을 하면, Member, Team 두개의 테이블을 통해 식별자(member.getId(), team.getId())로 조회해야 한다.
이처럼 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
따라서 객체지향 설계로 협력 관계를 만들기 위해 아래와 같이 생각해서 매핑한다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
객체는 참조를 사용해서 연관된 객체를 찾는다.
객체 지향적인 모델링(객체 연관관계 사용)을 하면 아래 그림과 같다.

@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
// @Column(name ="TEAM_ID")
// private Long teamId;
@ManyToOne // User: M이므로 '다'에 속하기 때문에 Many~로 시작한다.
@JoinColumn(name = "TEAM_ID") // 조인할 테이블의 식별키를 작성한다. => 외래 키
private Team team;
// 아래 Getter 생략
}

//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();

@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne // Member: M이므로 '다'에 속하기 때문에 Many~로 시작한다.
@JoinColumn(name = "TEAM_ID") // 조인할 테이블의 식별키를 작성한다. => 외래 키
private Team team;
// 아래 부분 생략
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team") // Team은 일대다에 '일'에 속하므로 @One으로 시작한다.
List<Member> members = new ArrayList<Member>();
// 아래 부분 생략
}
(mappedBy = "team")은 Member 엔티티에 있는 Team의 변수명인 team과 연관되었다는 걸 의미한다.객체와 테이블 간에 연관관계를 맺는 차이를 이해해보자.
객체는 연관관계가 2개이고, 테이블은 연관관계가 1개로 구성되어 있다.
객체) 회원 -> 팀 연관관계 1개(단방향), 팀 -> 회원 연관관계 1개(단방향)
테이블) 회원 <-> 팀의 연관관계 1개(양방향)

객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
따라서 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
이때, 둘 중 하나로 외래 키를 관리해야 한다.
양방향 매핑에는 다음과 같은 규칙이 있다.
객체의 두 관계중 하나를 연관관계의 주인으로 지정해야 한다.
연관관계의 주인만이 외래 키를 관리(등록, 수정)할 수 있다.
주인은 mappedBy 속성을 사용하지 않는다.
주인이 아닌쪽은 읽기만 가능하므로 mappedBy 속성으로 주인을 지정해준다.
결론부터 말하면, 외래 키의 위치를 기준으로 연관관계 주인을 정하면 된다. (연관관계 주인 => 외래 키가 있는 곳)
여기서는 Member.team이 연관관계의 주인이다.

Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team); //**중요**
em.persist(member);

단방향 매핑만으로도 이미 연관관계 매핑은 완료된다.
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
JPQL에서 역방향으로 탐색할 일이 많을 때 사용한다.
정리하면, [1] 처음에는 단방향 매핑으로 잡은 뒤, [2] 필요할 때 양방향으로 추가해준다. (단방향 -> 양방향으로 바꿀 때 테이블에 영향을 주지 않는다.)
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
@Entity가 붙은 클래스는 JPA가 관리하며, 엔티티 라 부른다.
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity가 필수이다.
몇 가지 주의점이 있다.
기본 생성자가 필수이다. (파라미터가 없는 public 또는 protected 생성자)
final 클래스, enum 클래스, interface, inner 클래스는 사용할 수 없다.
저장할 필드에 final 사용할 수 없다.
@Entity 속성 정리
속성: name: JPA에서 사용할 엔티티 이름을 지정한다.
기본값: 클래스 이름을 그대로 사용한다.
@Entity(name = "User")같은 클래스 이름이 없으면 가급적 기본값을 사용한다.
@Table은 엔티티와 매핑할 테이블을 지정한다.
속성 1) name : 클래스 이름이 MySQL 예약어와 겹치면 매핑되는 테이블 이름은 복수형으로 지정한다.
기본값은 엔티티 이름을 사용한다.
ex) Entity Class 이름 = User -> @Table(name = "users")
속성 2) catalog : 데이터베이스 catalog를 매핑한다.
속성 3) schema : 데이터베이스 schema를 매핑한다.
속성 4) uniqueConstraints : DDL 생성 시에 유니크 제약 조건을 생성한다.
User 클래스(엔티티)
package woorifisa.goodfriends.backend.user.domain;
import woorifisa.goodfriends.backend.global.common.BaseTimeEntity;
import woorifisa.goodfriends.backend.user.exception.InvalidUserException;
import javax.persistence.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Table(name = "users")
@Entity
public class User extends BaseTimeEntity {
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-z0-9._-]+@[a-z]+[.]+[a-z]{2,3}$");
private static final int MAX_NICKNAME_LENGTH = 20;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "nickname", nullable = false)
private String nickname;
@Column(name = "profile_image_url", nullable = false)
private String profileImageUrl;
protected User() {
}
public User(String email, String nickname, String profileImageUrl) {
validateEmail(email);
validateNickname(nickname);
this.email = email;
this.nickname = nickname;
this.profileImageUrl = profileImageUrl;
}
private void validateEmail(final String email) {
Matcher matcher = EMAIL_PATTERN.matcher(email);
if (!matcher.matches()) {
throw new InvalidUserException();
}
}
private void validateNickname(final String nickname) {
if(nickname.isEmpty() || nickname.length() > MAX_NICKNAME_LENGTH) {
throw new InvalidUserException(String.format("이름은 1자 이상 %d자 이하여야 합니다.", MAX_NICKNAME_LENGTH));
}
}
// getter
}
DDL이란 데이터베이스를 정의하는 언어이며, 데이터를 생성, 수정, 삭제하는 등의 데이터 전체의 골격을 결정하는 역할을 하는 언어이다.
테이블 관련 명령어 : create(생성), alter(수정), drop(삭제), truncate(초기화)
DDL을 애플리케이션 실행 시점에 자동으로 생성해준다. (테이블 -> 객체 중심)
데이터베이스 방언(DB Dialect) 을 활용해서 데이터베이스에 적절한 DDL을 생성한다.
Hibernate의 경우엔 persistence.xml에서 hibernate.dialect 설정 값을 변경하면 된다.
예를 들어, H2 Dialect 설정 코드는 <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" /> 이다.
```markdown
SQL은 다음과 같이 표준 SQL인 ANSI SQL이 있으며, 이외에 각 DBMS Vendor(벤더, 공급업체)인 MS-SQL, Oracle, MySQL, PostgreSQL 에서 자신만의 기능을 추가한 SQL이 있다.
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.

웹 애플리케이션을 개발하면서 고객으로부터 요청이 오면, 엔티티 매니저 팩토리를 통해 엔티티 매니저를 생성한다.
엔티티 매니저는 데이터베이스 커넥션 풀을 사용해서 DB를 사용하게 된다.
영속성 컨텍스트란 엔티티를 영구 저장하는 환경이라는 뜻을 가진 논리적인 개념으로 어플리케이션과 DB 사이에서 객체를 보관하는 가상의 DB같은 역할을 한다.
즉, 애플리케이션에서 DB에 저장하기 전에 사용을 하는 임시 저장 공간이라고 이해를 하면 편할 것이다.
EntityManager.persist(entity);엔티티 매니저를 통해서 영속성 컨텍스트에 접근한다.
엔티티 매니저(Entity Manager)란 영속성 컨텍스트에 접근하여 엔티티에 대한 데이터베이스 작업을 제공하는 것을 의미한다.
엔티티 매니저(Entity Manager)는 엔티티를 저장하고, 수정하고, 삭제하고 조회하는 등 엔티티와 관련된 모든 일을 처리한다.
영속성 컨텍스트를 통해 데이터의 상태 변화를 감지하고 필요한 쿼리를 자동으로 수행한다.

비영속 상태라고 한다.
영속 상태라고 한다.
import javax.persistence.Persistence;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin()
try {
// 비영속
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
// 영속
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
ex.close();
}
}
}
Member 엔티티를 영속성 컨텍스트에서 분리한 상태를 준영속 상태라고 한다.
준영속 상태로 만드는 방법
em.detach(entity) : 특정 엔티티만 준영속 상태로 전환
em.clear() : 영속성 컨텍스트를 완전히 초기화
em.close() : 영속성 컨텍스트를 종료
객체를 삭제한 상태를 삭제라고 하며, 코드에서는 em.remove(member);를 의미한다.
//삭제 대상 엔티티 조회
Member memberA = em.find(Member.class, “memberA");
em.remove(memberA); //엔티티 삭제
영속성 컨텍스트 내부에는 엔티티 인스턴스를 캐싱하는 1차 캐시 가 있다.
일반적으로 트랜잭션 내에서 유효한 생명주기를 갖는다.


member1를 조회하면, JPA는 DB가 아닌 1차 캐시에 저장된 값을 가져온다.
그런데 만약 member2를 조회할 때, 1차 캐시에 없다면, DB를 조회한다. 그런 다음에 해당 값을 1차 캐시에 저장하고 값을 반환한다.
사실, 엔티티 매니저라는 건 데이터베이스 트랜잭션이 끝날 때 종료된다. ⇒ 비즈니스가 끝나면, 해당 엔티티 매니저가 사라져버린다.
차 캐시라는건 데이터베이스 하나의 트랜잭션 안에서만 효과가 있기 때문에 성능에 대한 장점은 없다.
영속성 컨텍스트는 영속성 컨텍스트 안에서 영속 엔티티들의 동일성을 보장한다.
따라서 같은 식별자를 통해 1차 캐시에서 얻은 엔티티 인스턴스는 == 연산을 통해 동일성을 보장한다.
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 true
JPA가 영속 엔티티의 동일성을 보장한다. 마치 자바 컬렉션에서 똑같은 레퍼런스 객체를 꺼내오는 것과 같다. 1차 캐시가 있기 때문에 가능한 것이다.
같은 트랜잭션 안에서 비교할 때에만 동일하다는 것이다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA); // 1
em.persist(memberB); // 2
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // 3 [트랜잭션] 커밋
em.persist(memberA);

[1] em.persist(memberA);을 통해 memberA가 1차 캐시에 들어간다.(집어 넣는다)
[2] 그러면서 동시에 JPA가 이 엔티티를 분석해서 INSERT SQL 쿼리를 생성한다.
[3] 그래서 쓰기 지연 SQL 저장소에 쌓아둔다.
em.persist(memberB);

[4] 그런 다음에 em.persist(memberB); 을 하면, memberB도 1차 캐시에 들어간다.(집어 넣는다)
[5] 그리고 이때, INSERT SQL 쿼리를 생성해서 쓰기 지연 SQL 저장소에 차곡차곡 쌓아둔다.
transaction.commit();

[6] 그리고 transaction.commit(); 을 통해 커밋하는 순간 쓰기 지연 SQL 저장소에 있던 데이터들이 flush가 되면서, 데이터베이스에 INSERT SQL을 보낸다.
[7] 그리고 실제 데이터베이스가 commit 된다.
변경 감지란 JPA를 사용하여 데이터를 수정하려면 Entity를 조회하여 조회된 Entity 데이터를 변경만 하면 데이터베이스에 자동으로 반영이 되도록 하는 기능이다.
[1] 데이터베이스에 커밋하면, 내부적으로 flush가 발생하고
[2] JPA가 엔티티하고 스냅샷을 비교한다.
엔티티 인스턴스 값을 읽어온 그 시점의 최초 상태를 영속성 컨텍스트에 저장하는 데, 이를 스냅샷이라 부른다.만약 MemberA에 대한 데이터가 변경되면 쓰기 지연 SQL 저장소에 [3] UPDATE SQL 쿼리를 생성한다.
그리고 [4] UPDATE SQL 쿼리를 flush를 통해 데이터베이스에 반영하고
[5] commit 하게 된다.
flush 란 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 것을 말한다.
플러시가 발생하면, 다음 3가지가 일어난다.
변경 감지
수정된 엔티티의 쓰기 지연 SQL 저장소에 등록
쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제 쿼리)
영속성 컨텍스트를 플러시하는 방법은 3가지가 있다.
em.flush() - 직접 호출 -> 쿼리가 DB에 반영되어 버린다.
트랜잭션 커밋 - 플러시 자동 호출
JPQL 쿼리 실행 - 플러시 자동 호출
플러시 모드 옵션은 기본적으로 AUTO(em.setFlushMode(FlushModeType.AUTO)로 사용된다. (거의 사용할 일은 없다)
FlushModeType.AUTO : 커밋이나 쿼리를 실행할 때 플러스 (기본값)
FlushModeType.COMMIT : 커밋할 때만 플러시
정리하면, 플러시는
영속성 컨텍스트를 비우지 않고, 변경 내용을 데이터베이스에 동기화하고
트랜잭션 커밋 직전에만 동기화하면 된다.