Open Mvitimin opened 9 months ago
https://github.com/holyeye/jpabook
JPA 기본전략은 엔티티의 모든 필드를 업데이트 하게된다. 컬럼이 대략 30개이상이 된다면 기본 방법인 정적 수정 쿼리보다, @DynamicUpdate를 사용한 동적 쿼리수정이 훨씬 빠르다.
https://www.baeldung.com/spring-data-jpa-dynamicupdate
detach : 특정 엔티티만 준영속 상태 영속성 컨텍스트에게 해당 엔티티를 더이상 관리하지 말라는 뜻 변경감지, 데이터베이스 반영 모두 되지않음 지연로딩도 할 수 없게된다.
@Entity
@Table(name="MEMBER", uniqueConstraints = {@UniqueConstraint( //추가 //**
name = "NAME_AGE_UNIQUE",
columnNames = {"NAME", "AGE"} )})
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false, length = 10) //추가 //**
// @Column(name = "NAME") //추가 //**
private String username;
private Integer age;
//=== 추가
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
@Transient
private String temp;
//Getter, Setter
public String getId() {
return id;
}
기본키를 직접 할당하려면 @Id만 사용하면 됨. 자동 생성 전략을 사용하려면 @Id에 @GeneratedValue를 추가하고 원하는 키 생성 전략을 선택하면 된다.
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
JPA가 엔티티 데이터에 접근하는 방식을 지정
@Entity
@Access(AccessType.FIELD)
public class Member {
@Id
private String id;
}
@Id가 필드에있으면 굳이 Field 설정안해도됨
@Entity
@Access(AccessType.PROPERTY)
public class Member {
private String id;
@Id
public String getId(){
return id;
}
}
함께 사용도 가능하다.
@Entity
public class Member {
@Id
private String id;
@Trasient
private Stirng firstName;
@Trasient
private Stirng lastName;
private String fullName;
@Access(AccessType.PROPERTY)
public String getFullName() {
return firstName + lastName;
}
}
연관관계 중에선 다대일 (N:1) 단방향 관계를 가장 먼저 이해해야 한다. Order: N , Member: 1
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; //주문 회원
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<Order>();
@Entity
@Table(name = "ORDERS")
public class Order extends BaseEntity {
@Id
@GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member; //주문 회원
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
@OneToOne
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery; //배송정보
@Entity
public class Delivery {
@Id @GeneratedValue
@Column(name = "DELIVERY_ID")
private Long id;
@OneToOne(mappedBy = "delivery")
private Order order;
private String city;
private String street;
private String zipcode;
@Entity
public class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name; //이름
private int price; //가격
private int stockQuantity; //재고수량
@ManyToMany(mappedBy = "items") //**
private List<Category> categories = new ArrayList<Category>(); //**
@Entity
public class Category {
@Id @GeneratedValue
@Column(name = "CATEGORY_ID")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "CATEGORY_ITEM",
joinColumns = @JoinColumn(name = "CATEGORY_ID"),
inverseJoinColumns = @JoinColumn(name = "ITEM_ID"))
private List<Item> items = new ArrayList<Item>();
@Data
public class UserGroupMemberKey implements Serializable {
private String groupId;
private String userId;
}
@Data
@Entity
@NoArgsConstructor
@IdClass(UserGroupMemberKey.class)
public class UserGroupMember {
@Id
private String groupId;
@Id
private String userId;
}
@Entity
public class Category {
@Id @GeneratedValue
@Column(name = "CATEGORY_ID")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "CATEGORY_ITEM",
joinColumns = @JoinColumn(name = "CATEGORY_ID"),
inverseJoinColumns = @JoinColumn(name = "ITEM_ID"))
private List<Item> items = new ArrayList<Item>();
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<Category>();
@Entity
public class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name; //이름
private int price; //가격
private int stockQuantity; //재고수량
@ManyToMany(mappedBy = "items") //**
private List<Category> categories = new ArrayList<Category>(); //**
@Entity
public class Delivery {
@Id @GeneratedValue
@Column(name = "DELIVERY_ID")
private Long id;
@OneToOne(mappedBy = "delivery")
private Order order;
private String city;
private String street;
private String zipcode;
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member; //주문 회원
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery; //배송정보
ManyToOne, OneToOne
ManyToMany, OneToMany
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "MEMBER_ID")
private Member member; //주문 회원
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<OrderItem>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery; //배송정보
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공 (ORPHAN) 부모 엔티티 컬렉션에서 자식 엔티티 참조만 제거하면 자식 엔티티가 자동으로 삭제 되게 함
parent1.getChildren().remove(0); // 자식엔티티를 컬렉션에서 제거
@OneToMany(mappedBy = "parents", orphanRemoval = true)
DELETE SQL이 실행되게 됨
@OneToMany 안에서만 사용할 수 있으며
부모를 제거하면 자식도 같이 제거되기 때문에 CascadeType.REMOVE를 설정한것과 같다.
https://docs.spring.io/spring-data/jpa/docs/1.8.0.RELEASE/reference/html/
Keyword | Sample | JPQL snippet |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = 1? |
Between | findByStartDateBetween | … where x.startDate between 1? and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection |
… where x.age in ?1 |
NotIn | findByAgeNotIn(Collection |
… where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
쿼리에 이름을 부여해 사용하는 방법
https://www.baeldung.com/hibernate-named-query
@Query(
value = "SELECT * FROM USERS u WHERE u.status = 1",
nativeQuery = true)
Collection<User> findAllActiveUsersNative();
https://www.baeldung.com/hibernate-named-query
@Query("SELECT u FROM User u WHERE u.status = :status and u.name = :name")
User findUserByStatusAndNameNamedParams(
@Param("status") Integer status,
@Param("name") String name);
https://www.baeldung.com/rest-api-search-language-spring-data-specifications
JPA 리포지터리와 모델 구현
github
https://github.com/madvirus/ddd-start2
스프링 데이터 JPA 는 OrderRepository를 리포지터리로 인식해서 알맞게 구현한 객체를 스프링 빈으로 등록한다.
엔티티와 밸류기본매핑구현
애그리거트와 JPA 매핑을 위한 기본 규칙은 다음과 같다. 애그리거트 루트는 엔티티이므로 @Entity 로 설정한다
한테이블에 엔티티와 밸류 데이터가 같이 있다면 밸류는 @Embeddable 로 매핑 설정한다. 밸류 타입 프로퍼티는 @Embedded로 매핑 설정한다.
Order에 속하는 Orderer는 밸류이므로 @Embedabble로 매핑한다.
Orderer의 memberId는 Member 애그리거트를 ID로 참조한다. Member의 ID 타입으로 사용되는 MemberId는 다음과 같이 id 프로퍼티와 매핑되는 테이블 컬럼 이름으로 member_id를 지정하고있다.
루트 엔티티인 Order클래스는 @Embedded를 이용해서 밸류 타입 프로퍼티를 설정한다.
기본생성자
엔티티와 밸류의 생성자는 객체를 생성할 때 필요한 것을 전달받는다. 예를 들어 Receiver 밸류 타입은 생성 시점에 수취인 이름과 연락처를 생성자 파라미터로 전달받는다.
Receiver가 불변타입이면 생성 시점에 필요한 값을 모두 전달 받으므로 값을 변경하는 set 메서드를 제공하지 않는다. 이는 Receiver 클래스에 기본 생성자를 추가할 필요가 없다는 것을 의미한다.
하지만 JPA에서 @Entity와 @Embeddable로 클래스를 매핑하려면 기본생성자를 제공해야한다. DB에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 사용해서 객체를 생성하기 때문이다.
이런 기술적제약으로 인해 Receiver와 같은 불변타입은 기본 생성자가 필요없음에도 불구하고 기본생성자를 추가해야한다.
필드 접근방식
JPA는 필드와 메서드 두 가지 방식으로 매핑을 처리 할 수 있다. 메서드 방식을 사용하려면 get,set을 구현해야하지만 필드 접근은 안해도됨.
AttributeConverter 를 이용한 밸류 매핑 처리
int,long,String,LocaleDate 와 같은 타입은 DB xpdlqmfdml gksro zjffjadp aovld
-----> (1000mm저장) WIDTH VARCHAR(200)
convertToDatabaseColumn() => 밸류타입을 DB컬럼값으로 변환하는 기능 convertToEntityAttribute() => DB컬럼값을 밸류로 변환하는 기능
밸류컬렉션: 한개 컬럼 매핑
밸류 컬렉션을 별도 테이블이 아닌 한개 컬럼에 저장해야할 때가 있다. 예를 들어 도메인 모델에는 이메일 주소 목록을 Set으로 보관하고 DB 에는 한개 컬럼에 콤마로 구분해서 저장해야할 때가있다. 이때 AttributeConverter를 사용하면 밸류컬렉션을 한 개 컬럼에 쉽게 매핑할 수 있다.
밸류를 이용한 ID 매핑
OderNo, MemberId등이 식별자를 표현하기 위해 사용한 밸류 타입이다. 밸류 타입을 식별자로 매핑하면 @Id 대신 @EmbeddedId 어노테이션을 사용한다.
별도 테이블에 저장하는 밸류 매핑
밸류인지 엔티티인지 구분하는 방법은 고유 식별자를 갖는지 확인하는 것이다. 하지만 식별자를 찾을 때 매핑되는 테이블의 식별자를 애그리거트 구성요소의 식별자와 동일한 것으로 착각하면 안된다.
Artcle, ArticleContent가 ID로 서로 매핑되는 테이블이 있다하더라도 ArticleContent가 식별자를 가진다고해서 엔티티는 아니다. 이 식별자가 ArticleContent의 고유의 id가 아니라 매핑용 id기 때문이다. 그러므로 ArticleContent는 밸류이다.
@SecondaryTable 의 name 속성은 밸류를 저장할 테이블을 지정한다. pkJoinColumns 속성은 밸류 테이블에서 엔티티 테이블로 조인할 때 사용할 컬럼을 지정한다.
밸류 컬렉션을 @Entity로 매핑하기.
product 라는 테이블과 image 라는 테이블이있다.
image, product 모두 각자 개별 식별자 고유 id가 있으나 image 테이블이 product 테이블 id를 매핑하려고함. 또한 Image 클래스는 상위 클래스, InternalImage, ExternalImage 두개의 클래스가 상속받음
그러면 Image 테이블에
1.@Inhritance 애너테이션을 적용 2.strategy 값으로 SINGLE_TABLE 사용 3.@DiscriminatorColumn 어노테이션을 이용하여 타입 구분용으로 사용할 컬럼 지정
Image는 밸류이므로 독자적인 라이프 사이클을 갖지 않고 Product에 의존한다. Product를 저장할 때 함께 저장되고 PRoduct를 삭제할때 함께 삭제되도록 cascade속성지정한다. 리스트에서 Image 객체를 제거하면 DB에서 함께 삭제되도록 orphanRemoval 도 true로 설정한다.
clear 메소드를 실행하게되면 select * from image where product_id = ? 쿼리와 각 image를 삭제하기 위한 네번의 delete from image where image_id = ? 쿼리를 실행한다. 변경 빈도가 낮으면 괜찮지만 빈도가 높으면 전체 서비스 성능에 문제가 될 수 있다.
하이버네이트는 @Embeddable 타입에 대한 컬렉션의 clear() 메서드를 호출하면 컬렉션에 속한 객체를 로딩하지 않고 한 번의 delete 쿼리로 삭제처리를 수행하게 된다. @Embeddable 로 매핑된 단일 클래스로 구현해야한다.
if else 를 써야함
참조와 조인 테이블을 이용한 단방향 M-N매핑
M-N연관은 밸류 컬렉션 매핑과 동일한 방식이긴 하지만 차이점이 있다면 집합 값에 밸류대신 연관을 맺는 식별자가 온다는 점. product를 삭제하면 사용한 조인 테이블 데이터도 삭제된다.
애그리거트 영속성 전파
@Embeddable 매핑 타입은 함께 저장되고 삭제되므로 cascade 속성을 추가로 설정하지 않아도된다. 반면에 애그리거트에 속한 @Entity 타입에 대한 매핑은 cascade속성을 사용해서 삭제시에 함께 처리되도록 해야한다.
식별자 생성기능