JPA가 나타나게 된 배경
: 객체와 데이터베이스 간 실질적인 매핑작업을 개발자가 일일이 수행해줘야했고, 수많은 반복적인 CRUD 동작을 위한 SQL을 작성하는데 소요되는 비용과 시간을 줄이고, 개발자는 SQL에 의존적인 개발을 하지 않기 위해 만들어졌다. 즉 JPA가 SQL을 런타임시간에 자동으로 생성해줌으로서 개발자는 엔티티(비지니스 요구사항을 모텔링한 객체)를 신뢰하며 프로그래밍하는데 시간을 더 사용할 수 있는 장점을 가진다.
JPA를 사용하면 객체를 데이터베이스에 저장하고 관리할 때, 개발자가 직접 SQL을 작성하는 것이 아니라 JPA가 제공하는 API를 활용하여 자동으로 생성된 SQL이 데이터베이스에 전달이 된다. JPA가 제공하는 CRUD API로는 다음과 같다.
- 저장 기능 : jpa.persist(member); //저장
객체와 매핑정보를 확인하고 적절한 INSERT SQL을 생성
- 조회 기능: Member member = jpa.find(Member.class, memberId); //조회
SELECT SQL을 생성하여 리턴 결과를 Member 객체에 반환
- 수정 기능: Member.setName("이름변경") //수정
별도의 수정 method는 존재하지 않지만, find method를 통해 얻은 객체의 값을 변경하여 트랜잭션을 커밋할 때 적절한 UPDATE SQL이 전달된다.
- 연관된 객체 조회 : Team team = member.getTeam();
상속관계로 이루어진 객체들간의 관계도 JPA가 적절한 SQL문을 생성해준다는 큰 장점이 있다. 데이터베이스엔 상속개념은 존재하지 않는 것으로 알고 있기 때문에 해당 JPA의 기능은 유용하다고 생각이 든다.
데이터를 조회할 때 즉시 로딩과 지연 로딩 두가지 방식이 있다고 한다. 즉시 로딩은 데이터를 조회할 때 연관된데이터까지 한번에 불러오고, 지연 로딩은 필요한 시점에 연관된 데이터를 불러오는 것을 뜻한다.
테이블 A가 테이블 B와 연관되어있다고 할 때, 즉시 로딩은 A를 조회하는 시점에 B까지 불러오는 Query를 통해 관련 데이터를 모두 불러온다. 이에 반해, 지연 로딩은 A를 조회하는 시점에는 A를 조회하는 Query만 날아가고, 나중에 B를 사용하는 시점에 B를 조회하는 Query가 날아간다. 즉시 로딩방식보다는 지연 로딩 방식을 권하고 이를 사용해야한다고 한다. 불필요한 데이터까지 대량으로 가져오면서 생기는 불필요하게 소요되는 시간을 발생하기 때문이라고 이해했다. 다대일 관계에서 자칫 잘못하여 연관되어있는 데이터를 전부 끌고올 경우 결과는 상상하고 싶지 않다..
객체를 비교하는 방식에는 동일성 비교와 동등성 비교가 있다. 동일성 비교는 == 비교라고도 하는데, 객체 인스턴스의 주소값을 비교한다. 내부 값을 비교하는게 아닌 것이다. 이에 반해 동등성 비교는 equals() method를 활용하며 객체 내부의 값을 비교한다. Query문으로 데이터베이스에서 같은 row의 데이터를 두번 조회했다. 이렇게 꺼내어 Member 객체에 들어간 두 객체 member1, member2는 동일성 비교를 하면 false이다! 값은 동일할지 몰라도 객체 관점에선 개별적인 두 객체로 존재하기 때문이다. 하지만 JPA를 활용한 비교는 이야기가 다르다.
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2 //true
JPA는 같은 트랜잭션일 때 같은 객체가 조회되는 것을 보장한다.
JPA를 활용하면 객체 모델과 관계형 데이터베이스 모델 간 매핑작업을 대신해주고 개발자의 시간과 코드를 절약해주는 장점이 있고 정교한 객체 모델링 작업을 하더라도 데이터베이스간의 패러다임 불일치 문제를 해결해준다!
JPA는 Java Persistence API의 약자이고 ORM 기술표준이다. 스프링부트와 같은 JAVA Application에서 활용가능하고 JPA 내부에는 JDBC API가 있고 여기서 DB와 쿼리문과 결과가 오고가는 방식이다. 즉 ORM프레임워크 기반의 JPA를 사용하면 객체를 자바 컬렉션에 저장하듯이 ORM 프레임워크에 저장하고 적절한 쿼리문을 생성하여 DB에 객체를 저장해주는 것이다.
제일 처음 ORM 기술로 탄생한 것은 엔터프라이즈 자바 빈즈(EJB)였고, 사용법이 복잡하고 제한된 환경에서만 활용이 가능했다. 이후 탄생한 것이 하이버네이트(hibernate)라는 오픈소스 ORM 프레임워크이다. EJB에 비해 가볍고 실용적이었기 때문에 많은 개발자들이 사용하기 시작했다. 최종적으로 EJB 3.0에서는 하이버네이트를 기반으로 새로운 자바 ORM 기술표준이 만들어졌고 이것이 JPA이다.
JPA는 자바 ORM 기술에 대한 API 표준명세이다. 즉 각종 인터페이스들을 모아놓은 것이다.
JPA를 사용해야하는 이유
1. 생산성 : JPA를 활용하면 컬렉션에 객체를 저장하듯 JPA에게 저장할 객체를 전달하면 된다. 반복적인 CRUD SQL들을 일일이 개발자가 작성할 필요가 없어졌고 시간적으로 많은 이점을 부여해준다.
2. 유지보수 : 엔티티에 필드 하나를 추가하더라도 관련된 등록,수정,삭제,조회 등을 위한 JDBC API코드를 전부 변경해야했지만 JPA를 활용하면 수정해야할 코드가 줄어든다. 즉 유지보수해야하는 코드 수가 줄어든 것이다.
3. 성능 : JPA를 활용하면 같은 트랜잭션 안에서 한 번만 데이터베이스에 SELECT SQL이 전달되고, 두 번째는 조회한 회원 객체를 재사용한다.
String memberId = "hello";
Member member1 = jpa.find(memberId);
Member member2 = jpa.find(memberId);
객체 매핑
@Entity
// 해당 class를 테이블과 매핑한다고 JPA에게 알려준다. 해당 어노테이션이 붙은 Class는 엔티티 클래스라고 한다.
@Table
// 엔티티 클래스에 매핑할 테이블 정보를 알려준다. 보통 name 속성을 사용해 @Table(name = "MEMBER")로 Member 엔티티를 MEMBER테이블과 매핑한다.
// 해당 어노테이션을 생략하면 클래스 이름을 테이블 이름으로 매핑한다.
@Id
// 엔티티 클래스의 필드를 테이블의 기본키에 매핑한다. 해당 어노테이션이 사용된 필드를 식별자 필드라고 한다.
@Column
// 필드를 테이블의 컬럼에 매핑한다. 보통 name 속성을 사용해서 @Column(name="NAME")처럼 MEMBER 테이블의 NAME 컬럼에 매핑한다.
매핑정보가 없는 필드값들도 존재하는데, 매핑 어노테이션을 생략하면 필드명을 사용해 컬럼명으로 매핑한다. 데이터베이스가 대소문자를 구분하는지 않는다고 가정하고 있다. 하지만 실제론 이 부분은 인텔리제이에서 application.yaml이나 properties에서 추가적으로 대분자로 매핑시키도록 건드려야하는 부분이 있는 걸로 알고있다.
엔티티 매니저 설정
JPA를 시작하려면 persistence.xml의 설정 정보를 사용해서 엔티티 매니저 팩토리를 생성해야한다. persistence.xml은 JPA에 관한 정보만 담아있는 파일이라고 생각해야겠다. 스프링부트가 사용하는 설정 파일인 application.yaml과 구분지어 바라볼 필요가 있다. Persistence Class를 사용하는데 해당 클래스는 엔티티 매니저 팩토리를 생성해서 JPA를 사용할 수 있게 준비한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook"); //엔티티매니저팩토리 생성
이름이 jpabook인 영속성 유닛을 찾아 엔티티 매니저 팩토리를 생성한다. JPA를 동작시키기 위한 기반 객체를 생성하고, 데이터베이스 커넥션 풀도 생성하기 때문에 비용이 매우 크다. 따라서 EMF는 애플리케이션 전체에서 한번만 생성하고 공유하는 것이 효율적이다.
JPA의 기능 대부분은 엔티티 매니저가 제공한다. 데이터베이스에 CRUD 기능은 엔티티 매니저를 사용한다고 보면 된다. 주의할 점으로 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드간 엔티티매니저를 공유하면 동시성 문제가 발생하기 때문에 스레드간 엔티티매니저를 구분짓는 것이 좋아보인다. 이후에 사용이 끝난 EM과 EMF는 반드시 종료해야한다.
EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성
em.close(); //EM 종료
emf.close(); //EMF 종료
JPA를 사용하면 항상 트랜잭션 안에서 데이터를 변경해야한다. 엔티티 매니저에서 트랜잭션 API를 받아와서 데이터를 변경하거나 예외처리를 한다.
EntityTransaction tx = em.getTransaction(); //트랜잭션 API
try {
tx.begin(); //트랜잭션 시작
logic(em); //비지니스 로직 실행
tx.commit() //트랜잭션 커밋
} catach (Exception e){
tx.roolback();
}
String id = "id1";
Member member = new Member();
member.setId(id);
member.setUsername("현규");
member.setAge(23);
em.persist(member);
member.setAge(24);
Member findMember = em.find(Member.class, id);
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
em.remove(member);
엔티티를 저장하려면 엔티티 매니저의 persist() 메소드에 저장할 엔티티를 인자로 넘겨주면 된다. JPA가 엔티티의 매핑정보를 분석해서 "INSERT INTO MEMBER (ID, NAME, AGE) VALUES ('id1','현규',23); 의 SQL을 만들어 DB에 전달한다.
여기서 주의할 점은 UPDATE할 때이다. age 칼럼을 수정했는데, em.update()와 같은 메소드 없이 member 객체 내의 값만 바꿨다. 이렇게만 해도 JPA가 어던 엔티티가 변경되었는지 추정해서 알아서 UPDATE SQL을 생성해준다고 한다.
한 건 조회할 때는 find메소드를 활용한다. 인자로, 조회하려는 엔티티 클래스와 기본키를 적어준다. 이렇게 조회를 하면 SELECT * FROM MEMBER WHERE ID = 'id1'; 의 SQL을 생성하여 DB에 전달한다.
하나 이상의 회원 목록을 조회할 때는 JPQL 이라는 SQL을 추상화한 객체지향언어를 사용하는 것이 유용하다. JPA는 엔티티 객체를 중심으로 개발하는데 JPQL을 사용하지 않으면 검색하고자 하는 데이터베이스의 레코드들을 전부 불러와서 엔티티 객체로 변경한다음 검색을 해야한다. 비효율적인 과정이 진행되는 것을 확인할 수 있다. 따라서 검색조건이 포함되는 SQL인 JPQL을 활용하는 것이다. SQL문법과 유사하게 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등 활용이 가능하다. SQL과 JPQL의 차이점은 엔티티객체를 대상으로 하느냐 데이터베이스 테이블을 대상으로 하느냐의 차이이다.
//목록조회
TypeQuery<Member> query = em.createQuery("select m from Member m",Member.class)
List<Member> members = query.getResultList();
쿼리객체를 생성한 이후 getResultList()메소드를 호출하여 사용한다.
데이터베이스를 하나만 사용하는 애플리케이션은 EntityManagerFactory를 하나만 생성하면 된다. 생성비용이 매우 크기 때문에 그리고 여러 스레드가 동시에 접근해도 안전하기 때문이다.
//엔티티매니저팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
//엔티티매니저 생성
EntityManager em = emf.createEntityManager();
엔티티매니저는 생성비용이 매우 낮기 때문에 여러 개를 생성해도 된다. 다만 여러 스레드가 동시접근하면 동시성 문제가 발생하기 때문에 스레드간 공유는 하면 안된다.
persistence context라는 용어가 있다. 쉽게 말하면 엔티티를 영구 저장하는 환경이라는 뜻으로 저장하거나 조회할 때, 엔티티 매니저가 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
em.persist(member);
persist 메소드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장하는 과정을 진행한다. 영속성 컨텍스트는 엔티티 매니저 생성과 동시에 만들어지고 엔티티 매니저만이 영속성 컨텍스트에 접근하고 관리한다.
영속성 컨텍스트 특징 : 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩
- 1차 캐시 : 영속성 컨텍스트 내부에는 캐시가 존재하고 이를 1차 캐시라고 부른다. 영속상태의 엔티티는 모두 여기에 저장된다. (map 자료구조 내부에 @Id로 매핑한 식별자가 키이고, 엔티티 인스턴스가 값인 것과 유사)
//엔티티를 생성한 상태
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//엔티티가 영속된 상태
em.persist(member);
1차 캐시의 키는 식별자 값 즉 데이터베이스의 primary key와 매핑되어있다.
Member member = em.find(Member.class, "member1");
em.find()를 호출하면 먼저 1차 캐시에서 엔티티를 찾는다. 만약 식별자값으로 1차 캐시에서 조회했는데 없으면 db를 조회한다. 이후 1차 캐시에 저장하고 영속 상태의 엔티티를 반환한다. 성능상 이점을 누릴 수 있다는 장점이 있다.
더하여 member1의 식별값을 가지고 member 객체를 두번 찾는다면 이 두개의 객체는 동일하다. 1차 캐시에서 같은 객체를 두 번 가져온거라서 동일성을 보장한다.
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경 시, 트랜잭션을 시작해야한다.
transaction.begin();
em.persist(memberA);
em.persist(memberB);
//커밋하는 순간 db에 insert sql이 날아간다.
transaction.commit();
엔티티 매니저는 트랜잭션을 커밋하기 직전까지 내부 쿼리저장소에 INSERT SQL를 모아두고 커밋할 때, 쿼리를 데이터베이스에 보낸다. 이를 쓰기 지연(transactional write-behind)라고 한다.
flush 작업은 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 과정이다.
엔티티의 변경 사항을 데이터베이스에 자동으로 반영하는 기능인 dirty checking(변경 감지)이 존재한다. 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에게만 적용이 되며 엔티티를 영속성 컨텍스트에 보관할 때의 상태를 복사해서 스냅샷 찍듯이 저장해둔다. 플러시 시점에 엔티티와 스냅샷을 비교하여 변경사항이 있으면 수정 쿼리를 생성하여 나중에 커밋과 동시에 영속성 컨텍스트가 생성했던 sql들이 db로 전송된다.
엔티티 삭제 과정도 앞서 등록과 유사하게 진행된다. 조회를 한 다음에 em.remove(member)를 통해서 해당 member 객체를 영속성 컨텍스트에서 제거한다. 이후 트랜잭션을 커밋하여 플러시를 호출한다면 데이터베이스에 삭제 쿼리가 전달된다.
Flush(플러시)는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
- 더티체킹이 동작하여 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교하여 수정된 엔티티를 찾는다
- 수정된 엔티티는 수정 쿼리를 만들어 쓰기지연 SQL저장소에 등록해둔 후 이후에 DB에 쿼리를 전달한다
플러시 발동 조건 3가지
- 직접호출 : em.flush(), 엔티티메니저의 flush()메소드를 직법 호출. 주로 테스트할 때 사용한다
- 트랜잭션 커밋 시 자동호출 : 트랜잭션을 커밋하면 동작 직전에 플러시가 자동으로 호출이 된다. 스냅샷과 비교한 이후에 변경사항을 커밋해야하니 플러시가 먼저 동작한다.
- JPQL 쿼리 실행시 자동 호출
매핑 어노테이션
1. @Entity
@Entity가 붙은 클래스는 JPA가 관리하는 대상이다. JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하므로 파라미터가 없는 public 또는 protected 생성자가 필수이다.
public Member() {}
//기본 생성자
2. @Table : 엔티티와 매핑할 테이블을 지정한다.
3. @Id : 기본 키 매핑
4. @Column : 필드 와 컬럼 매핑
5. @ManyToOne, @JoinColumn : 연관관계 매핑
데이터베이스 스키마 자동 생성
JPA는 데이터 베이스 스키마를 자동으로 생성하는 기능을 지원한다. xml 혹은 properties 파일을 통해 ddl create 기능을 추가하면 애플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성한다.
hibernate.hbm2ddl.auto 속성
- create : 기존 테이블을 삭제하고 새로 생성(DROP + CREATE)
- create-drop : create 속성에 추가로 애플리케이션을 종료할 때 생성한 DDL을 제거한다. (DROP + CREATE + DROP)
- update : 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 변경 사항만 수정한다.
- validate : 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다.
@Entity
@Table(name="MEMBER")
public class Member{
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME", nullable = false, length = 10)
private String username;
자동생성되는 DDL은 다음과 같이 된다.
create table MEMBER(
ID varchar(255) not null,
NAME varchar(10) not null,
---
primary key (ID)
)
기본키를 직접 할당하는 것 대신 기본키 생성을 데이터베이스에 위임하는 전략인 IDENTITY 방법이 있다.
@Entity
public class Board{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
---
}
@GeneratedValue 어노테이션을 통해서 식별자 자동 생성 방법을 사용한다. AUTO_INCREMENT 기능을 수행하는 것과 마찬가지로 사용자의 개입 없이 기본키가 자동으로 배정이 되는 방식으로 이해하면 될 것 같다.
다른 전략으로 SEQUENCE 전략, TABLE 전략이 있지만 기본값으로 AUTO가 있다. 데이터베이스 dialect에 따라서 전략 중 자동으로 선택한다.
@Entity
public class Board{
@Id @GeneratedValue
private Long id;
---
}
필드 값을 데이터베이스 테이블의 컬럼에 매핑시킬 때 활용하는 어노테이션으로 @Column이 있다.
@Column(name = "AGE", nullable = false)
private Integer age;
필드를 선언할 때 타입으로 Integer 을 자주 활용한다. integer와 Integer는 다르다. integer 타입은 변수의 타입을 말하고 원시적인 자료형이다. 그에 반면 Integer를 활용하면 wrapper class로 하나의 객체를 만드는 것이다. 그래서 nullable이 true로도 작성이 가능하다. integer타입으로 선언한다면 null값이 가능하지 않을 것이다. 그래서 보통 엔티티 클래스에 활용되는 필드값들은 wrapper class로 선언되는 경우가 많은 것이다.
자주 활용하는 클래스가 enum class이다. 회원 등급, 회원 타입 같이 구분지어야할 경우 자주 작성한다. enum class를 작성한 이후에 엔티티 클래스 내에서 enum class를 활용하기 위해서는 @Enumerated 어노테이션을 활용한다.
@Enumerated(EnumType.STRING)
private RoleType roleType;
STRING 객체로 enum class에서 적었던 변수명 그대로 문자열 타입으로 칼럼에 저장된다. ADMIN은 'ADMIN'으로 저장이 된다.
@Enumerated(EnumType.ORDINAL)
private RoleType roleType;
EnumType.ORDINAL을 작성하면 enum class에서 정의한 순서대로 정수값이 들어간다. 0,1,2... 장점으론 db에 저장되는 데이터 크기가 작지만 큰 단점으로 이미 저장된 enum의 순서를 변경할 수 없다. 그래서 종종 STRING을 활용한다. 일종의 트레이드오프인 것이다.
JPA가 엔티티 데이터에 접근하는 방식을 지정하는 @Access 어노테이션이 있다. 보통은 @Id 어노테이션을 활용하기 때문에 @Access는 생략하더라도 FIELD에 접근이 가능하다.
@Access(AccessType.FIELD)
@Access(AccessType.PROPERTY)
필드에 직접 접근할 때는 FIELD를 활용한다. 필드 접근 권한이 private이어도 접근이 가능하다고 한다.
데이터베이스의 테이블 간 연관관계는 외래키를 바탕으로 맺는다. 그에 반면 객체 간에는 참조를 바탕으로 연관관계를 맺는다. 단방향 일대다 관계가 있다고 했을 땐, '다'쪽에서 '일'의 외래키를 가진다. 예를 들면 선수라는 player 객체와 team 객체가 있을 때, 여러 명의 선수가 팀에 속할 수 있다고 했을 때, 선수 객체에는 team_id라는 외래키를 가지고 있어야한다.
- @JoinColumn : 외래키를 매핑할 때 사용.
# 선수 객체 필드값
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
# 팀 객체 필드값
@OneToMany
private List<Member> members;
그래도 단방향보다는 양방향 매핑을 더 활용하지 않을까라는 생각도 든다. 팀에 속한 선수들의 목록들을 확인할 수 있게 하려면 일대다 양방향 매핑이 필요하다. 데이터베이스 테이블은 외래키 하나로 양방향으로 조회가 가능하다. 이에 반면 객체들은 join 기능이 없기 때문에 양쪽에서 단방향으로 쏴줘야만 양방향 매핑이 이루어질 수 있다.
# 선수 엔티티
@Entity
public class Player{
@Id
@Column(name = "PLAYER_ID")
private String id;
private String playerName;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
# 팀 엔티티
@Entity
public class Team{
@Id
@Column(name = "TEAM_ID")
private String id;
private String teamName;
@OneToMany(mappedBy = "team")
private List<Player> players = new ArrayList<Member>();
}
- mappedBy = ""에 적히는 team는 반대쪽 매핑의 필드값 이름을 적어주면 된다. 이는 연관관계 주인 관련해서 더 설명하고자 한다. 테이블은 외래키 하나로 두 테이블 간의 연관관계를 관리하고 양방향 매핑이 가능해진다. 그에 반면 객체에는 양방향 연관관계라는 것은 없고 서로에게 단방향 매핑을 쏴서 양방향 처럼 관리를 하는 것이 전부이다. 여기서 쟁점이 하나 발생한다. 객체의 참조는 둘인데 외래키는 하나만 존재하게 된다. 양쪽에서 반대편의 값에 수정을 할 수 있게 된다면 관리하기가 복잡해져서 연관관계의 주인 이라는 키워드가 나타나게 되었다. 연관관계의 주인만 외래키를 관리할 수 있고, 주인이 아닌 엔티티는 읽기만 가능해진다. 즉, 위 코드에서 작성한 mappedBy 속성은 주인이 아닌 엔티티에 작성이 된다. 결론은 연관관계의 주인은 테이블에 외래키가 있는곳으로 정하면 된다.
상속 관계 매핑
관계형 데이터베이스에는 상속이라는 개념이 존재하진 않지만, 슈퍼-서브타입 관계 모델링 기법을 활용해 상속 구조와 유사하게 만들 순 있다.
- 조인 전략 : 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본키를 받아서 기본키+외래키로 사용하는 전략 => 조회할 때 조인을 자주 사용한다.
// 부모 인터페이스 클래스
@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;
}
// 자식 클래스
@Entity
@DiscriminatorValue("A")
public class Album extends Item{
private String artist;
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
private String director;
private String actor;
}
- @Inheritance(strategy = InheritanceType.JOINED) : 상속 매핑은 부모 클래스에 해당 어노테이션을 붙여준다. 매핑전략은 조인이므로 JOINED을 작성한다
- @DiscriminatorColumn(name = "DTYPE") : 부모 클래스에 구분 칼럼을 지정한다. 이 컬럼으로 자식 테이블을 구분한다. 디폴트값이 DTYPE이므로 생략해도 무관하다.
- @DiscriminatorValue("M") : 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
장점 : 테이블 정규화, 외래키 참조 무결성 제약조건 활용 가능, 저장 공간 효율적
단점 : 조인 쿼리가 많이 발생하여 성능 저하, 데이터 등록시 INSERT 쿼리 두번 발생!
- 단일테이블 전략 : 테이블을 여러 개 두지 않고 하나만 사용한다. 구분컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 확인하는 전략
한가지 주의할 점으로 어느 타입의 정보가 들어올 지 모르므로, 자식 엔티티가 매핑 될 칼럼들은 모두 nullable 해야한다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
//자식 클래스
@Entity
@DiscriminatorValue("A")
public class Album extends Item {}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {}
장점 : 조인이 필요없으므로 조회 성능 빠르다.
단점 : 자식 엔티티가 매핑될 칼럼들은 nullable 해야한다. 단일테이블이므로 테이블이 커질 수도 있고 이로 인해 성능 저하 가능성 있음. 구분 컬럼을 꼭 써야한다.
- 구현 클래스 마다 테이블 전략 : 자식 엔티티마다 테이블을 만들고, 자식 테이블 각각에 필요한 컬럼이 모두 있다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
//자식 클래스
@Entity
@DiscriminatorValue("A")
public class Album extends Item {}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {}
장점 : 서브 타입을 구분해서 처리할 때 효과적이다. not null 제약조건 사용 가능
단점 : UNION을 활용한 쿼리문이 필요하므로 조회성능이 떨어짐.
@MappedSuperClass : 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공
=> @Entity는 실제 테이블과 매핑되지만, @MappedSuperClass는 실제 테이블과 매핑되지 않는다.
@MappedSuperClass
public abstract class BaseEntity{
@Id @GeneratedValue
private Long id;
private String name;
}
@Entity
public class Member extends BaseEntity{
//ID 상속 , NAME 상속
private String email;
}
@Entity
public class Seller extends BaseEntity{
//ID 상속 , NAME 상속
private String shopName;
}
baseEntity에는 공통 매핑 정보만 정의. 즉 Member class와 Seller class에는 id와 name 필드를 상속 받아서 실제 테이블과 매핑시키고, baseEntity class는 테이블로 매핑시키지 않음.
@MappedSuperClass로 지정한 클래스는 엔티티가 아니므로 엔티티매니저를 활용할 수 없다! 더하여 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것이 좋다.
* 엔티티(@Entity)는 엔티티이거나 @MappedSuperClass로 지정한 클래스만 상속받을 수 있다.
객체지향쿼리언어
- JPQL
- 엔티티 객체를 조회하는 객체지향 쿼리언어
// Entity class
@Entity(name="Member")
public class Member {
@Column(name = "name")
private String username;
}
// JPQL 사용
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
- 엔티티 객체 필드명을 검색 조건으로 작성하여 활용한다는 특징이 있다. em.createQuery() 메소드에 실행할 JPQL과 반환할 엔티티 클래스 타입은 Member.class를 넘겨준다.
- Criteria
- JPQL을 생성하는 빌더 클래스, 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이 특징이다.
- JPQL에 오류가 있다면 런타임 시점에서 발생한다. 문자 기반의 쿼리 언어이기 때문이다. 하지만 Criteria쿼리는 문자가 아닌 코드로 JPQL을 작성한다. 따라서 컴파일 시점에 오류를 발견할 수 있다.
- 동적쿼리를 작성하기편하다.
- 하지만 Criteria는 잘 사용하지 않는다. 가진 장점이 많지만, 모든 장점을 상쇄할 정도로 복잡하고 장황하기 때문에 사용하기도 불편하고, 코드가 길어질 경우 빠르게 이해되지 않는다는 점이 존재한다.
- QueryDSL
- Criteria와 마찬가지로 코드 기반이지만, 단순하고 사용하기 쉽다.
JPQQuert query = new JPAQery(em);
QMember member = QMember.member;
List<Member> members = query.from(member).where(member.username.eq("kim")).list(member);
- Native SQL
- SQL을 직접 사용할 수 있는 기능도 지원한다. 특정 데이터베이스에 의존하는 기능을 사용해야할 때 활용
'Backend' 카테고리의 다른 글
| [SpringBoot] 게시판 만들기 (advanced type) (3) -카테고리별 게시판 조회하기 (1) | 2023.12.23 |
|---|---|
| [SpringBoot] 게시판 만들기 (advanced type) (2) - JWT 적용하기 (Springboot 3.x, Swagger 3.0) (4) | 2023.12.22 |
| [SpringBoot] 게시판 만들기 (advanced type) (0) | 2023.12.18 |
| [Spring Boot] 게시판 만들기(step1) (3) | 2023.12.18 |
| [Intelij springboot] 단축키 모음 (window 버전) (0) | 2023.06.16 |