728x90

Entity 공통으로 포함할 createdAt, updatedAt 필드를 

BaseEntity를 만들어 다른 Entity들이 상속할 수 있게 SuperClass로 사용

@Data

@MappedSuperclass

public class BaseEntity {

@CreationTimestamp

@Column(updatable = false)

private LocalDateTime createdAt;

@UpdateTimestamp

private LocalDateTime updatedAt;

}

entity 관리를 위해 entity 생성, 수정 시간을 기록하는 createAt, updatedAt을 가진다. 

ToString, HashCode, Equals를 포함하는 Data 어노테이션과 

BaseEntity 테이블을 따로 만들지 않고 superclass로 하여 자식 클래스들이 필드만 사용할 수 있게 하기 위해 

@MappedSuperclass 어노테이션을 사용한다. 

 

@Data

@EqualsAndHashCode(callSuper = true)

@ToString(callSuper=true)

@AllArgsConstructor

@NoArgsConstructor

@Builder

@Entity

public class ProductDetail extends BaseEntity {

ProductDetail 엔터티는 BaseEntity를 상속하고 

superclass(BaseEntity)의 @Data 어노테이션(Equals,HashCode,ToString)을 사용하기위해 

@EqualsAndHashCode,ToString 어노테이션에 callSuper 속성을 true로 한다. 

 

 

product와 productDetail은 1:1 관계이고 

ProductDetail이 Product_id를 FK로 갖고 있으므로 OwnerTable이라 할 수 있다. 

 

public class Product extends BaseEntity {

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long productId;

 

private String name;

private Integer price;

private Integer stock;

 

 

//1:1 (Owner table 아님)

@OneToOne(mappedBy = "product", cascade = CascadeType.PERSIST)

private ProductDetail productDetail;

 

Product는 ProductDetail 객체를 가지고 

1:1 맵핑이므로 @OneToOne 어노테이션을 붙인다. 

 

OwnerTable이 아닌 클래스에서 OwnerTable 의 클래스를 mapping할 때 속성으로 

mappedBy를 작성해줘야 한다. 

Product가 가지고 있는 ProductDetail은 Product에 의해 mapping 되므로 mappedBy product 

 

cascade는 product를 작업할때 product가 가진 productDetail도 함께 작업할지에 대한 옵션이다. 

all, detach, merge, persist, refresh, remove가 있고 persist는 영속성에 추가할 때 관련 엔터티도 함께 추가된다. 

 

public class ProductDetail extends BaseEntity {

@Id

@GeneratedValue(strategy = GenerationType.SEQUENCE)

private Long productDetailId;

private String description;

 

/*

* 1:1 OWNER TABLE 참조키를 갖고 있어서

*/

@OneToOne(cascade = CascadeType.PERSIST)//save할 때 연관된 프로덕트도 같이 하겠다.

@JoinColumn(name="product_id")

@ToString.Exclude

private Product product;//tostring시 product의 detail의 product의 detail 순환참조 stackoverflow

 

 

ProductDetail은 Product를 FK로 가지고 있으므로 OwnerTable이라고 할 수 있다. 

마찬가지로 @OneToOne 어노테이션을 사용하고 cascade 설정을 줄 수 있다. 

오너테이블에서 FK 엔터티를 맵핑할때는 mappedBy 속성이 필요없다. 

대신 @JoinColumn 어노테이션을 작성해줄 수 있다. (default로 해주긴함)

JoinColumn 은 default로 name="FK" 를 가진다. 

JoinColumn의 이름은 productDetail 테이블에서 product를 컬럼으로 가져올때 가질 물리적 이름이다. 

productDetail 테이블에서 Product엔터티는 product_id 라는 FK로 가져가기 때문에 name="product_id"

관례상 보통 모든 엔터티의 PK(Id)는 [entityName]Id 가 아닌 "Id"로 동일하게 주더라도 

JPA에서 productDetail에 product는 product_id로 설정해준다. 

 

만약 ProductDetail에서 Product를, 혹은 Product에서 ProductDetail를 참조하여 ToString을 호출하면 

Product의 ToString은 product가 가진 ProductDetail을 ToString하다가 또 Product를 만나 Product를 ToString 하고 

순환참조하다 stackOverflow에 빠질 것이다. 

이를 막기 위해서 

@ToString.Exclude 어노테이션을 하면 ProductDetail에서 ToString 하는 과정에서 Product를 제외하고 ToString 하기 때문에 순환참조를 막을 수 있다. 

 

ProductDetail Save() 

ProductDetail(Owner Table)에서 save 할때 

productDetail이 가지는 product를 set 해주면 

알아서 product에도 product가 가지는 productDetail을 set해준다. 

Product product = Product.builder().name("name").price(1111).stock(111).build();

ProductDetail detail = ProductDetail.builder().description("des").build();

//연관관계설정

detail.setProduct(product);

productDetailRepository.save(detail);

 

>>> ProductDetail->ProductProduct(super=BaseEntity(createdAt=2023-10-11T18:01:41.229506, updatedAt=2023-10-11T18:01:41.229506), productId=2, name=name, price=1111, stock=111, productDetail=ProductDetail(super=BaseEntity(createdAt=2023-10-11T18:01:41.247503, updatedAt=2023-10-11T18:01:41.247503), productDetailId=1, description=des))

 

Product테이블 save 결과

 

ProductDetail 테이블 save 결과 

 

productDetail는 product를 가지기 때문에 productDetail에 product를 set 하여야 하고 

product는 그렇지 않아도 상관없다. 

productDetail의 product에 cascade persist가 적용되어 product도 함께 save 되었다. 

 

반면 

void productWithProductDetailSaveAndRead() {

 

ProductDetail productDetail=ProductDetail.builder().description("desc").build();

 

Product product=Product.builder().name("name").price(60000).stock(300).build();

/*

* 연관관계설정(OWNER테이블아닌경우)

* Product-->ProductDetail

*/

product.setProductDetail(productDetail);

productDetail.setProduct(product);

productRepository.save(product);

}

오너테이블이 아닌 product에서 product를 함께 save 하기 위해서는 

product는 productDetail을 set 해야하고 

productDetail은 product를 set 해야한다. 

 

만약 

product.setProductDetail(productDetail);

// productDetail.setProduct(product);

productRepository.save(product);

productDetail에 product를 set하지 않으면 

productDetail에 product_id가 null로 된 것을 확인할 수 있다. 

 

Provider와 Product는 1:N 관계이다. 

//1:n(owner table 아님)

@Builder.Default//초기값 주기

@OneToMany(mappedBy = "provider",cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)

private List<Product> products=new ArrayList<>();

Provider(One) Product(Many)이므로 

Provider의 Product는 컬렉션으로 가지고 OneToMany 어노테이션을 사용한다. 

provider는  ownerTable이 아니므로 mappedBy 속성을 부여한다. 

Builder.Default 값은 

Provider클래스의 List<Product>에 new ArrayList<>() 를 초기값을 추기 위해 붙여주는 어노테이션이다. 

 

fetch type이란

Jpa가 Entity를 조회할 때 연관관계에 있는 객체들을 전부 접근할지, 필요할때만 접근할지에 대한 설정이다. 

default는 최적화를 위해 Lazy이고 

연관 관계에 있는 Entity를 가져오지 않고, getter로 접근하는 경우만 가져온다. 

Eager는 항상 연관관계에 있는 Entity를 모두 가져온다. 

 

 

Product는 Provider를 

//n:1 Owner Table

@ManyToOne(cascade = CascadeType.PERSIST,fetch = FetchType.EAGER)//default : lazy

@JoinColumn(name="provider_id")

@ToString.Exclude

private Provider provider;

 

ManyToOne으로 매핑한다. 

 

 

Product product = Product.builder().name("name").price(10000).stock(100).build();

Product product2 = Product.builder().name("name2").price(20000).stock(200).build();

Product product3 = Product.builder().name("name3").price(30000).stock(300).build();

Provider provider = Provider.builder().name("name").build();

product.setProvider(provider);

product2.setProvider(provider);

product3.setProvider(provider);

List<Product> products = provider.getProducts();

products.add(product);

products.add(product2);

products.add(product3);

provider.setProducts(products);

 

providerRepository.save(provider);

product1,2,3, provider를 생성하고 

product들에 provider를 set하고 

Provider의 Products를 가져온다. 

Products는 초기값으로 new ArrayList<>()를 갖고 있다. 

Provider의 products에 product를 add 하고 setProducts 한 후 provider를 save한다. 

한 transaction 안에 (@Transactional)있다면 products.add(product1,2,3) 만 해주고 provider에 다시 setProducts 하지 않아도 context에서 확인하고 save 처리해준다. 

 

 

반대로 Product에서는 

Provider provider=Provider.builder()

.name("name")

.build();

 

Product product=Product.builder()

.name("name")

.price(10000)

.stock(100)

.build();

/***** 연관설정 Product-->Provider *****/

product.setProvider(provider);

productRepository.save(product);

product에  provider만 set해줘도 된다. 

 

 

category엔터티는 

@Column(unique = true,nullable = false)

private String code;

code 컬럼을 unique로 갖고 있다. 

@Column에 unique, nullable 속성을 추가해준다. 

 

@OneToMany(cascade = {CascadeType.ALL},orphanRemoval = true,mappedBy = "category",fetch = FetchType.EAGER)

@Builder.Default

private List<Product> products=new ArrayList<>();

 

orphanRemoval 속성은 자식 엔터티가 고아가 되면 삭제해주는 속성이다. 

이렇게 하면 

findCategory.getProducts().clear();

카테고리에서 product리스트를 지우면 카테고리에서 프로덕트를 갖지 않게되므로 쫓겨난 프로덕트는 고아가 된다. 

그럼 그 고아엔터티를 삭제해주어 product테이블에서도 삭제된다. 

 

'Java > Spring Boot' 카테고리의 다른 글

Thymeleaf Layout  (1) 2023.11.24
JPA 계층형 게시판  (0) 2023.10.10
Spring Data JPA begins  (0) 2023.10.06
Spring CRUD with RestAPI  (0) 2023.09.20
Spring addViewControllers  (0) 2023.09.14

+ Recent posts