티스토리 뷰

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

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., 본 강의는 자바 백엔

www.inflearn.com

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30