객체지향 쿼리 언어

원종서·2022년 2월 12일
1

JPA

목록 보기
9/13

JPQL : Java Persistence Query Language

JPQL의 특징
1. 엔티티 객체를 조회하는 객체지향 쿼리.
2. SQL 을 추상화해서 특정 데베에 의존하지 않음.

JPQL을 잘 분석한 다음 SQL을 만들어 데베를 조회하고, 그 결과로 엔티티 객체를 생성해서 반환한다.

@Entity
public class Member {
	@Id
    Long id;
    
    @Column(name ="name")
    String username;


String jpql = "select m from Member as m where m.username = 'kim";

em.createQuery(jpql, Member.class).getResultList();

여기서 Member는 엔티티 이름이다.
m.username은 테이블 컬럼명이 아니라 객체의 필드명이다.

createQuery()은 jpql 과 반환한 엔티티의 타입을 매개변수로 받는다 .

getResultList() 를 실행하면 JPA 는 JPQL을 SQL로 변환 후 데베를 조회.
그후 조회한 결과로 Member 엔티티를 생성해서 반환함.

Criteria 쿼리

JPQL 을 생성하는 빌더 클래스.

장점
1 컴파일 시 오류 발견 가능
2. 코드 자동완성 지원
3. 동적쿼리 작성 편함

  1. 네이티브 쿼리

네이티브 쿼리는 특정 데이터베이스에 종속되는 단점이 있다.
String sql = "select * from member where name = 'kim'";
List result = em.createNativeQuery(sql,Member.class).getResultList();

1 JPQL

엔티티와 속성은 대소문자 구분한다.
별칭은 필수 - Select username from Member m // error

TypeQuery, Query

JPQL을 실행하라면 쿼리 객체를 만들어야한다.
쿼리 객체는 TypeQuery, Query 두개가 존재한다.
반환할 타입이 명확하면 TypeQuery, 명확하지 않으면 Query를 사용한다.

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

List<Member> members = query.getResult();
  

createQuery() 의 두번째 메소드에 클래스 명을 지정하면 TypedQuery 타입을 반환하고, 지정하지 않으면 Query 을 반환한다.


Query query = em.createQuery("select m.username, m.age from Member m ");
List results = query.getResultList();

for(Object o :results) {
    Object[] result = (Object[]) o;
    System.out.println("username = " + result[0]);
    System.out.println("age = " + age[0]);
}
  

이처럼 select 절에서 여러 엔티티나 컬럼을 선택 할 시 조회 대상 타입이 명확하지 않음으로 Query 객체를 사용함
Query 객체는 select 절의조회 대상이 예제처럼 둘 이상이면 Object[] 를 반환하고, 조회 대상이 하나면 Object를 만환한다.

결과 조회

  • query.getResultList() | 결과를 예제로 반환함. 결과가 없으면 빈 컬렉션
    • query.getSingleResult() | 결과가 정확히 하나일 때
      결과가 없거나 2개 이상이면 에러 발생 (NoResultException, NonUniqueResultException)

파라미터 바인딩

이름 기준 파리미터와 위치 기준 파라미터 두개를 지원한다.

  • 이름기준 파라미터
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username = :username", Member.class).setParameter("username", "User1");

:username 이라는 이름 기준 파라미터 정의하고 setParameter() 메소드를 이용해 username이라는 이름으로 파라미터를 바인딩한다.

  • 위치 기준 파라미터

em.createQuery("select m from Member m where m.username = ?1", Member.class).setParameter(1, "User1");
위치값은 1부터 시작한다.

Projection

select 절에서 조회할 대상을 지정하는 것을 프로텍션이라 한다.
[SELECT {프로젝션 대상} FROM ]
프로텍션의 대상은 엔티티, 임베디드타입, 스칼라 타입이있다.

엔티티 프로젝션

SELECT m FROM Member m;  // 멤버 조회
SELECT m.team FROM Member m; // 팀 조회

이렇게 조회한 엔티티들은 영속성 컨텍스트에서 관리된다.

임베디드 타입 프로텍션

임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.

select a FROM Address a ; // error ,Addres 임베디드 타입

SELECT m.address FROM Member m ; // 엔티티를 통해 임베디드 타입을 조회할 수 있다.

위처럼 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리하지 않는다. (임베디드 타입은 값 타입이기 때문에)

스칼라 타입 프로젝션

숫자 문제 날짜 같은 기본 데이터 타입들을 스칼라 타입.
전체 회원의 이름을 조회하려면

List<String> usernames = em.createQuery("SELECT username FROM Member m ", String class).getResultList();

중복을 제거하려면

