보글보글 개발일지
article thumbnail
반응형
섹션 9~11

섹션 9. 값 타입

기본값 타입

[JPA의 데이터 타입 분류]

  • 엔티티 타입
    • @Entity로 정의하는 객체
    • 데이터가 변해도 식별자로 지속해서 추적 가능
    • 예) 회원 엔티티의 키나 나이 값을 변경해도 식별자로 인식 가능
  • 값 타입 
    • int, Integer, String처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체
    • 식별자가 없고 값만 있으므로 변경 시 추적 불가
    • 예) 숫자 100을 200으로 변경하면 완전히 다른 값으로 대체
    • 값 타입 분류: 기본 값타입, 임베디드 타입, 컬렉션 값 타입(collection value type) 

    • 기본값 타입
      • 자바 기본 타입(int, double)
      • 래퍼 클래스(Integer, Long)
      • String
      • 생명 주기를 엔티티에 의존. 회원을 삭제하면 이름, 나이 필드도 함께 삭제
      • 값 타입은 공유하면 안 됨. 회원 이름 변경 시 다른 회원이 이름도 함께 변경되면 안 됨.
      • 참고: 자바의 기본 타입은 절대 공유되지 않음. 기본타입은 항상 값을 복사한다. 저장공간이 따로따로 있다.
      • Integer 같은 래퍼 클래스나 String 같은 특수 클래스는 공유 가능한 객체지만 변경X

임베디드 타입

  • 임베디드 타입(embedded type, 복합 값 타입): x,y좌표 묶어서 쓰고 싶을 때
    • 새로운 값 타입을 직접 정의할 수 있음 
    • JPA는 임베디드 타입(embedded type)이라 함
    • 주로 기본 값 타입을 모아서 만들어서 복합 값 타입이라고도 함
    • int, String과 같은 값 타입
    • 임베디드 타입 사용법
      • @Embedded: 값 타입을 사용하는 곳에 표시
      • @Embeddable: 값 타입을 정의하는 곳에 표시
      • 기본 생성자 필수
    • 임베디드 타입의 장점
      • 재사용
      • 높은 응집도
      • Period.isWork()처럼 해당 값 타입만 사용하는 의미 있는 메소드를 만들 수 있음
      • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함
      • 임베디드 타입과 테이블 매핑
        • 임베디드 타입은 엔티티의 값일 뿐이다.
        • 임베디드 타입을 사용하기 전과 후에 매핑하는 테이블은 같다.
        • 객체와 테이블을 아주 세밀하게(find-grained) 매핑하는 것이 가능
        • 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많음 
      • @AttributeOverride: 속성 재정의
        •  엔티티에서 같은 값 타입을 사용하면? 컬럼 명이 중복됨
          ex) private Address a; private Address b;
        • @AttributeOverrides, @AttributeOverride를 사용해서 컬럼명 속성을 재정의
      • 임베디드 타입의 값이 null이면 매핑한 컬럼 값은 모두 null

[Period.java]

임베디드 타입을 쓴것. 기본 생성자 필수

package jpabook.jpashop.domain;

import javax.persistence.Embeddable;
import java.time.LocalDateTime;
@Embeddable
public class Period {
    private LocalDateTime startDate;
    private LocalDateTime endDate;

    public Period() {
    }

    public LocalDateTime getStartDate() {
        return startDate;
    }

    public void setStartDate(LocalDateTime startDate) {
        this.startDate = startDate;
    }

    public LocalDateTime getEndDate() {
        return endDate;
    }

    public void setEndDate(LocalDateTime endDate) {
        this.endDate = endDate;
    }
}

[Address.java]

package jpabook.jpashop.domain;

import javax.persistence.Embeddable;

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    public Address() {
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    public void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }
}

[BaseEntity.java]

package jpabook.jpashop.domain;

import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.time.LocalDateTime;

public class BaseEntity {

    @Id @GeneratedValue @Column(name = "MEMBER_ID")
    private Long id;

    @Column(name= "USERNAME")
    private String username;

    //Period
    @Embedded
    private Period workPeriod;

    @Embedded
    private Address homeAddress;

}

[@AttributeOverride 예시]

값 타입과 불변 객체

값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다.
따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.

  • 값 타입 공유 참조: 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함. 부작용 발생.

의도적으로 둘다 바꾸고 싶으면 엔티티를 써야한다.
값을 복사해서 사용한다.

  • 객체 타입의 한계
    • 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
    • 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입이 아니라 객체 타입이다.
    • 자바 기본 타입에 값을 대입하면 값을 복사한다.
    • 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
    • 결론: 객체의 공유 참조는 피할 수 없다.

객체타입: a,b가 같은 인스턴스를 공유한다.

  • 불변 객체
    • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
    • 값 타입은 불변 객체(immutable object)로 설계해야함
    • 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
    • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨
      • --> Getter만 만들고 Setter지우기. 값 바꾸는거 자체가 불가능. 
      • --> Setter을 private으로 선언해서 밖에서 바꾸지 못하도록.
      • 값 바꾸고 싶으면 내부에 copy메서드 만들어서!
    • 참고: Integer, String은 자바가 제공하는 대표적인 불변 객체

