티스토리 뷰
728x90
반응형
자바 ORM 표준 JPA 프로그래밍 - 기본편
김영한님 강의듣고 정리하기
1. 값 타입
JPA의 데이터 타입 분류
- 엔티티 타입
- @Entity로 정의하는 객체
- 데이터가 변해도 식별자로 지속해서 추적 가능
- 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
- 값 타입
- int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
- 식별자가 없고 값만 있으므로 변경시 추적 불가
- 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
값 타입 분류
- 기본값 타입 (int, double, Integer, Long 등)
- 임베디드 타입
- 컬렉션 값 타입
임베디드 타입
- 새로운 값 타입을 직접 정의할 수 있다.
- 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 한다.
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embedded : 값 타입을 사용하는 곳에 표시
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String email;
@Embedded
private Adderss address;
}
@Embeddable
public class Adderss {
private String city;
private String street;
public Adderss(String city, String street) {
this.city = city;
this.street = street;
}
}
임베디드 타입은 엔티티의 값일 뿐이다. 사용전과 후에 매핑하는 테이블은 같다.
객체타입의 한계
- 기본 타입(primitive type)
int a = 10;
int b = a; // 기본 타입은 값을 복사
b = 4;
기본 타입은 값을 복사하기 때문에 b를 바꿔도 a에 영향이 없다.
- 객체 타입
Address a = new Address("Old");
Address b = a; // 객체 타입은 참조를 전달
b.setCity("New")
객체타입은 참조를 공유 하기 때문에 b를 바꾸면 a까지 바뀌게 된다.
그래서 값을 바꿀려면 set을 사용하지 말고 객체를 새로 생성해 생성자로 바꾸는게 바람직하다.
불변 객체
- 불변 객체 : 생성 시점 이후 절대 값을 변경할 수 없는 객체
- 생성자로만 값을 설정하고 수정장(Setter)를 만들지 않으면 된다.
- 참고 : Integer, String은 자바가 제공하는 대표적인 불변 객체
값 타입의 비교
- 동일성(identity) 비교 : 인스턴스의 참조 값을 비교, == 사용
- 동등성(equivalence) 비교 : 인스턴스의 값을 비교, equals() 사용
- 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야함
- 값 타입의 equals() 메소드를 적절하게 재정의 (주로 모든 필드 사용)
int a = 10;
int b = 10;
a == b // true
Address a = new Address("서울시");
Address b = new Address("서울시");
a == b // false, a와 b의 참조값이 다르다.
a.equals(b) // true (근데 객체같은경우는 equals를 재정의해서 사용해야 true가 나온다. 안에 필드끼리 다 equals를 해줘야하는듯)
값 타입 컬렉션
- 값 타입을 하나 이상 저장할 때 사용한다.
- 컬렉션(Set, List 등)에 기본값 타입 또는 임베디드 타입을 넣은 형태이다.
- 연관관계 매핑에서 엔티티를 컬렉션으로 사용하는 것이 아니라 값 타입을 컬렉션에 쓰는 것이다.
- @ElementCollection, @CollectionTable 사용
@Entity
public class Member {
...
@ElementCollection
@CollectionTable(
name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME") // 컬럼명 지정 (예외)
private Set<String> favoriteFoods = new HashSet<>();
@ElementCollection
@CollectionTable(
name = "ADDRESS",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
...
}
값 타입 컬렉션의 제약사항
- 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다. (결과는 원하는대로 나오더라도 쿼리가 예상하지 못하게 발생할 수 있다.)
- 값은 변경하면 추적이 어렵다.
값 타입 컬렉션 대안
- 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 낫다. (일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용하자)
- 영속성 전이(cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션처럼 사용하자
대안에 대한 예시 코드
@Entity
public class Member {
...
@ElementCollection
@CollectionTable(
name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "MEMBER_ID"))
@Column(name = "FOOD_NAME")
private Set<String> favoriteFoods = new HashSet<>();
// 변경
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
...
}
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "ADDRESS")
public class AddressEntity { // 엔티티를 만들고 그안에 값타입을 생성하는게 실무에 좋다.
@Id @GeneratedValue
private Long id;
private Address address; // 값 타입
}
정리
- 엔티티 타입의 특징
- 식별자 O
- 생명주기 관리
- 공유
- 값 타입의 특징
- 식별자 X
- 생명주기를 엔티티에 의존
- 공유하지 않는것이 안전 (복사해서 사용)
- 불변객체로 만드는 것이 안전
값타입은 정말 값 타입이라 판단될 때만 사용해야한다.
식별자가 필요하고, 지속해서 값을 추적, 변경해야 한다면 그것은 값 타입이 아닌 엔티티이다.
2. 객체지향 쿼리언어(JPQL)
JPA는 다양한 쿼리 방법을 지원
- JPQL
- JPA Criteria
- QueryDSL
- 네이티브 SQL
- JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용
JPQL
- 엔티티 객체를 대상으로 쿼리
- SQL과 문법유사
- 문제는 검색쿼리, 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
- JPQL을 한마디로 정의하면 객체 지향 SQL
// 검색
String jpql = "select m from Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class)
.getResultList();
// 실행된 SQL
select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age > 18
Criteria
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있다.
- JPA 공식 기능
- 너무 복잡하고 실용성이 없다.
- Criteria 대신에 QueryDSL 사용 권장
QueryDSL
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있다.
- 동적쿼리 작성 편리함, 단순하고 쉬움
- 컴파일 시점에 문법 오류를 찾을 수 있다.
- 실무사용 권장
네이티브 SQL
- JPA가 제공하는 SQL을 직접 사용하는 기능
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
JPQL 문법
- select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자 구분 O (Member, age)
- JPQL 키워드는 대소문자 구분 X (SELECT, FROM, where)
- 엔티티 이름 사용, 테이블 이름이 아님 (Member)
- 별칭은 필수 (m) (as는 생략가능)
TypeQuery, Query
- TypeQuery : 반환 타입이 명확할 때 사용
- Query : 반환타입이 명확하지 않을 때 사용
// 반환타입이 한개로 명확
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m" , Member.class);
// 반환타입이 여러가지 (String(username), int(age))
Query query = em.createQuery("SELECT m.username, m.age from Member m");
결과 조회 API
- query.getResultList() : 결과가 하나 이상일 때, 리스트 반환 (결과가 없으면 빈 리스트 반환)
- query.getSingleResult() : 결과가 정확히 하나, 단일 객체 반환 (나머지는 에러반환)
파라미터 바인딩 - 이름기준, 위치기준
// 이름기준
SELECT m FROM Member m where m.username = :username
query.setParameter("username", usernameParam);
// 위치기준
SELECT m FROM Member m where m.username = ?1
query.setParameter(1, usernameParam);
이름 기준으로 쓰는게 좋다, 위치기준으로 하게되면 나중에 추가할 경우 순서가 바껴 에러에 원인이 될 수 있다.
프로젝션
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 -> 스칼라 타입 프로젝션
프로젝션 - 여러 값 조회
- Query 타입으로 조회
- Object[] 타입으로 조회
- new 명령어 조회
- SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
페이징 API
- setFirstResult (int startPosition) : 조회 시작 위치 (0부터 시작)
- setMaxResults (int maxResult) : 조회할 데이터 수
// 페이징 쿼리
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();
조인
- 내부조인 : SELECT m FROM Member m [INNER] JOIN m.team t
- 외부조인 : SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- 세타조인 : SELECT count(m) from Member m, Team t where m.username = t.name
조인 - ON절
- 조인 대상 필터링
- 연관관계 없는 엔티티 외부 조인
// 조인 대상 필터링
// 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
// JPQL
SELECT m , t FROM Member m LEFT JOIN m.team t on t.name = 'A'
// SQL
SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID = t.id and t.name = 'A'
// 연관관계 없는 엔티티 외부 조인
// 회원의 이름과 팀의 이름이 같은 대상 외부 조인
// JPQL
SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
// SQL
SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
서브쿼리
// 나이가 평균보다 많은 회원
select m from Member m where m.age > (select avg(m2.age) from Member m2)
// 한 건이라도 주문한 고객
select m from Member m where (select count(o) from Order o where m = o.member) > 0
- [NOT] EXISTS (subquery)
- {ALL | ANY | SOME} (subquery)
- ALL : 모두 만족하면 참
- ANY, SOME : 같은의미, 조건을 하나라도 만족하면 참
- [NOT] IN (subquery) : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
// 팀A 소속인 회원
select m from Member m where exists (select t from m.team t where t.name = '팀A')
// 전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o where o.orderAmount > ALL (select p.stockAmount from Product p)
// 어떤 팀이든 팀에 소속된 회원
select m from Member m where m.team = ANY (select t from Team t)
JPA 서브 쿼리 한계
- JPA는 WHERE, HAVING 절에서만 서브쿼리 사용 가능
- SELECT 절도 가능(하이버네이트에서 지원)
- FROM절의 서브쿼리는 JPQL에서 불가능
조건식 - CASE 식
// 기본 CASE 식
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
- COALESCE : 하나씩 조회해서 null이 아니면 반환
- NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
// 사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username, '이름 없는 회원') from Member m
// 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
경로 표현식
.(점)을 찍어 객체 그래프를 탐색하는 것
- 상태필드 (state field) : 단순히 값을 지정하기 위한 필드 (ex: m.username)
- 연관필드 (association field) : 연관관계를 위한 필드
- 단일값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티 (ex: m.team)
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션 (ex: m.orders)
경로표현식 특징
- 상태필드 : 경로 탐색의 끝, 탐색 X
- JPQL : select m.username, m.age from Member m
- SQL : select m.username, m.age from Member m
- 단일값 연관 경로 : 묵시적 내부 조인(inner join) 발생, 탐색 O
- JPQL : select o.member from Order o
- SQL : select m.* from Order o inner join Member m on o.member_id = m.id
- 컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색 X
- FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색가능
명시적 조인, 묵시적 조인
- 명시적 조인 : join 키워드 직접 사용
- 묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부조인만 가능)
예제
// 성공 (단일값이므로 탐색가능)
select o.member.team from Order o
// 성공 (team은 단일값이므로 탐색 가능)
select t.members from Team
// 실패 (members는 컬렉션값 이므로 더이상 탐색 불가)
select t.members.username from Team t
// 성공 (컬렉션이라도 명시적 조인을 통해 별칭으로는 가능함)
select m.username from Team t join t.members m
묵시적 조인 주의사항
- 내부조인이 일어나므로 파악 힘듬
- 가급적 묵시적 조인 대신에 명시적 조인 사용
- 조인은 SQL 튜닝에 중요 포인트
페치 조인(fetch join)
- SQL 조인 종류 X
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한번에 함께 조회하는 기능
- join fetch 명령어 사용
// JPQL
select m from Member m join fetch m.team
// SQL
select M.*, T.* FROM MEMBER M INNER JOIN TEAM T ON M.TEAM_ID = T.ID
페치조인과 일반조인의 차이
- 일반조인
- JPQL은 결과를 반환할때 연관관계 고려 X
- 단지 SELECT절에 지정한 엔티티만 조회할 뿐
- 페치조인
- 연관된 엔티티도 함께 조회 (즉시로딩이 된다. 지연로딩 설정하더라도 페치조인을 사용하면 즉시로딩이 된다.)
- 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념
페치조인 특징
- 연관된 엔티티들을 SQL 한번으로 조회 - 성능 최적화
- 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함 (@OneToMany(fetch = FetchType.LAZY => 글로벌 로딩 전략)
- 최적화가 필요한 곳은 페치조인 적용
페치조인 한계
- 페치조인 대상에는 별칭을 줄 수 없다.
- 둘 이상의 컬렉션은 페치조인 할 수 없다.
- 컬렉션을 페치조인하면 페이징 API (setFirstResult, setMaxResults)를 사용할 수 없다.
Named 쿼리
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 정적 쿼리
- 애플리케이션 로딩 시점에 초기화후 재사용
- 애플리케이션 로딩 시점에 쿼리를 검증 (에러가 날경우 컴파일시에 알 수 있기 때문에 좋다.)
@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();
Reference
www.inflearn.com/course/ORM-JPA-Basic/dashboard
반응형
'Programming > Springboot' 카테고리의 다른 글
[Springboot] JPA, N + 1 쿼리 증가 문제 (0) | 2021.05.15 |
---|---|
[Springboot] 자바 ORM 표준 JPA 프로그래밍 - 2 (0) | 2021.04.11 |
[Springboot] 자바 ORM 표준 JPA 프로그래밍 - 1 (0) | 2021.04.08 |
[Springboot] Spring 헷갈리는 내용 (21.03.25) (0) | 2021.03.25 |
댓글