반응형
섹션 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 예시]
값 타입과 불변 객체
값 타입은 복잡한 객체 세상을 조금이라도 단순화하려고 만든 개념이다.
따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 한다.
- 값 타입 공유 참조: 임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 위험함. 부작용 발생.
- 객체 타입의 한계
- 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있다.
- 문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입이 아니라 객체 타입이다.
- 자바 기본 타입에 값을 대입하면 값을 복사한다.
- 객체 타입은 참조 값을 직접 대입하는 것을 막을 방법이 없다.
- 결론: 객체의 공유 참조는 피할 수 없다.
- 불변 객체
- 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
- 값 타입은 불변 객체(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(): 결과가 정확히 하나, 단일 객체 반환
- 파라미터 바인딩 - 이름 기준, 위치 기준
프로젝션(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
- 패키지 명을 포함한 전체 클래스 명 입력
- 순서와 타입이 일치하는 생성자 필요
- 단순 값을 DTO로 바로 조회
- SELECT 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
- 연관관계 없는 걸 막 조인하는 것
- 내부 조인: SELECT m FROM Member m [INNER] JOIN m.team t
- 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:
- 연관관계 없는 엔티티 외부 조인:• 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
- 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
- JPQL:
- 조인 대상 필터링: • 예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
서브 쿼리
- 서브 쿼리
- 나이가 평균보다 많은 회원
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): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
- [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
- JPA 서브 쿼리 한계: WHERE, HAVING 절에서만 서브 쿼리 사용 가능.
- SELECT 가능
- FROM 안의 서브쿼리(from (select m.age from Member m) 이런 것) 불가능 -> 조인으로 풀어서 해결해야함
- ->그거안되면 쿼리 두번 날리기. 그것도 안되면 native
JPQL 타입 표현과 기타식
- ‘She’’s’
- ENUM: jpabook.MemberType.Admin (패키지명 포함)
- 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)
SQL과 거의 비슷하다..
조건식(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에서 위치값 구할때!
- CONCAT: 문자열 두개 더하기
- 사용자 정의 함수 호출
- 하이버네이트는 사용전 방언에 추가해야 한다.
- 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
- select function('group_concat', i.name) from Item i
- 하이버네이트는 사용전 방언에 추가해야 한다.
반응형
'코딩기록 > 스프링' 카테고리의 다른 글
MyBatis vs JPA (0) | 2023.05.04 |
---|---|
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 정리2 (0) | 2022.08.07 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 정리1 (0) | 2022.07.25 |
[실전! 스프링 부트와 JPA 활용1] 정리2 (0) | 2022.07.19 |
[실전! 스프링 부트와 JPA 활용1] 정리1 (0) | 2022.07.11 |