본문 바로가기
Spring/JPA

JPA 4 - 값 타입, JPQL

by 구너드 2023. 7. 3.

JPA의 데이터 타입 분류

 

엔티티 타입 - 데이터가 변해도 지속적으로 추적이 가능

 

값 타입- 값만 존재하므로 변경시 추적 불가

기본값 타입 - int,double과 같은 자바 기본 타입 / Integer, Long과 같은 래퍼 클래스 / String

임베디드 타입 - 복합 값 타입

컬렉션 값 타입


기본값 타입

생명주기를 엔티티에 의존, 엔티티가 삭제되면 해당 엔티티에 속해있는 기본값 타입들도 삭제

공유 X ex) 특정 엔티티의 기본값 타입의 변경이 다른 엔티티의 기본값 타입의 변경으로 부수효과 발생 가능성

 

자바의 기본 타입은 공유되지 않음 

int a = 20;
int b = a;

a = 50;

System.out.println("a = " + a)
System.out.println("b = " + b)

a = 50
b = 20

 

그러나 Integer와 같은 래퍼클래스는 값이 아닌 객체의 레퍼런스를 가리키기 때문에 공유가 가능하지만 해당 값을 변경하게 될 시 다른 값들도 변경되므로 변경 자체를 할 수 없게 만듬


임베디드 타입

새로운 값 타입을 직접 정의할 수 있음

주로 기본값 타입을 모아서 만들기 때문에 복합 값 타입이라고도 함

public calss Member {

    private Long id;
    private String name;
    
    @Embedded
    private Address homeaddress;
}


@Embeddable
@NoArgsConstructor
public class Address {
    private String city;
    private Stirng street;
    private String zipcode;
    
    public String fullAdress() {
    return city + street + zipcode;
}

Member의 Address 필드를 임베디드 타입으로 새롭게 정의.

임베디드 타입은 기본 생성자가 필수

 

장점 - 재사용성, 비슷한 속성의 높은 응집도. 해당 값 타입을 이용하여 의미 있는 메소드 생성 가능, 의존하는 엔티티가 생명주기를 관리

 

임베디드의 타입은 엔티티의 값일 뿐 사용하기 전과 사용한 후의 매핑하는 테이블은 동일. 대신 객체와 테이블을 세밀하게 매핑하는 것이 가능.

 

한 엔티티에서 같은 값 타입을 사용하게 되면 컬럼 값이 중복되는데, 이 때 @AttributeOverride를 사용해서 컬럼명을 재정의하여 중복하여 사용할 수 있음


Address address = new Address("city", "street", "11111")

Member member1 = new Member();
member.setUsername("mamber 1");
member.setHomeAddress(address);
em.persist(member1);

Member member2 = new Member();
member.setUsername("mamber 2");
member.setHomeAddress(address);
em.persist(member2);

member1.getHomeAddress.setCity("new City");

의도한 것은 member1의 address 중 city만 new City로 바꾸고 싶었으나, 실행된 결과는 member2의 city도 new City로 바뀌어서 저장

-> 해당 member1,member2가 address를 공유하기 때문에 발생하는 부수효과

만약 해당 address를 공유해서 쓰고자 하면 임베디드 타입이 아닌 엔티티로 생성해야

 

Address address = new Address("city", "street", "11111")

Member member1 = new Member();
member.setUsername("mamber 1");
member.setHomeAddress(address);
em.persist(member1);

Address copyAddress = new Address(address.getCity(), address.getStreet(), address.getZipcode());

Member member2 = new Member();
member.setUsername("mamber 2");
member.setHomeAddress(copyAdress);
em.persist(member2);

member1.getHomeAddress.setCity("new City");

임베디드 타입 같은 값 타입을 여러 엔티티에서 공유하면 공유참조의 부수효과 위험성이 발생할 수 있음. 따라서 해당 값 타입을 공유해서 사용하는 게 아닌 인스턴스를 복사해서 사용하는 것이 필요

위와 같이 값을 복사해서 엔티티에 해당 값들을 설정하게 되면 member1의 homeAddress의 CIty를 new City로 변경해도 member2는 copyAddress이기 때문에 영향이 없

 

항상 값을 복사해서 사용하면 공유 참조로인해 발생하는 부수효과는 피할 수 있음

그러나 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본 타입(primitive 타입)이 아니라 객체 타입

-> primitive 타입은 값을 자동으로 복사해서 넘어가지만, 객체 타입은 그렇지 않음.

자바 기본 타입에 값을 대입하면 값을 복사하지만 객체 타입은 값 자체를 복사하는 것이 아닌 해당 객체의 레퍼런스 값을 복사하기 때문에 이를 직접 대입하면 공유참조가 발생할 가능성을  막을 방법이 없음

따라서 임베디드 타입은 Setter를 설정하지 않는 방법등을 사용해서 불변객체로 만들 필요가 있음, 생성자로만 값을 설정한 뒤, 해당 객체들의 값을 직접 복사하여 복사된 인스턴스를 대입해야 

 

값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야함.

값 타입의 equals() 메소드를 적절하게 재정의해야 함


값 타입 컬렉션

 

값 타입을 하나 이상 저장할 때 사용

@ElementCollection, @CollectionTable 사용

컬렉션을 저장하기 위해서는 별도의 다른 테이블을 생성해야함, 지연로딩 전략 사용

값 타입 컬렉션은 영속성 전이(Cascade)와 고아객체 제거 기능을 필수로 가짐


JPQL

 

JPA는 엔티티 객체를 중심으로 개발. 이러한 점은 검색 쿼리에서 테이블이 아닌 엔티티 객체를 대상으로 검색. 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능. 즉 애플리케이션이 필요한 데이터만 DB에서 불러오려면 검색 조건이 포함된 SQL이 필요

 

JPA 는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공.

JPQL은 엔티티 객체를 대상으로 쿼리, SQL은 DB테이블을 대상으로 쿼리

 

select m from Member as m where m.age > 18

엔티티와 속성은 대소문자 구분 O

JPQL 키워드는 대소문자 구분 X

테이블이름이 아닌 엔티티 이름 사용

별칭은 필수(m)

기존 SQL 문과 매우 흡사하다는 점


TypedQuery<Member> query = em.createQuery("Select m from Member m", Member.class);

Query query = em.createQuery("Select m.username, m.age from Member m");

위의 코드에서처럼 반환 타입이 Member로 명확할 때에는 TypedQuery를 사용하지만,

String 타입의 m.username과 Integer 타입의 m.age을 같이 반환하여 반환타입이 명확하지 않을 때에는 Query를 사용함

 

query.getResult() - 결과가 하나 이상일 때, 리스트 반환. 만약 결과가 없다면 빈 리스트 반환

query.getSingleResult() - 결과가 정확히 하나일 때, 단일 객체 반환 ( 결과가 없다면 NoResultException, 결과가 둘 이상이면 NonUniqueResultExceptino 발생) 따라서 얻고자 하는 결과가 정말 하나일 때만 사용해야함.

 

Select m from Member m where m.username=:username

query.setParameter("username",해당 username)

파라미터 바인딩 방법


프로젝션 

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 // 스칼라 타입 프로젝션

Select 절에 조회할 대상을 지정하는 것

대상 :엔티티, 임베디드 타입, 스칼라타입(숫자,문자 등 기본 데이터 타입)

임베디드 타입은 엔티티에 의존하기 때문 해당 임베디드 타입을 가지고 있는 엔티티를 통해서 찾을 수만 있음(값 타입의 한계)

 

List<MemberDto> result = em.createQuery(Select new 파일경로.파일경로.MemberDto(m.username, m.age) from Member m, MemberDto.class)
                           .getResultList();
                           
MemberDto memberDto = result.get(0)
memberDto.getUsername();
memberDto.getAge();

스칼라타입으로 반환받는 반환타입이 두 개 이상일 때 쓰는 가장 간편한 방법 (new 명령어)

이외에도 Query, Object[]이 있음


페이징

JPA 는 페이징을 setFristResult(int startPosition), setMaxResults(int maxResult)로 추상화

String jpql = "Select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql,Member.calss)
        .setFirstResult(10)
        .setMaxResult(20)
        .getResultList();

setFristResult(int startPosition) - 조회 시작 위치