 SELECT DISTINCT username FROM Member m  를 사용한다.

여러 값 조회

Query q = em.createQuery("select m.username, m.age From Member m");
List resultList = q.getResultList();

Iterator iterator = resultList.iterator();

while(iterator.hasNext()){
    Object[] row = (Object[]) iterator.next();
    String username = (String) row[0];
    Integer age = (Integer) row[1];
}

제네릭에 Object[] 를 사용하면 더 간단히 사용 가능


 List <Object[]> resultList =  em.createQuery("select o.member , o.product, o.orderAmount From Order o")
  .getResultList();

for(Object[] o : resultList ) {
    Member member = (Member) o[0];
    Product p = (Product) o[1];
    int orderAmount = (Integer) o[2];
}

New 명령어

단 위처럼 오브젝트[] 를 직접 사용하지 않고 UserDTO 처럼 의미있는 객체로 변환해서 사용한다.

public class UserDTO {
  private String username;
  private int age;

  // AllArgumentStructor();
}

-> New 명령어 사용 후
TypedQuery<UserDTO> query= em.createQuery("SELECT new {full-package-name } + .UserDTO(m.username, m.age) FROM Member m ", UserDTO.class);

List<UserDTO> list = query.getResultList();

select 다음 new 명령어를 사용하면 반환받을 클래스를 지정할 수 있다. 이 클래스 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.

주의점
1. 패키지 명을 포함한 전체 클래스명 입력.
2. 순서 타입과 일치하는 생성자 필요

페이징 API

TypedQuery<Member> q = em.createQuery("Select m FROM Member m ORDER BY m.username DESC", Member.class);
q.setFirstResult(10); // 조회 시작 위치 ( 10부터 , 기본값은 0)
q.setMaxResults(20); // 조회할 데이터 수
// 따라서 11부터 30까지 검색

집합과 정렬

집합은 집합함수와 함께 통계 정보를 구할 때 사용.


SELECT 
	COUNT(m),
	SUM(m.age),
  AVG(m.age),
  MAX(m.age),
	MIN(m.age)
FROM MEMBER m

집합함수
COUNT
= 결과 수 반환, 반환타입 Long
MAX, MIN
= 최대, 최소를 구함 , 문자, 숫 , 숫자 날짜 등에 사용
AVG
= 평균 , 숫자 타입에만 사용 가능 . 반환타입 Double
SUM
= 합 구함, 숫자 타입에만 사용 가능. 반환타입 Long, Double, BigInteger, BigDecimal

  • 집합함수 참고사항
    1. null 값은 무시함으로 통계에 잡히지 않음 (DISTINCT가 정의되어 있어도 무시됨.)
    2. 값이 없는데 SUM, AVG, MAX, MIN 함수 시 null 단 COUNT는 0;
    1. DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고 나서 집합 구하기 가능
    ex) select  count (DISTINCT m.age) from Member m
    1. DISTINCT를 COUNT 에서 사용할 떄 임베디드 타입은 지원하지 않는다.

GROUP BY ,HAVING

통계 데이터를 구할 떄 특정 그룹끼리 묶는다.

팀 이름을 기준으로 그룹별로 묶어서 통계 데이터 구함

SELECT t.name , COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
    FROM Member m LEFT JOIN m.team t
    GROUP BY t.name

HAVING은 GROUP BY와 같이 사용, GROUP BY로 그룹한 통계 데이터를 기준으로 필터링

평균 나이가 10살 이상인 그룹

SELECT t.name , COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
  FROM Member m LEFT JOIN m.team t
  GROUP BY t.name
HAVING AVG(m.age) >= 10

정렬

select m from Member m ORDER BY m.age DESC, m.username ASC

JPQL 조인!!