값 타입의 비교

  • 값 타입: 인스턴트 달라도 그 안 값이 같으면 같은걸로 봐야함.
  • 값 타입의 비교 
    • 동일성(identity) 비교: 인스턴스의 참조 값을 비교, == 사용
    • 동등성(equivalence) 비교: 인스턴스의 값을 비교, equals() 사용
      • equals()를 기본 제공하는 걸로 Override해서 써야함. hasCode도 같이.
    • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함
    • 값 타입의 equals() 메소드를 적절하게 재정의(주로 모든 필드 사용)

값 타입 컬렉션

값 타입을 컬렉션에 담아서 쓰는 것. DB에서 컬렉션을 담을 수 없다. 

@Embedded
private Address homeAddress;

@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
	@JoinColumn(name = "MEMBER_ID"))
private Set<String> favoriteFoods = new HashSet<>();

@ElementCollection
@CollectionTable(name = "ADDRESS", joinColumns =
	@JoinColumn(name = "MEMBER_ID"))
private List<Address> addressHistory = new ArrayList<>();
  • 값 타입 컬렉션
    • 값 타입을 하나 이상 저장할 때 사용
    • @ElementCollection, @CollectionTable 사용
    • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
    • 컬렉션을 저장하기 위한 별도의 테이블이 필요함
  • 값 타입 컬렉션 사용
  • 값 타입 저장 예제 : 값타입은 본인 스스로 라이프 사이클이 없고 생명 주기가 멤버에 소속된다. 
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("city1", "street", "10000"));

member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("족발");
member.getFavoriteFoods().add("피자");

member.getAddressHistory().add(new Address("old1", "street", "10000"));
member.getAddressHistory().add(new Address("old2", "street", "10000"));

  • 값 타입 조회 예제
    • 값 타입 컬렉션도 지연 로딩 전략 사용
em.persist(member);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());

List<Address> addressHistory = findMember.getAddressHistory();
for (Address address : addressHistory) {
    System.out.println("address.getCity() = " + address.getCity());
}

Set<String> favoriteFoods = findMember.getFavoriteFoods();
for (String favoriteFood : favoriteFoods) {
    System.out.println("favoriteFoods() = " + favoriteFood);
}
  • 값 타입 수정 예제: 완전히 교체를 해줘야한다. 컬렉션 값만 바꿔도 실제 데베 쿼리 날라가면서 뭐가 변경되는지 알고 JPA가 알아서 바꿔준다.
Member findMember = em.find(Member.class, member.getId());
//homeCity->newCity
//findMember.getHomeAddress().setCity("newCity") 이걸로 하면 안된다
Address a = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("newCity", a.getStreet(), a.getZipcode()));
//치킨-->한식
findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");

멤버에 의존관계를 다 맡긴다.

//equals로 비교를 하기때문에 메소드를 잘 만들어야한다.
findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));
findMember.getAddressHistory().add(new Address("newCity1", "street", "10000"));
  • 참고: 값 타입 컬렉션은 영속성 전에(Cascade) + 고아 객체 제거 기능을 필수로 가진다고 볼 수 있다.
  • 값 타입 컬렉션의 제약사항
    • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
    • 값은 변경하면 추적이 어렵다.
    • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
    • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야 함: null 입력X, 중복 저장X
  • 값 타입 컬렉션 대안 --> 값타입 컬렉션은 매우 단순하고 추적도 필요없을 때 사용. 
    • 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려
    • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
    • 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
    • EX) AddressEntity

실전 예제 6 - 값 타입 매핑

 

섹션 10. 객체지향 쿼리 언어1 - 기본 문법

소개

  • JPA는 다양한 쿼리 방법을 지원- 대부분을 JPQL쓰고, 안되면 다른거 사용
  • 가장 단순한 조회 방법 : EntityManager.find(), 객체 그래프 탐색(a.getB().getC())
  • JPQL:
    • JPA를 사용하면 엔티티 객체를 중심으로 개발
    • 문제는 검색 쿼리: 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
    • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
    • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
    • JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
    • SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
    • JPQL은 엔티티 객체를 대상으로 쿼리 // SQL은 데이터베이스 테이블을 대상으로 쿼리
    • 정리
      • 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
      • SQL을 추상화해서 특정 데이터베이스 SQL에 의존X
      • JPQL을 한마디로 정의하면 객체 지향 SQL 
        • 하지만 동적쿼리에는 조금 어려움이 있다!

//검색
String jpql = "select m From Member m where m.name like ‘%hello%'";
List<Member> result = em.createQuery(jpql, Member.class)
.getResultList();
  • Criteria: 문자가 아닌 자바 코드로 JPQL 작성 가능. JPA 공식 기능이지만 너무 복잡하고 실용성이 없다. 그래서 QueryDSL 사용
  • 실무에서 안쓴다! 유지보수가 어렵다.. 이런게 있구나 정도만 알고 있기. 