 setMaxResults(int maxResult) - 조회할 데이터 수

SQL의 Limit과 흡사


조인

엔티티를 중심으로 동작하기 때문에 객체스타일로 join이 작성됨.

select m from Member m join team t가 아닌

select m from Member m join m.team t와 같이 Member 엔티티를 중심으로 join 하고자 하는 테이블로 작성

 

내부 조인(inner join) /inner 생략 가

Select m from Member m Join m.team t

 

외부 조인(left join)

Select m from Member m left join m.team t

 

On 절

조인 대상 필터링, 하이버네이트 5.1부터 연관관계가 없는 엔티티 외부조인도 가능해짐(세타 조인과 비슷)

 

Select m,t from Member m left join m.team t on t.name = 'A'

회원과 팀을 가져오기

회원과 팀을 left join 하면서

팀 이름이 A인 조건만

실제 SQL

Select m . * , t . *

from Member m

left join Team t  on m.Team_ID = t.id and t.name = '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

한 건이라도 주문한 고객

 

중요한 점은 일반적인 SQL의 서브쿼리가 지원됨

 

 

Select m from Member m where Exists(Select t frmo m.team t where t.name = '팀A')

Exists(subquery) - 서브쿼리에 결과가 존재하면 참(팀A 라는 이름의 팀에 소속한 회원 데이터 가져오)

 

Select o from Order o where o.orderAmount > All(Select p.stockAmount from Product p)

All(subquery) - 모두 만족하면 참(전체 상품 테이블에서 가져온 각 상품의 잔여수량보다 주문량이 더 많은 주문 데이터 가져오기)

 

Select m from Member m where m.team = Any(Select t from Team t)

(Any or Some) (subquery) - 조건을 하나라도 만족하면 참(어떤 팀이든 팀에 소속되어 있는 회원 데이터 가져오기)

 

In (subquery) - 서브쿼리의 결과 중 하나라도 같은 것이 있다면 참

 

하이버네이트 6부터는 From절의 서브쿼리를 지원 가능( select, from, where, having 모두 사용 가능)

 

대부분 SQL과 비슷한 형식을 가지고 있기에 JQPL 기본 문법들에 대해서 SQL을 학습하는 방향으로