소개
코드 간편화 + 쿼리 X
관계형DB 중요 -> SQL 중심적인 개발 (객체 <-> SQL), 단순 SQL의 반복
객체와 DB 차이로 인한 SQL매핑작업이 힘듦 -> Collection에 넣는다고 생각하면 편하다(JPA)
객체 -> DB : Insert
객체 <- DB : Select
차이점 | 객체 | DB | 문제 |
---|---|---|---|
1 상속 | 상속 | 테이블 슈퍼타입/서브타입 | insert시 2번실행, select시 조인필요 |
2 연관관계 | 연관관계 | 외래키 | 객체를 가지면(객체지향) 객체는 한방향/ DB는 양방향 조회 가능, 키값만 가지면(유사 객체지향) select시 2번 조회하여 강제로 연관관계를 맺어줘야한다. |
3 데이터 타입 | |||
4 데이터 식별방법 | 같은 쿼리를 조회하여 객체를 생성해도, 주소값이 다르기 때문에 다른것으로 인식된다. | ||
5 객체 그래프 탐색 | 객체에선 가지고 있는 것처럼 보여 IDE에서 get을 사용할 수있지만, 쿼리에서 전체 데이터를 넣지 않으면 실제 값이 없게된다. |
JPA
- Java Persistence API
- ORM(Object-relational mapping) : 객체와 테이블을 매핑
- JPA는 인터페이스이며, 보통 Hibernate 구현체 사용한다.
장점
- 생산성 : 기본CRUD
- 유지보수 : 필드 수정시 유리
- 패러다임 불일치 해결 : 위 DB와 객체의 차이점 모두 해결
- 성능 최적화 기능
- 1차 캐시와 동일성 보장(같은 트랜잭션 안에선 같은 엔티티 반환), DB Isolation Level 1단계 낮아도 된다.
- 트랜잭션을 지원하는 쓰기 지연 : Insert -> JDBC Batch사용가능,
Update/Delete -> row lock 방지 - 지연 로딩 : 객체가 실제 사용될 떄 쿼리 실행
- 즉시 로딩 : Join SQL로 한번에 연관 객체 미리 조회
설정
/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="hello">
<properties>
<!-- 필수 속성 -->
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="javax.persistence.jdbc.user" value="sa"/>
<property name="javax.persistence.jdbc.password" value=""/>
<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<!--SQL 표준을 지키지 않는 특정 DB만의 고유기능 추가-->
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<!-- 옵션 -->
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.use_sql_comments" value="true"/>
<!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
</properties>
</persistence-unit>
</persistence>
구동방식
EntityManagerFactory enf = Persistence.createEntityManagerFactory("hello");
EntityManager em = enf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member findMember = em.find(Member.class, 1L);
findMember.setName("change");
Member member = new Member();
member.setId(1L);
member.setName("Hello");
em.persist(member);
em.remove(member);
tx.commit();
} catch (Exception e ) {
tx.rollback();
} finally {
em.close();
}
enf.close();
주의
- EntityManagerFactory는 애플리케이션 전체에서 공유
- EntityManagerFactory는 쓰레드 간에 공유X (사용하고 버려야함)
- JAP의 모든 데이터 변경은 트랜잭션 안에서 실행
JPQL - Entity 객체를 대상으로 쿼리 (SQL - DB table 대상으로 쿼리)
List<Member> list = em.createQuery("select m from Member as m", Member.class)
.setFirstResult(2).setMaxResults(7) //페이징
.getResultList();
영속성 컨텍스트
- 엔티티를 영구 저장하는 환경
- 논리적인 개념
- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근
- 스프링에선 EntitiyManget : PersistenceContext = N : 1
- 생명주기
- 비영속 : 새로운 상태
- 영속 : 영속성 컨텍스트에 관리되는 상태
- 준영속 : 관리 되었다가 분리된 상태
- 삭제 : 삭제된 상태
장점
- 1차 캐시 : 조회쿼리시 1차 캐시먼저 조회, 없으면 조회 후 캐시에 저장 후 반환 (캐시 lifecycle은 트랜잭션 동안)
(1차 캐시 안엔 ID, Entity, Snapshot 적재) - 동일성 보장 : 트랜잭션 내에서 같은 것을 조회한다면, == 비교시 같음 으로 보장
(1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭 션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공) - 트랜잭션 사용시 쓰기 지연 : 1차 캐시 저장, 쓰기 쿼리를 쓰기 지연SQL에 저장, 트랜잭션 종료 후 쿼리 수행
- 변경 감지 : flush()실행시, Entitiy와 스냅샷(초기에 읽어온 값)을 비교하여 변경되면 update
- 지연 로딩
flush 실행 : 트랜잭션 커밋시, em.flush() 실행, JPQL 쿼리 실행 시
- (모드를 변경할 수 있긴함 em.setFlushMode(FlushModeType.COMMIT))
- 변경 감지
- 수정된 엔티티 쓰기 지연 SQL 저장소에 저장
- SQL 저장소의 쿼리를 DB에 전송
준영속 상태 : 영속 -> 준영속
em.detach(entity) 준영속 상태로 전환
em.clear() 영속성 컨텍스트 완전 초기화
em.close() 영속성 컨텍스트 종료
엔티티 매핑
스키마 자동 생성 (Entity(객체) 설정값에 맞게)
-> hibernate.hbm2ddl.auto
- create : drop + create
- create-drop : drop + createe + drop
- update : 변경부분 alter
- validate : 엔티티와 테이블이 정상 매핑인지만 확인
- none : 사용X
- 개발초기 : create, update
- 테스트 : update, validate
- 운영 : validtae, none
@Entity
- JPA가 관리하여 매핑하도록
- 기본 생성자 필수
- final , enum, inerface, inner 클래스 사용X
- 저장할 필드에 final X
@Table
- @Table(name="테이블명", schema="스키마명", catalog="카탈로그명",
uniqueConstraints={@uniqueConstraints(name = "NAME_AGE_UNIQUE", columnNames={"NAME", "AGE"})
})
@Column
- @Column(name = "name") : column명
- @Column(nullabe = false, length = 10)
- @Column(insertable = true, updateable = true) : 등록/ 변경여부
- @Column(precision=19, scale=2 ) : BigDecimal 타입에서 자리수 설정
- @Enumerated(EnumType.STRING) : enum 타입
EnumType.ORDINAL -> enum 순서를 DB에 저장(위험)
EnumType.STRING -> enum 이름을 DB에 저장 - @Temporal(TemporalType.TIMESTAMP) : 날짜타입
-> JAVA8 부턴 LocalDate 타입 사용하면 안써도 된다. - @Lob : CLOB(문자), BLOB(나머지) 매핑
- @Transient : JPA매핑 무시
@Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- AUTO : 방언에 따라 자동 지정
- IDENTITY : DB에 위임(MySQL) -> Batch 불가능
JPA는 커밋 시점에 쿼리를 수행한다. 쿼리 저장소에 저장시 AUTO_INCREMENT는 쿼리 수행전이라,
확인불가능 하므로, 이경우 em.persist()시점에 즉시 쿼리 수행 - SEQUENCE : 시퀀스 사용(Oracle)
얘는 insert쿼리는 나중에 수행할 수 있지만, sequnce는 매번 가져와야한다.
다만 allocationSize을 늘리면 한번에 sequence를 여러개 가져오므로 1번만 가져올 수도 있다. - SEQUENCE : 시퀀스 사용(Oracle)
얘는 insert쿼리는 나중에 수행할 수 있지만, sequnce는 매번 가져와야한다.
다만 allocationSize을 늘리면 한번에 sequence를 여러개 가져오므로 1번만 가져올 수도 있다
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ", //매핑할 데이터베이스 시퀀스 이름
initialValue = 1, allocationSize = 1) // 시작값과 증가값
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBER_SEQ_GENERATOR")
- ~~
TABLE : 키 생성용 테이블 사용, @TableGenerator 필요~~
@ManyToOne / @JoinColumn
객체 그래프 탐색, 객체지향적으로 구현하기 위해 -> 연관관계 매핑 (외리캐를 가져오는게 아닌 클래스를 참조)
연관관계의 주인 : 객체 양방향 연관관계는 관리 주인이 필요
단방향 : 한쪽만 외래키 대신에 클래스를 가질때
@Column(name = "TEAM_ID")
private Long teamId; // DB중심
@ManyToOne
@JoinColumn(name = "TEAM_ID") // Id값과 클래스 매핑
private Team team; //객체지향
양방향 : 양쪽에 클래스를 추가 (DB는 애초에 양방향이라 변화가 없음)
연관관계 주인과 mappedBy
- 양방향 = 연관관계(단방향) 2개, 테이블은 연관관계(양방향) 1개
- 따라서 양방향은 둘 중 하나로 외래키를 관리해야함 (연관관계 주인이 관리)
- 주인이 아닌쪽만 mappedBy 속성 사용하고, 외래키를 읽기만 가능하다.
(외래키가 있는 곳을 주인으로)
@OneToMany(mappedBy = "team") // 반대편에선 team으로 매핑 되있다는 표시
List<Member> members = new ArrayList<Member>();
양방향 매핑시 주의
- 연관관계의 주인에 값을 입력해야함, 반대로 하면 외래키를 못쓰므로 null들어감(순수한 객체 관계를 고려하면 양쪽 다 입력해야 하긴함)
-> flush 이전에 Team에서 member찾으려고 하면 존재하지 않게 됨(1차캐시에만 존재하기 때문)
-> 연관관계 편의 매소드를 추가하여 한번에 양쪽입력하도록 설계해도 좋음
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team);
em.persist(member);
- 무한 루프 조심 (toString(), lombok, JSON) 생성 라이브러리 -> 양쪽으로 계속 호출
Controller에서는 Entity로 받지 말자 - 단방향 매핑만으로 개발 -> JPQL으로 역방향탐색하면서 필요하다면 양방향으로 추가
- 외래키가 있는 곳을 주인으로
다대일 단방향 : 외래키 있는곳에 @ManyToOne
다대일 양방향 : 다대일 단방향 + @OneToMany(mappedBy = "")
일대다 단방향 : 외래키 반대쪽 @OneToMany (차라리 다대일 양방향을 추천)
- 외래키를 반대편 테이블이 관리함
->업데이트 쿼리가 1번 나가게 됨 - @OneToMany
@JoinColumn(name = "TEAM_ID") //사용하지 않으면 조인테이블 하나 추가됨
private List members = new ArrayList<>();
일대다 양방향(X) => 다대일 양방향
- @JoinColumn(insertable=false, updatable=false) 으로 유사하게 사용은 가능
일대일
- 외래키를 양쪽에 넣어도 됨 -> 주테이블에 외래키 / 대상 테이블에 외래 키
- 주테이블에 외래키 단방향 : 다대일 단방향과 유사
- 주테이블에 외래키 양방향 : 다대일 양방향과 유사
- 대상 테이블에 외래키 단방향 : JPA 지원 X
- 대상 테이블에 외래키 양방향 = 주테이블 외래키 양방향
- 외래키에 DB unique 제약조건 추가 (없으면 일대다/다대일이 될 수 있으므로)
- 주테이블 외래키 -> 객체지향 개발자가 선호, 값없으면 null 허용해야함
대상테이블 외래키 -> DBA가 선호, 지연로딩 설정해도 즉시 로딩댐
다대다(사용비추)
- DB는 조인 테이블을 추가해서 1 : N N : 1 관계로 풀어야함, 객체는 가능
- 연결 테이블에 외래키 이외의 값은 매핑이 안되므로 사용비추
-
@ManyToMany @JoinTable(name = "MEMBER_PRODUCT") private List<Product> products = new ArrayList<>(); // 단방향 @ManyToMany(mappedBy = "products") private List<Member> members = new ArrayList<>(); // 양방향시 추가
- 연결 테이블이 단순히 연결만 하지 않고 정보도 가지고 있음, 하지만 그런 정보를 접근할 수 없음
-> 조인 테이블도 Entity로 만들어서 @OneToMany @ManyToOne 사용
@JoinColumn
@ManyToOne
@OneToMany
상속 관계 매핑
DB의 슈퍼타입/서브타입 like 객체의 상속
전략 | 조인 | 단일 테이블 | 구현 클래스마다 테이블(비추) |
---|---|---|---|
설명 | 각각 테이블로 변환 후 조인하여 사용 | 전체 상속 구조를 1개의 테이블로 관리 | 각 자식이 부모의 컬럼을 가지는 테이블로 관리 |
@Inheritance(strategy = InheritanceType.XXX) | JOINED | SINGLE_TABLE(default) | TABLE_PER_CLASS |
장점 | 테이블 정규화 | 조인X, 일반적으로 빠른검색 | 서브 타입 구분할 때 효과적, not null 사용가능 |
단점 | 많은 조인, insert 2회 | 자식 entitiy 컬럼에 null 허용, 많은 컬럼 | 전체 조회시 UNION 필요, 통합 쿼리 어려움 |
@DiscriminatorColumn(name=“DTYPE”) : 어떤 자식클래스인지 표시 가능
@DiscriminatorValue(“XXX”) : DTYPE에 넣을 값 설정도 가능
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
@DiscriminatorColumn(name="DTYPE")
public abstract class Item
@Entity
@DiscriminatorValue("M")
public class Movie extends Item
기본 조인으로 구현, 단순/속도 중요하면 단일로 변경
@MappedSuperclass
- 공통 매핑 정보가 필요할 때(등록일, 수정일, 등록자, 수정자 등)
- 상속관계 X, 엔티티X, 테이블과 매핑X, 조회 불가, 추상 클래스로 선언
프록시와 연관관계 관리
프록시 : Member조회 시 Team도 같이 조회 해야할까?
em.find() : 실제 Entity 반환
em.getReference() : 가짜 프록시Entity 반환
프록시
- 내부적으로 실제 클래스를 상속 받음, 겉 모습 동일 -> 사용자 입장에선 구분하지 않고 사용하면 됨
- 실제 객체의 참조(target)을 보관
- 호출시 -> 위 그림과 같이 DB 조회 후 -> 실제 객체의 메소드 호출
- 프록시 객체는 처음 사용할 때 한 번만 초기화 -> 이후 실제 엔티티로 바뀌는것이 아닌 프록시 객체를 통해 실제 Entitiy에 접근 가능한 것 -> 따라서 == 대신에 instance of 사용
- em.getReference() 호출해도 만약 이미 영속성 컨텍스트에 Entity가 있으면, 프록시가 아닌 Entity 반환
반대로, em.getReference() 이후에, em.find() 할경우 em.find()에서 프록시가 반환된다.
-> 처음 반환 타입을 따라 간다고 생각하자. - 준영속 상태일 때, 프록시를 초기화하면 LazyInitializationException 발생
프록시 확인
- 초기화 여부 확인
PersistenceUnitUtil.isLoaded(Object entity) - 프록시 클래스 확인
entity.getClass().getName() 출력(..javasist.. or HibernateProxy…) - 프록시 강제 초기화
org.hibernate.Hibernate.initialize(entity);
지연 로딩 & 즉시 로딩
지연 로딩
- @ManyToOne(fetch = FetchType.LAZY) -> .find()로도 연관 객체 프록시로 지연로딩
- @OneToMany, @ManyToMany는 default 지연 로딩
즉시로딩(가급적 사용X) -> 모든 연관관계를 지연로딩으로 선개발
- @ManyToOne(fetch = FetchType.EAGER) -> 조인하여 쿼리 1번에 모두 조회
- @ManyToOne, @OneToOne은 default가 즉시로딩 -> 변경필요
- JPQL에서 N+1 문제 일으킨다.
-> 쿼리 실행 후, 즉시로딩 확인하고 추가 쿼리 N개 발생
-> 지연로딩 + join fetch OR 엔티티 그래프 로 해결
영속성 전이 CASCADE
특정 엔티티를 영속성 상태로 만들 때, 연관된 엔티티도 같이 영속상태로 만들고 싶을 때, 연관관계 매핑과는 관련이 없고, 편리함만 제공
단일 소유관계일 때만 사용하는것이 좋다.
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : REFRESH
- DETACH : DETACH
고아 객체
- 참조하는 곳이 하나 일때, 다른 곳에서 참조하지 않는 객체를 삭제할 수 있도록 하자
(CascadeType.REMOVE처럼 동작) - @OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST, orphanRemoval = true)
Parent parent1 = em.find(Parent.class, id); parent1.getChildren().remove(0);
-> DELETE FROM CHILD WHERE ID=? - @OneToOne, @OneToMany만 가능
영속성 전이 + 고아 객체, 생명주기
- CascadeType.ALL + orphanRemovel=true
- 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
값 타입
(식별자가 필요하고, 지속해서 값을 추적/조회, 변경해야 한다면 그것은 값 타입이 아닌 엔티티)
기본 타입
- 엔티티 타입 : @Entity로 정의하는 객체, 데이터가 변해도 식별자로 추적 가능, 생명 주기 관리
- 값 타입 : 식별자(@Id)가 없으므로 변경시 추적 불가능, 생명주기를 엔티티에 의존, 값타입 공유되면 X(side effect)
->자바 기본 타입은 값을 복사함(call by value), 래퍼 클래스는 주소값 복사(call by refer) 다만 값 변경 불가- 기본 값 타입 : 자바 기본 타입(int), 래퍼 클래스(Integer), String
(행이 삭제되면 해당 컬럼들 같이 삭제) - 임베디드 타입 : 복합 값 타입(class)
- 기본 값 타입 : 자바 기본 타입(int), 래퍼 클래스(Integer), String
새로운 값 직접 정의, JPA는 임베디드 타입, 재사용가능, 의미있는 함수를 별도로 빼서 관리가능 매핑하는 테이블은 같으나, 객체와 테이블을 세밀히 매핑하게 된다. (Table 수 < Class 수)
@Entity
public class Member{
@Embedded // @Embeddable: 값 타입을 정의하는 클래스에 표시, @Embedded: 값 타입을 사용하는 곳에 표시
private Period workPeriod;
@Embedded
private Address homeAddress;
@Embedded
@@AttributeOverrides({
@AttributeOverride(name = "startDate", column = @Column("EMP_START")),
@AttributeOverride(name = "endtDate", column = @Column("EMP_END"))
}) // 같은 값타입 사용한다면, @AttributeOverrides, @AttributeOverride를 사용해서 컬러 명 속성을 재정의
private Address workAddress;
}
- 컬렉션 값 타입 : 자바 컬렉션(비추) -> 일대다 관계 + Casecade + 고아객체제거 로 변경
@Entity
public class Member{
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID") // 테이블의 외래키 값 설정
) // 별도의 테이블 생성
@Column(name = "FOOD_NAME") // 추가 컬럼이 1개 이므로 컬럼명 설정가능
private Set<String> favoriteFood = new HashSet<>();
@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
@JoinColumn(name = "MEMBER_ID")
) // 별도의 테이블 생성
private List<Address> addressHistrory = new ArrayList<>();
}
컬렉션 값 타입은 영속성 전이(Cascade) + 고아 객체 제거 기능이 필수로 가진다고 보면 된다. (Member만 persist 해도 나머지 table관리) 지연 로딩 전략 사용 컬렉션 업데이트는 지우고 재생성으로 구현 (컬렉션을 다루므로 equals, hashcode 정확히 구현해야함) (또한, 변경사항이 발생하면 추적이 불가능하므로 연관 모든 데이터를 비우고, 현재 있는 값을 다시 저장) 값 타입 컬렉션을 매핑하는 테이블은 수동으로 모든 컬럼을 묶어서 기본 키를 구성해야 함: null 입력X, 중복 저장X
값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다. 따라서 값 타입은 단순하고 안전하게 다 룰 수 있어야 한다.
임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면, 전체 데이터가 변경(side effect)될 수 있다.
Address add = new Address("city","street");
Member mem1 = new Member();
mem1.setAddress(add);
Member mem2 = new Member();
mem2.setAddress(add);
mem2.getAddress().setCity("newCity"); // mem1, mem2의 Address의 city값 둘 다 변경됨
임베디드 타입은 객체 타입이므로 주소값 복사(call by refer) -> 객체의 공유 참조 피할 수 없다.
-> 불편 객체로 설계, 생성자로 만들고 setter 생성X
-> 값 변경하고자 한다면, 값을 복사한 후 새로운 생성자 호출
값 타입 비교
int a = 10; int b = 10;
if(a == b) // 동일성 비교 : 인스턴스가 달라도 그안의 값이 같으면 같은것으로
if(new Address("10") != new Address("10"))
if(new Address("10").equals(new Address("10")) // 동등성 비교
객체지향 쿼리 언어 ( JPQL )
JPA 검색법 : JPQL, JPA Criteria, QueryDSL, 네이티브SQL, JDBC API, Mabatis, SpringJdbcTemplate
JPQL
객체를 대상으로 DB를 조회하기 때문에 모든 경우에서 사용할 순 없음, 결국 검색 조건이 포함된 SQL이 필요
-> SQL을 추상화한 JPQL 제공 (SELECT/FROM/WHERE/GROUP BY/HAVING/JOIN 지원)
String jpql = "select m From Member m where m.name like '%hello%'"; // Member는 테이블이 아닌 객체 -> 객체 지향 쿼리
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
프로젝션 : SELECT 절에 조회할 대상을 지정하는것
- 엔티티 : SELECT m FROM Member m, SELECT m.team FROM Member m -> 영속성 관리 됨
- 임베디드타입 : SELECT m.address FROM Member m
- 스칼라타입 : SELECT m.username, m.age FROM Member m
(DISTINCT 사용가능)
문법
- select_문 :: = select_절 from_절 [where_절] [groupby_절] [having_절] [orderby_절]
- 엔티티와 속성은 대소문자 구분(객체니까)
- JPQL 키워드는 대소문자 구분X(select, SELECT)
- 엔티티의 이름 사용, 테이블 이름 아님
- 별칭 필수(as는 생각가능)
- 집계함수 사용가능(SUM, AVG, COUNT)
-
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class); // TypeQuery 반환 타입 명확 Query query = em.createQuery("SELECT m.username, m.age from Member m"); // Query 반환 타입 명확치 않을 때 List<Object[]> resultList = em.createQuery("SELECT m.username, m.age from Member m").getResultList(); Object[] result = resultList.get(0); for(Object obj : result) {} // 반환타입이 명확치 않을 때, 왼쪽에서부터 하나씩 오브젝트로 들어감 List<MemberDTO> memberDTOs = em.createQuery(SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m ).getResultList(); // new 명령어로 생성자 조회
-
query.getResultList(); // 결과가 하나 이상일 때, 없으면 빈 리스트 query.getSingleResult(); // 결과가 정확히 하나, 없으면 NoResultException, 복수면 NonUniqueResultException
- 파라미터 바인딩
em.createQuery("SELECT m FROM Member m where m.username=:username", Member.class) // 이름기준
.setParameter("username", usernameParam);
em.createQuery("SELECT m FROM Member m where m.username=?1", Member.class) // 위치기준(비추)
.setParameter(1, usernameParam);
_update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
페이징 -> DB방언 별로 맞춰서 쿼리 발생
List<Member> members = em.createQuery("select m from Member m oreder by m.age desc", Member.class)
.setFirstResult(0) // 조회 시작 위치 (0부터)
.sertMaxResults(10) // 조회할 데이터 수
.getResultList();
조인
- 내부조인 : SELECT m FROM Member m [INNER] JOIN m.team t
- 외부조인 : SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- ON절 사용
- 조인 대상 필터링 : SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
-> SELECT m. *, t. * FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A' - 연관관계 없는 엔티티 외부 조인 : SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
-> SELECT m. *, t. * FROM Member m LEFT JOIN Team t ON m.username = t.name
- 조인 대상 필터링 : SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
- ON절 사용
- 세타조인 : select count(m) from Member m, Team t where m.username = t.name (cross join 발생)
서브쿼리
- 한 건이라도 주문한 고객 select m from Member m where (select count(o) from Order o where m = o.member) > 0
- 지원 함수
- [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
팀A 소속인 회원 select m from Member m where exists (select t from m.team t where t.name = ‘팀A') - {ALL | ANY | SOME} (subquery) ALL 모두 만족 ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
전체 상품 각각의 재고보다 주문량이 많은 주문들 select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p) - [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
- AND, OR, NOT, =, >, <, <>, BETWEEN, LIKE, IS NULL 은 SQL과 동일
- [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
- 한계
- JPA는 WHERE, HAVING 절에서만 서브쿼리 가능
- Hibernate는 SELECT 절도 가능
- FROM 절의 서브 쿼리는 현재 JPQL에서 불가능(조인으로 풀어서 해결, 혹은 네이티브쿼리, 혹은 쿼리 2번으로)
JPQL 타입 표현
ex)
SELECT m.username, 'HELLO', TRUE
FROM Member m
WHERE m.type = jpql.MemberType.ADMIN
- 문자 - ''
- 숫자 - 10L(Long), 10D(Double), 10F(Float)
- Boolean - TRUE, FALSE
- ENUM - jpabook.MemberType.Admin (패키지명 포함)
- 엔티티 타입 - select i from Item i where type(i) = Book (상속 관계에서 사용)
조건식
- 기본 CASE
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금' end
from Member m - 단순 CASE
select
case when '팀A' then '인센티브110%'
when '팀B then '인센티브120%'
else '인센티브105%' end
from Team t - COALESCE(오라클 NVL)
select coalesce(m.username,'이름 없는 회원') from Member m - NULLIF(두 값이 같으면 null, 다르면 첫번째 값 반환)
select NULLIF(m.username, '관리자') from Member m
JPQL 기본함수
- CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE(문자열찾기), ABS, SQRT, MOD, SIZE
사용자 정의 함수
-
public class MyH2Dialect extends H2Dialect { // H2Dialect class 참조하여 사용 public MyH2Dialect() { registerFunction("group_concat", new StandardSQLFunction("group_concat"), StandardBasicTypes.STRING)); } } // 이후에 설정에 MyH2Dialect
- select function('group_concat', i.name) from Item i
경로 표현식
select m.username -- 상태 필드 : 단순히 값을 저장하는 필드
from Member m
join m.team t -- 단일 값 연관 필드 : 연관관계 필드 (@ManyToOne, @OneToOne, 대상이 엔티티)
join m.orders o -- 컬렉션 값 연관 필드 : 연관관계 필드 (@OneToMany, @ManyToMany, 대상이 컬렉션)
where t.name = '팀A'
- 탐색 = .(점)을 찍어 객체 그래프를 탐색
- 상태 필드 : 경로 탐색의 끝(탐색X)
- 단일 값 연관 경로 : 묵시적 내부 조인 발생(탐색O)
- 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생(탐색X) -> FROM 절에서 명시적 조인을 통해 별칭 얻으면 탐색 가능
- 묵시적 내부 조인은 성능 저하의 원인이 될 수 있고, 위치를 파악하기 힘드므로 비추
패치 조인(fetch join)
정말정말 중요하다
- JPQL에서 성능 최적화를 위해 제공하는 기능 -> N + 1 문제 해결
- 지연로딩 보다 패치조인이 우선 적용(전부 지연 로딩 + 최적화 필요한 곳에 패치조인)
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
- 여러 테이블을 조인해서 엔티티를 반환하는 것이 아니라면, 일반 조인을 사용하고 DTO를 반환하는것이 효과적
-
select m from Member m join fetch m.team -- JPQL -> SELECT M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID=T.ID -- SQL
- 컬렉션 패치 조인 - @OneToMany, @ManyToMany, 대상이 컬렉션이면 데이터가 여러개 나온다. -> DISTINCT 활용하기
select t from Team t join fetch t.members where t.name = '팀A' -- JPQL
->
SELECT T.*, M.* FROM TEAM T
INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
WHERE T.NAME = '팀A' -- SQL -->Team이 여러개 생성됨, Member를 select 했다면 1개 생성
- DISTINCT
- SQL에 DISTINCT 추가
- 애플리케이션에서 엔티티 중복 제거
-
SQL에선 로우전체가 다른것이 아니므로 중복제거는 안됨select distinct t from Team t join fetch t.members where t.name = '팀A'
-> 하지만 이후에 애플리케이션에서 같은 식별자를 가진 Team 엔티티를 제거해줌
- 패치 조인과 일반 조인의 차이
- 일반조인은 연관관계 고려 X, 단지 SELECT 절에 지정한 엔티티만 조회
- 패치조인은 연관관계를 고려하여 한번에 조회가능(즉시 로딩)
- 특징과 한계
- 패치 조인 대상에는 별칭을 줄 수 없다. 대상의 일부만 가져오면 안된다.(hibernate는 되지만, 가급적 사용X)
- 둘 이상의 컬렉션은 패치 조인 할 수 없다. ( 1: N , N : M 된다.)
- 컬렉션을 패치조인하면 페이징 API 사용할 수 없다. (일대다에서 결과가 5개나왔지만, 2개씩 페이징한다면? 위험)
(일대일, 다대일 같은 단일 값 연관 필드는 페이징 가능 -> 따라서 반대로 가져오기)
다형성 쿼리
- TYPE (조회 대상을 특정 자식으로 한정)
select i from Item i
where type(i) IN (Book, Movie) -- JPQL
->
select i from i
where i.DTYPE in ('B', 'M') -- SQL
- TREAT (타입 캐스팅과 유사)
select i from Item i
where treat(i as Book).auther = 'kim' -- JPQL
->
select i.* from Item i
where i.DTYPE = 'B' and i.auther = 'kim' -- SQL
엔티티 직접 사용
- 기본 키 값 ( 엔티티 직접 사용시, 기본 키 값을 사용)
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용 -- JPQL
->
select count(m.id) as cnt from Member m -- 둘다 같은 SQL 발생
- 외래 키 값
select m from Member m where m.team = :team -- JPQL
... .setParameter("team", team) -- 파라미터 전달
->
select m.* from Member m where m.team_id = ? -- SQL
Named 쿼리
@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
//선언 후 쿼리 사용
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1").getResultList();
- 미리 정의해서 이름을 부여, 정적 쿼리
- 애플리케이션 로딩 시점에 초기화 후 재사용
- 애플리케이션 로딩 시점에 쿼리 검증
- .xml로 관리 가능
- .xml의 우선순위가 annotation보다 높다
- 애플리케이션 환경별로 다른 쿼리 가능하다
- SpringData JPA 에선 @Query("")를 인터페이스 함수 위에 사용가능
벌크 연산
- 쿼리 한 번으로 엔티티를 여러개 UPDATE, DELETE 할 때 유용 (hibernate는 INSERT(insert into .. select )도 지원)
-
String qlString = "update Product p " + "set p.price = p.price * 1.1 " + "where p.stockAmount < :stockAmount"; int resultCount = em.createQuery(qlString) .setParameter("stockAmount", 10) .executeUpdate();
- 주의
- 영속성 컨텍스트를 무시하고 직접 쿼리 실행
- 해결
- 벌크 연산을 먼저 실행
- 벌크 연산을 실행, 이후에 영속성 컨텍스트 초기화한 후 비지니스 로직 구현
Criteria(비추)
JPQL은 동적쿼리 생성시 고려해야 할 사항이 너무 많다.
-> typesafe하게 개발, 동적쿼리에 유리, 하지만 보다 복잡하고 SQL과의 괴리감.
//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
//루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
//쿼리 생성 CriteriaQuery<Member> cq =
query.select(m).where(cb.equal(m.get("username"), “kim”));
List<Member> resultList = em.createQuery(cq).getResultList();
QueryDSL
컴파일 시점에 오류 찾을 수 있고, Criteria 상위호환(typesfae, 동적쿼리, 단순하고 쉽다)
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member; // QFile 미리 세팅
List<Member> list =
query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
네이티브 SQL
SQL을 직접사용할 수 있다. (힌트나 테이블스페이스 같은 쿼리 사용시)
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
JDBC, SpringJdbcTemplate
JPA와 함께 사용할 수 있다. 다만, JPA가 아니므로 적절한 시점에 강제로 플러시 해줘야 함. (JPQL은 자동으로 플러시 됨)
-> JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
'스프링 > JPA' 카테고리의 다른 글
JPA 상속 관계 (TABLE_PER_CLASS전략) (0) | 2021.05.21 |
---|---|
JPA 활용 1 (0) | 2021.04.23 |
JPA 활용 2 (0) | 2021.04.23 |
스프링-입문-스프링부트 + JPA (0) | 2021.04.16 |
다중 DBconnection (with JPA) (0) | 2021.02.27 |
댓글