공부기록/스프링

스프링 JPA(Java Persistence API) (1)

gyungmean 2023. 5. 24. 22:51

JPA란?

Java 객체를 데이터베이스를 통해 저장 및 관리함으로써 객체의 영속성을 보장하는데

여기서 영속성이란 무엇이냐면 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성을 의미한다

JPA는 Object-Relational Mapping(ORM, 객체-관계 맵핑)을 실행하기 위한 Java 표준 API로 어노테이션을 이용해 엔티티로 정의된 자바 객체와 데이터베이스 사이의 맵핑 방법을 설정한다.

설정된 맵핑에 따라 Java 객체를 DB에 저장, 수정, 삭제, 검색을 할 수 있는 인터페이스를 제공하는데 이를 EntityManager라고 한다.

 

구성요소

Entity : ORM을 통해 DB에 영속적으로 저장, 관리되는 객체

ORM설정 : Entity와 DB테이블 사이의 mapping 방법을 정의, 어노테이션이나 xml을 이용한다.

EntityManager : Entity에 대한 CRUD 기능을 실행한다.

JPQL(Java Persistence Query Language : SQL과 유사한 질의 언어

Criteri API : 자바 코드를 이용한 질의 생성 방법 제공

Entity

영속적인 데이터를 포함하는 POJO 객체식별자 속성(ID)와 저장 관리 될 속성들을 정의 / 비 영속적 속성도 포함 가능EntityManager에 의해 영속적으로 관리되는 단위 객체일반적인 도메인 클래스 정의하듯이 정의하면 된다

 

주요 어노테이션

-Entity 정의 : @Entity, @Embeddable, @Embedded

-Entity 식별자 속성 정의 : @Id, @IdClass , @EmbeddedId

-Entity와 테이블 간의 Mapping 방법 정의 : @Table, @SecondaryTable , @Column, @JoinColumn ,
@PrimaryKeyJoinColumn , @Transient, @Temporal, @Enumerated,
@GeneratedValue , @AttributeOverides , @Inheritance 등

-Entity들 간의 연관 관계 정의 : @OneToOne , @OneToMany , @ManyToMany , @JoinTable

EntityManager

Entity의 생명 주기를 관리한다.

트랜잭션 완료시 영속 객체와 테이블 간 동기화 실행

Entity 검색 : SQL 질의 생성 및 실행 / 질의 결과에 대해 영속 객체 생성

 

생명주기

New : 영속성을 갖지 않는 단순한 객체 상태

Managed : 영속 컨텍스트에 속하여 영속성을 갖도록 관리 되는 상태 / DB테이블 특정행에 대응하는 데이터를가짐

Detached : 영속 컨텍스트를 벗어나 영속성을 갖지 않는 상태 / 테이블에 대응하는 데이터는 맞으나 더 이상 db와 동기화 되지 않음

Persisted : 데이터가 테이블에 저장되어 있는 상태

Removed : 영속 컨텍스트에서 삭제된 상태 / 테이블에서 삭제됨

 

주요 메소드

-persist(Object entity)

  • entity를 데이터베이스에 저장하고 엔티티를 managed상태로 만든다. (INSERT에 해당)
  • 새로 생성된 entity의 값을 entity와 맵핑 되어 있는 테이블에 저장한다(하나의 entity가 다른 테이블에 나눠서 저장되기도 한다)
  • primary key 등 테이블의 제약조건을 만족해야함 -> INSERT 실패시 javax.persistence.PersistenceException 발생
  • 수행 후 entity는 persistence context에 포함되어 영속성을 가짐 "Managed"상태
  • 연관 관계가 있고 cascade 속성이 정의된 entity들도 함께 저장 (@OneToOne(cascade = CascadeType.PERSIST))

-<T> T merge(T entity)

  • merge된 entity를 반환한다.
  • detached 상태에 있는 entity는 merge()에 의해 managed상태가 된 후 DB에 반영됨
  • DB에 이미 존재하는 entity에 대해서만 수행 가능(존재하지 않는 entity에 실행시 IllegalArgumentException 발생
  • 연관 관계가 있는 entity 들에 대해 cascading merge 수행 가능

-remove(Object entity)

  • 해당 entity를 데이터베이스에서 삭제한다.
  • SQL DELETE문 생성 및 실행
  • managed 상태의 entity에 대해서만 실행 가능 detached entity에게 실행시 IllegalArgumentException 발생
  • 연관 관계가 있는 entity 들에 대해 cascading remove 수행 가능

-<T> T find(Class<T> entitiyClass, Objcet primaryKey)

  • entity 의 ID 값을 통해 entity를 검색한다.
  • ID가 복합 식별자일 경우 복합 키 클래스의 객체를 전달한다.(@IdClass나 @EmbeddedId를 이용해 정의했을 경우)
  • 검색 결과로 Entity 객체를 반환한다. 없을땐 null 반환
  • SQL SELECT 문을 생성 및 실행한것과 같다.
  • Persistence provider는 생성된 entity 객체들에 대해 caching 수행
  • Lazy fetch mode 로 지정된 entity field 에 대해서는 그것이 실제로 이용될 때 데이터베이스로부터 load 됨 @Basic(fetch=FetchType.LAZY)
  • 연관 관계가 있는 다른 entity들도 함께 검색되어 로드됨

-flush() : EntityManager의 영속 컨텍스트를 데이터베이스와 동기화한다.

-refresh(Object entity) : 데이터베이스로 부터 refresh

-Query createQuery (String jpqlString) : JPQL을 이용해 쿼리를 생성한다

-Query createNamedQuery (String name) 

-Query createNativeQuery (String sqlString)

 

ORM설정

Entity 설정

entity클래스를 작성할때는 인자가 없는 기본 생성자가 필요하다.

entity클래스와 내부 필드, 메소드는 final로 정의하지 않는 것이 좋다.

JPA provider에서 caching실행에 필요하므로 java.io.Serializable 인터페이스 구현이 필요하다.

@Entity
@Table(name="ITEM") // 대응되는 테이블 이름이 같으면 생략 가능
public class Item implements Serializable {
    @Id // 식별자 필드 지정 테이블의 PK 컬럼과 대응
    private String itemId;
    private String productid; // @Column 생략 시 테이블 내 동일한 이름의 컬럼에 대응
    private double listPrice;
    …
    @Column(name="supplier") // 테이블 내 대응되는 컬럼 이름 명시
    private int supplierId;
    @Transient
    // 데이터베이스에 저장되지 않는 비 영속적 필드를 나타냄
    private String attribute1; // 또는 transient private String attribute1;
    …
    public Item () { }// 기본 생성자 정의
}

@Id

테이블에서 primary key 컬럼에 대응된다.

복합식별자를 위해서는 별도의 class를 정의해야한다.

-public 기본 생성자 정의 / equals() 및 hashCode() 구현 / Serializable 인터페이스 구현

Field나 getter에 적용할 수 있다.

float, double 등 정밀도 제한이 있는 타입은 피해야한다.

 

@Column

name으로 column이름 지정

 

복합식별자 지정

@Embeddable, @EmbeddedId를 이용한 방법

@Embeddable
public class LineItemPK implements Serializable {
    private int orderId
    private int lineNumber
    ...
}
@Entity
public class
LineItem implements Serializable {
    @EmbeddedId
    private LineItemPK id
    ...
}

@Embeddable의 경우 독립적인 entity가 아니기 때문에 식별자를 가질 필요는 없다. 해당 객체를 포함하는 entity와 같은 테이블에 저장되는데 별도의 테이블에 저장하려면 @SecondaryTable을 사용한다.

 

@IdClass, @Id를 이용한 방법

public class LineItemPK implements Serializable {
    private int orderId
    private int lineNumber
    ...
}
@Entity
@IdClass(LineItemPK.class)
public class
LineItem implements Serializable {
    @Id private int orderId;
    @Id private int lineNumber;
    ...
}

 

@SecondaryTable

하나의 entity의 데이터들을 두 개의 테이블에 mapping할때 사용한다.

@Entity
@Table(name="USERS")
@SecondaryTable(name="USER_PICTURES",
	pkJoinColumns=@PrimaryKeyJoinColumn(
		name="U_ID", referencedColumnName="USER_ID"))
public class User implements Serializable {
    …
    @Column(name="PICTURE", table=" USER_PICTURES ") // 2 차 테이블명 지정
    @Lob @Basic(fetch=FetchType.LAZY)
    private byte[] picture;
    …
}

@SecondaryTable의 내용을 살펴보자면 USER_PICTURES라는 이름의 보조 테이블을 정의한다.

이 테이블은 U_ID컬럼을 기준으로 주 테이블(여기서는 USERS에 해당)의 USER_ID컬럼과 관련된 데이터들을 저장하게 된다.

name=현재 테이블의 pk컬럼명

referencedColumnName=참조하는 상대 테이블의 컬럼명

두개의 insert문이 실행된다

 

Relationship

@OneToOne

-단방향 1:1연관관계

 

@Entity
public class Account{
    @Id
    @Column(name="userid")
    private String username;
    private String password;
    private String email;
    …
    @OneToOne(cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn(name="userid")
    private Profile profile;
    …
}

@PrimaryKeyJoinColumn : PK를 통해 연관되는 식별 관계에 사용한다.

-name 속성 : entity와 mapping된 테이블의 pk컬럼명

-referencedColumnName속성 : 상대 entity와 연관된 테이블에 저장된 컬럼명(이름이 동일한 경우 생략가능)

 

@OneToOne의 cascade옵션은 주인 엔티티에 변경이 발생하면 연관된 엔티티는 어떻게 할것인지에 대한 옵션이다.

예시 코드의 CascadeType.ALL은 모든 CRUD가 함께 발생된다. 그외 옵션으로는

CascadeType.PERSIST: 저장 작업에 대해 전파
CascadeType.MERGE: 업데이트 작업에 대해 전파
CascadeType.REMOVE: 삭제 작업에 대해 전파
CascadeType.REFRESH: 새로고침 작업에 대해 전파
CascadeType.DETACH: 분리 작업에 대해 전파

@Entity
public class Profile{
    @Id
    private String userid;
    @Column(name="favcategory")
    private String favouriteCategoryId;
    @Column(name="langpref")
    private String languagePreference;
    ...
}

예시 코드를 살펴보면 Account와 Profile은 userid라는 pk를 통해 연관되고 있음을 확인 할 수 있다.

 

-양방향 1:1 연관관계

두 객체가 서로 접근 가능한 관계이다.

Account class는 위와 동일하다고 가정하고

@Entity
public class Profile {
    @Id
    private String userid;
    @Column(name="favcategory")
    private String favouriteCategoryId;
    ...
    @OneToOne(mappedBy="profile"
    optional="false");
    private Account user;
    ...
}

@OneToOne

-mappedBy옵션 : 상대 객체의 field 지정, relatonship의 소유권 부여

-optional 옵션 : 상대 객체가 반드시 존재해야 하는가 여부를 지정

 

@ManyToOne

N:1 연관관계

@Entity
public class Item {
    @Id
    private String itemId;
    ...
    @ManyToOne
    @JoinColumn(name="productid")
    private Product product;
    ...
}
@Entity
public class Product { 
    @Id
    private String productId;
    private String name;
    …
}

위의 코드에서 item이 many쪽이고 product가 one인쪽이된다.

@ManyToOne은 many쪽 엔티티에서 scalar-type 필드를 통해 하나의 엔티티를 참조한다.

양방향 관계에서는 many쪽 엔티티가 relationship owner이고 db테이블에서 foreign key를 사용하여 관계를 저장한다.

 

@JoinColumn에서 name속성은 상대 테이블을 참조하기 위한 컬럼명을 지정하는 것이고 referencedColumnName 속성을 이용해 참조되는 상대 테이블의 PK칼럼명을 지정할 수 있다.

 

@OneToMany

1:N 연관 관계

@Entity
public class Product {
    @Id
    private String productId;
    private String name;
    …
    @OneToMany
    @JoinColumn(name="productid")
    private List<Item> itemList;
}

one쪽 엔티티에서 여러개의 many쪽 엔티티 참조를 저장하기 위해 collection type의 필드를 사용한다.

Generic을 사용하지 않을 경우 targetEntity속성을 사용해서 지정해야함.

예를 들어 @OneToMany(targetEntity=Item.class)

 

양방향 관계인 경우에는 many쪽 엔티티 참조 필드에 @JoinColumn을 이용해야한다.

@OneToMany에 mappedBy속성을 이용한다.

@Entity
public class Item {
    @Id
    private String itemId;
    ...
    @ManyToOne
    @JoinColumn(name="productid")
    private Product product;
    ...
}
@Entity
public class Product { 
    @Id
    private String productId;
    private String name;
    …
    @OneToMany(mappedBy="product")
    private List<Item> itemList;
}

@ManyToMany

양쪽 객체가 모두 여러개의 상대 객체를 참조한다.

예를 들어 하나의 category가 여러 개의 product를 포함하고 하나의 product가 여러개의 category에 속할 수 있는 경우

@Entity
public class Category { // relationship owner
    @Id
    private Long categoryId;
    private String name;
    ...
    @ManyToMany
    @JoinTable(name="CATEGORIES_ITEMS",
    joinColumns=@JoinColumn(name="CI_CATEGORY_ID",
    		referencedColumnName="CATEGORY_ID"),
    inverseJoinColumns=@JoinColumn(name="CI_ITEM_ID",
    		referencedColumnName="PRODUCT_ID"))
    private Set<Item> items;
    ...
}

@JoinTable : 두 entity 사이의 관계를 저장하는 연관 테이블 지정/정의

-joinColumns 속성 : join 테이블과 relationship owner 간의 FK-PK 컬럼 지정

-inverseJoinColumns 속성 : join table과 relationship subordinate간의 FK-PK 컬럼 지정

 

여기서 CI_CATEGORY_ID와 CI_ITEM_ID가 Join 테이블의 컬럼이다.

@Entity
public class Product{
    @Id
    private Long productId;
    private String name;
    ...
    @ManyToMany(mappedBy="items")
    private Set<Category> categories;
    ...
}

 

Inheritance

@Inheritance 설정을 통해 3가지 mapping strategy 사용 가능

single-table strategy

상속 계층 상의 모든 entity 클래스들을 하나의 테이블로 mapping한다.

하나의 테이블에 모든 entity들의 데이터를 저장

Discriminator 컬럼을 사용하여 entity들을 구분한다.

@Entity
@Table(name="USERS")
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="USER_TYPE",
		discriminatorType=DiscriminatorType.STRING , length=1)
public class User { ... }
@Entity
@DiscriminatorValue(value="S")
public class Seller extends User { ... }
@Entity
@DiscriminatorValue(value="B")
public class Bidder extends User { ... }

Joined-tables strategy

상속 계층 상의 모든 entity 클래스에 대해 각각 별도의 테이블을 생성한다.

부모 엔티티와 자식 엔티티 사이에 one to one relationship을 정의한다. 자식 엔티티들의 PK는 부모 엔티티 테이블의 PK값을 참조한다. 즉 PK이면서 FK

부모 엔티티 테이블에 discriminator column을 생성한다.

@Entity
@Table(name="USERS")
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="USER_TYPE",
		discriminatorType=DiscriminatorType.STRING , length=1)
public class User { ... }

@Entity
@Table(name="SELLERS") //TABLE명 지정
@DiscriminatorValue(value="S")
@PrimaryKeyJoinColumn(name="USER_ID") //참조되는 부모 테이블의 PK
public class Seller extends User { ... }

@Entity
@Table(name="BIDDERS") //TABLE명 지정
@DiscriminatorValue(value="B")
@PrimaryKeyJoinColumn(name="USER_ID") //참조되는 부모 테이블의 PK
public class Bidder extends User { ... }

Table-per-class strategy

모든 entity클래스에 대해서 각각 테이블을 생성하고 데이터를 저장한다.

부모 클래스로부터 자식 클래스로 상속되는 데이터들은 자식 테이블에도 중복 저장된다.

테이블 사이에 명시적인 relationship을 정의하지는 않는다.

@Entity
@Table(name="USERS")
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class User { ... }

@Entity
@Table(name="SELLERS") //TABLE명 지정
public class Seller extends User { ... }

@Entity
@Table(name="BIDDERS") //TABLE명 지정
public class Bidder extends User { ... }