
데이터베이스에 쓰일 테이블과 칼럼을 정의한다
Product(상품정보, 공급업체번호, 상품이름, 상품가격, 상품재고, 상품분류번호, 상품 생성 일자, 상품 정보 변경 일자)
ProductDetail(상품정보번호, 상품번호, 상품설명, , 상품 정보 생성 일자, 상품 정보 변경 일자)
Provider(공급업체번호, 업체 이름, 업체 생성 일자, 업체 정보 변경 일자)
Product와 ProductDetail은 일대일 관계
Product와 ProductDetail은 다대일 관계
@Entity
@Getter
@NoArgsConstructor
@ToString(callSuper=true)
@EqualsAndHashCode(callSuper=true)
@Table(name="product")
public class Product extends BaseEntity{
@id // 모든 Entity는 @id 어노테이션이 꼭 필요하다.
@GenerateValue(strategy = GenerationType.IDENTITY) //기본값 생성을 데이터베이스에 위임하는 방식, 데이터베이스의 AUTO_INCREMENT를 사용해 기본값을 생성
private Long id;
@Column(nullable=false) // 이 어노테이션을 명시하지 않아도 클래스의 필드는 자동으로 테이블의 칼럼으로 매핑됨
private String name;
@Column(nullable=false)
private String price;
@Column(nullable=false)
private String stock;
@ManyToOne
@JoinColumn(name="provider_id")
@ToString.Exclude
private Provider provider;
}
@Entity
@Getter
@NoArgsConstructor
@ToString(callSuper=true)
@EqualsAndHashCode(callSuper=true)
@Table(name="product_detail")
public class ProductDetail extends BaseEntity{
@id // 모든 Entity는 @id 어노테이션이 꼭 필요하다.
@GenerateValue(strategy = GenerationType.IDENTITY) //기본값 생성을 데이터베이스에 위임하는 방식, 데이터베이스의 AUTO_INCREMENT를 사용해 기본값을 생성
private Long id;
@Column(nullable=true) // 이 어노테이션을 명시하지 않아도 클래스의 필드는 자동으로 테이블의 칼럼으로 매핑됨
private String description;
@OneToOne
@JoinColumn(name="product_number")
private Product product;
}
@Entity
@Getter
@NoArgsConstructor
@ToString(callSuper=true)
@EqualsAndHashCode(callSuper=true)
@Table(name="provider")
public class Provider extends BaseEntity{
@id // 모든 Entity는 @id 어노테이션이 꼭 필요하다.
@GenerateValue(strategy = GenerationType.IDENTITY) //기본값 생성을 데이터베이스에 위임하는 방식, 데이터베이스의 AUTO_INCREMENT를 사용해 기본값을 생성
private Long id;
@Column(nullable=true) // 이 어노테이션을 명시하지 않아도 클래스의 필드는 자동으로 테이블의 칼럼으로 매핑됨
private String name;
}
엔티티를 작성할 때는 어노테이션을 많이 사용한다. 그중엔 테이블과 매핑하기 위해 사용하는 어노테이션도 있고, 다른 테이블과의 연관관계를 정의하기 위해 사용하는 어노테이션 자동으로 값을 주입하기위한 어노테션도있다
@Entity
- 해당 클래스가 엔티티임을 명시하기 위한 어노테이션이다. 클래스 자체는 테이블과 일대일로 매칭되며 해당클래스의 인스턴스는 매핑되는 테이블에서 하나의 레코드를 의미한다.
@Table
- 엔티티 클래스는 테이블과 매핑되므로 특별한 경우가 아니면 이 어노테이션이 필요하지 않다. 사용하는 경우는 클래스의 이름과 테이블의 이름을 다르게 지정해야하는 경우이다. @Table 어노테이션을 명시하지 않으면 테이블의 이름과 클래스의 이름이 동일하다는 의미이며, 서로다른 이름을 쓰려면 @Table(name=값) 형태로 데이터베이스의 테이블명의 명시해야한다. 대체졸 자바의 명명법과 데이터베이스가 사용하는 명명법이 다르기 때문에 자주 사용된다
@Id
- 엔티티 클래스의 필드는 테이블과 칼럼과 매핑된다. @Id 어노테이션이 선언된 필드는 테이블의 기본값 역할로 사용된다. 모든 엔티티는 @Id 어노테이션이 필요하다.
@GeneratedValue
- 일반적으로 @Id 어노테이션과 함께 사용된다.
AUTO
IDENTITY
SEQUENCE
TABLE
엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑된다. 그래서 별다른 설정을 하지 않을 예정이라면 이 어노테이션을 명시하지 않아도 괜찮다
@Column 어노테이션에서 많이 사용하는 요소는 다음과 같다
엔티티 클래스에는 선언돼 있는 필드지만 데이터베이스에는 필요없을 경우 이 어노테이션을 사용해 데이터베이스에서 이용하지 않게 할 수 있다.
NoArgsConstructor
AllArgsConstructor
RequiredArgsConstructor
ToString
EqualsAndHashCode
Data
단방향 : 두 엔티티의 관계에서 한쪽의 엔티티만 참조하는 형식
양방향 : 두 엔티티의 관계에서 각 엔티티가 서로의 엔티티를 참조하는 형식
연관관계가 설정되면 한 테이블에서 다른 테이블의 기본값을 외래키로 갖게 된다
일반적으로 외래키를 가진 테이블이 그 관계의 주인이 되며, 주인을 외래키를 사용할수 있으나 상대 엔티티는 읽는 작업만 수행 할 수있다
속성
name : 매핑할 외래키의 이름을 설정
referencedColumnName : 외래키가 참조할 상대 테이블의 칼럼명을 지정
foreignkey : 외래키를 생성하면서 지정할 제약 조건을 설정(unique, nullable, insertable, updatable)
@OneToOne 어노테이션은 기본 fetch 전략으로 EAGER, 즉 즉시로딩 전략으로 채택되어있다. 그리고 optional() 메서드는 기본값으로 true가 설정돼있다. 기본값이 true인 상태는 매핑되는 값이 nullable이라는 것을 의미한다. 만약 반드시 값이 있어야 한다면 @OneToOne(optional = false)로 설정하면된다. 이러면 null을 허용하지않게 된다. 그리고 쿼리문이 left outer join이 inner join으로 바뀌어 실행된다.
JPA에서 지연로딩과 즉시로딩은 중요한 개념이다. 엔티티라는 객체의 개념으로 데이터베이스를 구현 했기 때문에 연관 관계를 가진 각 엔티티 클래스에는 연관관계가 있는 객체들이 필드에 존재하게 된다. 연관관계와 상관없이 즉각 해당 엔티티의 값만 조회하고 싶거나 연관관계를 가진 테이블의 값도 조회하고 싶은 경우 등 여러 조건들을 만족하기 위해 등장한 개념이 지연로디오가 즉시로딩이다.
단방향
@OneToOne
@JoinColumn(name="product_number")
private Product product;
양방향으로 설정을 하면
@OneToOne
@JoinColumn(name="product_number")
private Product product;
@OneToOne(mappedBy="product")
private ProductDetail productDetail;
여러 테이블 끼리 연관관계가 설정돼 있어 여러 letf outer join이 설정되는것은 괜찮으나 위와 같이 양쪽에서 외래키를 가지고 left outer join이두번이나 수행되는 경우는 효율성이 떨어진다. 실제 데이터베이스에서도 테이블 간 연관 관계를 맺으면 한쪽 테이블이 외래키를 가지는 구조로 이뤄진다. JPA에서도 실제 데이터베이스의 연관관계를 반영해서 한쪽의 테이블에서만 외래키를 바꿀수 있도록 정하는것이좋다. 이 경우 양방향으로매핑하되 한쪽에게만 외래키를 줘야하는데 이때 사용되는 속성값이 mappedBy 이다. mappedBy는 어떤 객체가 주인인지 표시하는 속성이다.
위처럼 코드를 작성하면 ProductDetail엔티티가 Product엔티티의 주인이 된다.
양방향으로 연관관계가 설정되면 ToString을 사용할 때 순환참조가 발생하기 때문에 필요한 경우가 아니라면 대체로 단방향으로 연관관계를 설정하거나 양방향 설정이 필요한 경우에는 순환참조 제거를 위해 아래와 같이 exclude를 사용해 ToString에서 제외 설정을 하는것이 좋ㄷ
@OneToOne(mappedBy="product")
@ToString.Exclude
private ProductDetail productDetail;
단방향
@ManyToOne
@JoinColumn(name="provider_id")
@ToString.Exclude
private Provider provider;
양방향
@ManyToOne
@JoinColumn(name="provider_id")
@ToString.Exclude
private Provider provider;
@OneToMany(mappedBy="provider", fetch=FetchType.EAGER)
@ToString.Exclude
private List<Product> productlist = new ArrayList<>();
일대다 연관관계의 경우 여러 상품 엔티티가 포함 될 수 있어 컬렉션 형식으로 필드를 생성한다.
@OneToMany의 기본 fetch 전략은 Lazy이다
일대다 양방향 매핑은 어느 엔티티 클래스에서도 연관관계의 주인이 될수 없기 때문에 다루지 않는다.
@OneToMany(fetch=FetchType.EAGER)
@JoinColumn(name="category_id")
private List<Product> productlist = new ArrayList<>();
이렇게 상품 분류 엔티티에서 @OneToMany와 @JoinColumn을 사용하면 상품 엔티티에서 별도의 설정을 하지 않아도 일대다 단방향 연관관계가 매핑된다. 이어노테이션을 사용하지 않으면 중간 테이블로 Join 테이블이 생성되는 전략이 채택된다.
다대다 연관관계는 실무에서 거의 사용되지 않는 구성이다. 각 엔티티에서 서로를 리스트로 가지는 구조가 만들어진다. 이런 경우에는 교차 엔티티라고 부르는 중간 테이블을 생성해서 다대다 관계를 일대다 또는 다대일관계로 해소한다
리스트로 필드를 가지는 객체에서는 외래키를 가지지 않기 때문에 별도의 @JoinColumn은 설정하지 않아도 된다.
@ManyToMnay
@ToString.Exclue
private List<Product> products = new ArrayList<>();
영속성 전이(cascade)란 특정 엔티티의 역속성 상태를 변경할 때 그 엔티티와 연관된 엔티티의 영속성에도 영향을 미쳐 영속성 상태를 변경하는것의 의미.
| 종류 | 설명 |
|---|---|
| ALL | 모든 영속 상태 변경에 대해 영속성 전이를 적용 |
| PERSIST | 엔티티가 영속화할 때 연관된 엔티티도 함께 영속화 |
| MERGE | 엔티티를 영속성 컨텍스트에 병합할 때 연관된 엔티티도 병합 |
| REMOVE | 엔티티를 제거할 때 연관된 엔티티도 새로고침 |
| REFRESH | 엔티티를 새로고침할때 연관된 엔티티도 새로고침 |
| DETACH | 엔티티를 영속성 컨텍스트에서 제외하면 연관된 엔티티도 제외 |
@OneToMany(mappedBy="provider", cascade=CascadeType.PERSIST)
@ToString.Exclude
private List<Product> productlist = new ArrayList<>();
JPA에서 고아란 부모 엔티티와 연관관계가 끊어진 엔티티를 의미
JPA에는 이런한 고아 객체를 자동으로 제거하는 기능이 있음
자식 엔티티가 다른 엔티티와 연관관계를 가지고 있다면 이 기능은 사용하지 않는 것이 좋다
@OneToMany(mappedBy="provider", cascade=CascadeType.PERSIST, orphanRemoval=true)
@ToString.Exclude
private List<Product> productlist = new ArrayList<>();
orphanRemoval=true속성은 고가 객체를 제거하는 기능이다
// user.java
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
private String name;
@NonNull
private String email;
@Enumerated(value = EnumType.STRING)
private Gender gender;
// 주소 정보
private String city; // 도시
private String district; // 구
private String detail; // 상세주소
private String zipCode; // 우편번호
}
@Embeddable를 붙여줘야합니다.@Embeddable : 값 타입을 정의하는 곳에 표시@Embedded : 값 타입을 사용하는 곳에 표시// user.java
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
private String name;
@NonNull
private String email;
@Enumerated(value = EnumType.STRING)
private Gender gender;
@Embedded
private Address address;
}
// Address.java
@Embeddable
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
// 주소 정보
private String city; // 도시
private String district; // 구
@Column(name = "address_detail")
private String detail; // 상세 주소
private String zipCode; // 우편번호
}
@AttributeOverrides, @AttributeOverride를 통해 하나의 class를 사용해 여러 표현을 할 수 있습니다. (객체의 재활용)@AttributeOverrides, @AttributeOverride를 사용해 column의 이름을 전부 재정의하여 사용하기에 코드가 지저분해 보일 수 있다. -> 객체를 재활용 하지 않고 따로 선언해서 하는 대신 깔끔하게 보이는 코드를 작성할 지, 객체의 재활용을 하는 코드를 작성할지는 개발자가 결정해야한다.@Entity
public class User {
.....
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "home_city")), // city를 home_city라는 column명으로 사용
@AttributeOverride(name = "district", column = @Column(name = "home_district")),
@AttributeOverride(name = "detail", column = @Column(name = "home_address_detail")),
@AttributeOverride(name = "zipCode", column = @Column(name = "home_zipCode"))
})
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "company_city")),
@AttributeOverride(name = "district", column = @Column(name = "company_district")),
@AttributeOverride(name = "detail", column = @Column(name = "company_address_detail")),
@AttributeOverride(name = "zipCode", column = @Column(name = "company_zipCode"))
})
private Address companyAddress;
.....
}
주의할 점
@Embedded
private Address homeAddress;
@Embedded
private Address companyAddress;
하나의 class를 통해 여러개의 정보를 만들고 싶은데 위와 같이 @AttributeOverrides, @AttributeOverride를 통해 column명을 재정의해주지 않으면 아래와 같이 Repeated column in mapping for entity 에러가 나오니 꼭 column명을 재정의해줘서 사용해야한다.
Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.MappingException: Repeated column in mapping for entity: com.example.jpa_study.entity.User column: city (should be mapped with insert="false" update="false")
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:421)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1845)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1782)
... 107 more
스프링에서는 상속전략을 지정해줄있음
@Inheritance(strategy= InheritanceType.JOINED)
@Inheritance(strategy= InheritanceType.SINGLE_TABLE)
@Inheritance(strategy= InheritanceType.TABLE_PER_CLASS)
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "item_id")
private Item item;
}
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
}
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("A")
@Getter
@Setter
public class Album extends Item {
private String artist;
private String etc;
}
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("B")
@Getter
@Setter
public class Book extends Item {
private String author;
private String isbn;
}
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("M")
@Getter
@Setter
public class Movie extends Item {
private String director;
private String actor;
}
기본값 속성인 EnumType.ORDINAL은 인덱스로 들어간다. 인덱스로하면 중간에 다른 상태의 enum값을 넣으면 인덱스가 달라지기 때문에 속성타입을 꼭 STRING을 써야한다.
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //READY, COMP
엔티티에는 가급적 Setter를 사용하지 말자
Setter가 모두 열려있다 → 변경 포인트가 너무 많아서, 유지보수가 어렵다. 나중에 리펙토링으로 Setter 제거
모든 연관관계는 지연로딩으로 설정!
즉시로딩 지연로딩
@OneToOne, @ManyToOne → 기본 fetch가 EAGER
@OneToMany → 기본 fetch가 LAZY
즉시로딩( EAGER )은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다.
실무에서 모든 연관관계는 지연로딩( LAZY )으로 설정해야 한다.
연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 엔티티 그래프 기능을 사용한다.
@XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한다.
컬렉션은 필드에서 초기화 하자.
컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
null 문제에서 안전하다.
하이버네이트는 엔티티를 영속화 할 때, 컬랙션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다. 만
약 getOrders() 처럼 임의의 메서드에서 컬력션을 잘못 생성하면 하이버네이트 내부 메커니즘에 문제가 발생
할 수 있다. 따라서 필드레벨에서 생성하는 것이 가장 안전하고, 코드도 간결하다.
Member member = new Member();
System.out.println(member.getOrders().getClass());
em.persist(member);
System.out.println(member.getOrders().getClass());
//출력 결과
class java.util.ArrayList
class org.hibernate.collection.internal.PersistentBag