//QueryDSL 소개
//JPQL
//select m from Member m where m.age > 18

JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list =
    query.selectFrom(m)
    .where(m.age.gt(18))
    .orderBy(m.name.desc())
    .fetch();
  • QueryDSL
    • 문자가 아닌 자바 코드로 JPQL 작성 가능. 빌더 역할
    • 컴파일 시점에 문법 오류를 찾을 수 있음.
    • 동적 쿼리 작성 편리, 단순, 쉽다.
    • 실무 사용 권장
  •  네이티브 SQL: 잘 안쓰심
    • JPA가 제공하는 SQL을 직접 사용하는 기능
    • JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
    • 예) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트

 

  • JDBC 직접 사용, SpringJdbcTemplate
    • JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능
    • 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
    • 예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
  • --> JPQL(95%) + QueryDSL 섞어서 쓴다.

기본 문법과 쿼리 API

  • JPQL은 객체지향 쿼리 언어다.
  • 따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다
  • JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하 지 않는다.
  • JPQL은 결국 SQL로 변환된다.

  • 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: 반환 타입이 명확하지 않을 때 사용
  • 결과 조회 API
    • query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
    • query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
  • 파라미터 바인딩 - 이름 기준, 위치 기준

member1이 결과로 나온다. 위치기준은 웬만해서 쓰지 말기

프로젝션(SELECT)

  • [프로젝션]
    • SELECT 절에 조회할 대상을 지정하는 것
    • 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)
    • SELECT m FROM Member m -> 엔티티 프로젝션 (영속성 컨텍스트가 관리함)
    • SELECT m.team FROM Member m -> 엔티티 프로젝션 (조인 쿼리가 나간다)
      • SELECT m.team from Member m join m.team -> 조인 명시하는게 좋다.
    • SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
      • "select o.address from Order o" , Address.class
      • 어디 소속인지 명시를 해줘야한다. 엔티티부터 시작
    • SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
      • 원하는 것을 막 가져오는 것.
    • DISTINCT로 중복 제거
  • 프로젝션 - 여러 값 조회
    • SELECT m.username, m.age FROM Member m
      • 1. Query 타입으로 조회
      • 2. Object[] 타입으로 조회
      • 3. new 명령어로 조회
        • 단순 값을 DTO로 바로 조회
          • SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
        • 패키지 명을 포함한 전체 클래스 명 입력
        • 순서와 타입이 일치하는 생성자 필요

페이징

  • 페이징 API
    • JPA는 페이징을 다음 두 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();

limit ? offset ? 이건 데이터베이스 방언이다. 방언에 따라 다르게 나간다.

조인

  • 조인
    • 내부 조인: SELECT m FROM Member m [INNER] JOIN m.team t
      • INNER은 생략 가능
    • 외부 조인: SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
    • 세타 조인: select count(m) from Member m, Team t where m.username = t.name
      • 연관관계 없는 걸 막 조인하는 것

INNER JOIN
세타 조인

  • ON 절
    1. 조인 대상 필터링: • 예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
      1. JPQL:
        SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
      2. SQL:
        SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
    2. 연관관계 없는 엔티티 외부 조인:• 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
      1. JPQL:
        SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
      2. 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): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
  • JPA 서브 쿼리 한계: WHERE, HAVING 절에서만 서브 쿼리 사용 가능. 
    • SELECT 가능
    • FROM 안의 서브쿼리(from (select m.age from Member m) 이런 것) 불가능 -> 조인으로 풀어서 해결해야함
    • ->그거안되면 쿼리 두번 날리기. 그것도 안되면 native 

 

JPQL 타입 표현과 기타식

  • ‘She’’s’
  • ENUM: jpabook.MemberType.Admin (패키지명 포함)
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)

ENUM은 바인딩해서 사용

SQL과 거의 비슷하다..

조건식(CASE 등등)

기본 case는 이렇게 쓴다.

    • COALESCE: 하나씩 조회해서 null이 아니면 반환
    • NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환

JPQL 함수

  • 기본함수
    • CONCAT: 문자열 두개 더하기
      • select 'a' || 'b' From Member m
      • select concat('a','b') From Member m
    • SUBSTRING: 잘라내기
      • select substring(m.username,2,3 ) From Member m
    • TRIM: 공백제거
    • LOWER, UPPER
    • LENGTH
    • LOCATE: 문자 위치 찾아줌
      • select locate('de','abcdefg') From Member m
    • ABS, SQRT, MOD
    • SIZE, INDEX(JPA 용도): 특이.
      • SIZE는 컬렉션의 크기를 반환
      • INDEX는 @OrderColumn에서 위치값 구할때!
  • 사용자 정의 함수 호출
    • 하이버네이트는 사용전 방언에 추가해야 한다.
      • 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
      • select function('group_concat', i.name) from Item i

반응형
profile

보글보글 개발일지

@보글

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!