내부조인.

List<Member> members  = em.createQuery("SELECT m FROM Member m INNER JOIN m.team t WHERE t.name = :name ")
                          .setParameter("name","Team1)
                          .getResultList();

JPQL 조인은 연관 필드를 사용한다. m.team 이 연관필드

'팀A 소속인 회원을 나이 내림차순으로 정렬하고 회원명과 팀 명을 조회'
SELECT t.name , m.username
    FROM Member m JOIN m.team t
    where t.name = "팀A"
    ORDER BY m.age DESC;

서로 다른 타입의 두 엔티티를 조회했음으로 TypeQuery 사용 불가능하다.

Query q = em.createQuery("SELECT m ,t  FROM Member m JOIN m.team t");
List<Object[]> result = q.getResultList();

for(Object[] row : result) {
  Member member = (Member) row[0];
  Team team = (Team) row[1];
}

외부조인

SELECT m
    FROM Member m
    LEFT (OUTER) JOIN m.team t

sql과 같음

컬렉션 조인

일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인 하는 것을 컬렉션 조인 이라함.

회원->팀 으로의 조인은 다대일 조인이면서 단일 값 연관 필드를 사용함
팀->회원 으로의 조인은 일대다 조인이면서 컬렉션 값 연관 필드를 사용함.

SELECT t, m FROM Team t LEFT JOIN t.members m;

팀과 팀이 보유한 회원목록을 컬렉션 값 연관 필드로 외부조인함.

세타조인

세타조인은 내부조인만 지원한다.
전혀 관계 없는 엔티티도 조인할 수 있다.

select count(m) from Member m , Team t
    where m.username = t.name

//SQL
SELECT COUNT(M.id)
FROM
    MEMBER M CROSS JOIN T
WHERE
    M.USERNAME = T.NAME

JOIN ON

ON절을 사용하면 조인 대상을 필터링 하고 조인할 수 있다. 결과 값이 WHERE 절을 사용할때와 같으므로 보통 ON 절은 외부조인 시에 사용한다.

모든 회원을 조회하면서 회원과 연관된 팀도 조회 , 팀이름은 A
SELECT m, t FROM Member m LEFT JOIN m.team t ON t.name ='A'
  
  //sql
  SELECT m.*, t.* FROM MEMBER m 
  LEFT JOIN t ON m.TEAM_ID = t.id and t.name='A'

SQL 을 보면 and t.name='A' 로 조인 시점에 조인 대상을 필터링하낟.

페치조인

연관된 엔티티나 컬렉션을 한번에 같이 조회
JPQL에서 성능 최적화를 위해 제공하는 기능
연관된 엔티티나, 컬렉션을 한번에 같이 조회한다.

join fetch 명렁어로 사용

[ LEFT [OUTER] | INNER] JOIN FETCH 조인경로

엔티티 페치 조인

 SELECT m FROM Member m  JOIN FETCH m.team

일반적이 조인과 차이점은 m.team 에 별칭이 없다. 페치 조인은 별칭을 사용할 수 없다.

``sql
-- 위의 SQL
SELECT M., T.
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID = T.ID


 페치 조인에서 멤버만 조회했는데 검색 결과로 팀까지 반환된 것을 볼 수 있다. 그래프 조회까지 가능하다.
```java
 List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();
  
  for(Member member : members){
  	System.out.println("member.getUsername() + " " +member.getTeam().getName());
  }

회원과 팀을 지연 로딩으로 설정해도. 조회할 때 페치조인을 사용해서 팀도 함께 조회했음으로 프록시가 아닌 실제 객체가 반환된다.

member.getTeam().getName() 을 사용 할 수 있구, 만일 엔티티 정의 떄 지연로딩으로 설정한다고 해도 , 패치 조인은 한번에 연관된 객체까지 가져옴으로 프록시가 아닌 실제 연관관계 엔티티가 된다

컬렉션 페치 조인

SELECT t FROM Team t
  JOIN FETCH t.members
  WHERE t.name = '팀A';

SELECT
  T.*, M.*
  FROM TEAM T
  INNER JOIN MEMBER M ON T.ID= M.TEAM_ID
  WHERE T.NAME = "팀A"

team1의 members 컬렉션에 (연관된 멤버가) 2개 이상일경우.

단 만일 팀A와 관계를 맺은 멤버의 개수가 2개 이상이면 ,
물리적으로 테이블은 각각의 멤더 행에 팀A를 갖게 된다 (어떻게 보면 당연한 것)
, 엔티티관점으로 바라보면 , 팀A를 주소값을 갖고 있는 객체가 두개이상 존재한다. (왜냐 회원이 둘 이상이기 때문)

teams 결과 리스트 (0x100, 0x100) 두개 존재 -> members (회원1,회원2)

결과 테이블
팀A, 회원1
팀A, 회원2

List<Team> teams = em.createQuery("select t from Team t join fetch t.members where t.name= "A" ",Team.class).getResultList();

for(Team team : teams){
	soutv("team name = + team.getName());

	for(Member member : team.getMembers()){
		soutv("member name = + member.getName());
	}


// 출력결과
/*
   team name = 팀1
   member name = 회원1
   member name = 회원2
   team name = 팀1
   member name = 회원1
   member name = 회원2
*/

페인조인과 DISTINCT

DISTINCT를 사용하면 위에서 팀A의 객체가 두개 이상인 현상을 줄일 수 있다.
DISTINCT사용하면 애플리케이션에서 한번 더 중복 제거한다.

SELECT DISTINCT t FROM Team t JOIN FETCH t.members
WHERE t.name = '팀A'

즉 팀 엔티티의 중복을 제거하란 것이다.

   team name =1
   member name = 회원1
   member name = 회원2

으로 팀A 의 중복이 사라진다.

일반조인과 패치조인의 차이

 select t from Team t join t.members m where t.name ='teamA' 

로 일반 패치를 사용할 경우 회원 컬렉션도 함께 조인하는 것을 기대하면 안된다.
JPQL은 결과를 반환할때 연관관계까지 고려하지 않는다. 단지 셀렉트 절에 지정한 엔티티만 조회한다,.

회원 컬렉션을 지연로딩으로 설정하면 프록시나 컬렉션 래퍼를 반환하고
즉시로딩으로 설정했으면 컬렉션을 로딩하기 위해 하나의 쿼리를 더 날린다.

판면에 패치조인을 하면 조인한 연관된 테이블의 내용도 반환된다.

페치조인의 특징과 한계

SQL 한번으로 연관된 엔티티를 찾을 수 있어서 성능에는 좋다.
그리고 글로벌 패치 전략 보다 우선순위가 높다.

  • 페치 조인 대상에는 별칭 줄 수 없다.
  • 둘 이상의 컬렉션을 페치할 수 없다.
  • 컬렉션을 페치 조인하면 페이징 api를 사용할 수 없다.

경로 표현식

이 뭐냐면 .(점)을 찍어 객체 그래프를 탐색하는 것.

SELECT m.username
    FROM Member m
    join m.team t
    join m.orders o
    where t.name = 'A'

m.username , t.team. m.orders 이 모두 경로 표현한 것이다.'

  • 경로표현식 용어
  1. 상태필드 : 단순히 값을 저장하기 위한 필드 (필드, 프로퍼티)
  2. 연관필드: 연관관계를 위한 필드, 임베디드 타입 포함
    - 단일값 연관필드 :@ManyToOne, @OneToOne
    - 컬렉션 값 연관필드 : @OntToMany, @ManyTOMany
  • 경로표현식과 특징
  1. 상태 필드 경로: 경로 탐색의 끝, 더는 탐색 불가
  2. 단일 값 연관 경로: 묵시적으로 내부조인이 일어남 ,계속 탐색 가능
  3. 컬렉션 값 연관 경로 : 묵시적으로 내부조인 일어남, 더는 탐색 불가 단 From 절에서 별칭을 얻으면 별칭으로 탐색가능

단일값 연관 경로 탐색

select o.member from Order o

//sql
select m.* from Orders o 
inner join Member m on o.member_id = m.id

단일값 연관 필드로 경로 탐색을 하면 sql에서 내부조인이 일어나느데 이를 묵시적 조인(내부조인) 이라고 한다.

  • 명시적 조인 :
    	Select m From member m Join m.team t
  • 묵시적 조인
    	Select m.team From member m
select o.member.team
from Order o
where o.product.name = 'productA' and o.address.city = 'JINJU'

```sql
        //SQL
select t.*
	from Order o
	inner join Member m on o.member_id = m.id
	inner join Team t on m.team_id = t.id
	inner join Product p on o.product_id = p.id
	where p.name = 'productA' and o.city = 'JINJU'

참ㄱ로 o.address 처럼 임베디드 타입에 접근하는 것도 단일 값 연관 탐색이지만, 주문테이블에 이미 포함돼 있기 때문에 따로 조인이 발생하지 않는다.

컬렉션 값 연관 경로탐색

! 주의할점

select t.members from Team t // ok
select t.members.username from Team t // error

t.members 처럼 컬렉션 까지는 경로 탐색이 가는하지만 컬렉션안의 엔티티의 경로를 추가로 탐색하는것은 불가능하다.

select m.username from Team t join t.members m // ok 컬렉션의 새로운 별칭을 획득해야함

컬렉션 에서 경로 탐색을 하고 싶으면 위 같이 join 을 쓰고 새로운 별칭을 획득해야한다.

서브쿼리

서브쿼리는 WHERE, HAVING 에만 사용 가능.

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 // 한건이라도 주문한 회원찾기

// 위와같다
select m from Member m
where m.orders.size > 0

엔티티 직접 사용

기본 키 값

객체 인스턴스는 참조 값으로 식별하고, 테이블 로우는 기본 키 값으로 식별한다.

select count(m.id) from Member m
select count(m) from Member m

두번째 엔티티의 별칭을 카운트 매개변수로 넘겨주었다. 이렇게 엔티티를 직접 사용하면 JPQL이 sql로 바뀔때 해당 엔티티의 기본 키 값을 사용한다.

위의 두 sql문은

select count(m.id) as cnt 
from Member m 

으로 동일한다.

select m from Member m where m := member;
select m from Member m where m.id := memberId;

두 쿼리는 동일하다. 엔티티를 매개변수로 받아도 sql 문에서 엔티티의 기본키를 대상으로 조회를 한다.

select m.* 
from member m 
where m.id = ?

외래 키 값

Team team = em.find(Team.class, 1L);

String qlString = "select m from Member m where m.team =: team";
List result = em.createQuery(qlString)
  				.setPatameter("team",team)
  				.getResultList();

m.team 은 TEAM_ID 라는 외래키에 연관되어 있다.

외래키 역시 기본키와 마찬가지로 m.team 을 사용하든 m.team_id 를 사용하든 같다.

Named 쿼리 : 정적쿼리

동적쿼리 : JPQL 을 문자로 완성해서 직접 넘기는 것,
정적쿼리 : 미리 정의한 쿼리에 이름을 부여해 필요할 때 사용할 수 있는데 이를 Named쿼리라 하며 네임드 쿼리는 정적쿼리다,.

  • 네임드 쿼리 정의
@Entity
@NamedQuery(
	name = "Member.findByUsername",
	query ="select m from Member m where m.username = :username)
)
public class Member {...}

-네임드 쿼리 사용

List<Member> list = em.createNamedQuery
 ("Member.findByUsername",Member.class)
 .setParameter("username", "회원1").getResultList();


           하나 이상 네임드 쿼리 정의
           @NamedQuerys({
               @NamedQuery(name ="A...", query="..."),
               @NamedQuery(name ="B...", query="...")
           })

Criteria

JPQL을 자바 코드로 작성하도록 도와주는 빌드 클래스 API
문법 오류를 컴파일 단계에서 잡을 수 있뜸
동적쿼리 안전하게 작성 가능
단 복작함

기초

 CriteriaBuilder cb = em.getCriteriaBuilder();

 CriteriaQuery<Member> cq = cb.createQuery(Member.class); // 반환타입 지정

 Root<Member> m = cq.from(Member.class); // m을 조회의 시작점이라는 의미로 쿼리 루트라고함
cq.select(m); // 셀렉트 절을 사용함

TypedQuery<Member> quer함 = em.createQuery(cq);

0개의 댓글