본문 바로가기

STUDY/JPA

[자바 ORM 표준 JPA 프로그래밍 - 기본편] 객체지향쿼리언어1 - 기본문법

인프런 - 자 ORM 표준 JPA 프로그래밍 (김영한님) 

 

객체지향 쿼리 언어 소개

jpa는 다양한 쿼리 방법을 지원

  • JPQL
  • JPA Criteria
  • QueryDSL
  • Native SQL
  • JDBC API 직접 사용, Mybatis, SpringJdbcTemplate 함께 사용

 

 

JPQL 소개

  • JPA를 사용하면 엔티티 객체를 중심으로 개발
  • 문제는 검색 쿼리
  • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
    • WHERE, GROUP BY 등
  • JPA는 SQL을 추상화환 JPQL이라는 객체 지향 쿼리 언어를 제공
  • SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
  • JPQL은 엔티티 객체를 대상으로 쿼리
    • SQL은 데이터베이스 테이블을 대상으로 쿼리

 

JPQL 조회 예제

List<Member> members = entityManager.createQuery(
	"select m from Member m where m.username like '%kim%'",
	Member.class
).getResultList();

for (Member member : members) {
	System.out.println("Username = " + member.getUsername());
}

// 실행 시 아래 쿼리가 나감
Hibernate: 
    /* select
        m 
    from
        Member m 
    where
        m.username like '%kim%' */ 
		select
            member0_.MEMBER_ID as MEMBER_I1_2_,
            member0_.city as city2_2_,
            member0_.street as street3_2_,
            member0_.zipcode as zipcode4_2_,
            member0_.USERNAME as USERNAME5_2_ 
        from
            Member member0_ 
        where
            member0_.USERNAME like '%kim%'

 

 

Criteria 소개

  • JPQL은 단순 String으로 조회
    • 동적 쿼리 생성이 매우 어려움
  • Criteria는 문자가 아닌 자바코드로 작성하기 때문에 동적 쿼리 사용이 쉬운 편
    • 컴파일 시 오류 잡을 수 있음
  • 너무 복잡하고 유지보수 하기 어려움
  • QueryDSL 사용 권장
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Member> query = builder.createQuery(Member.class);

Root<Member> member = query.from(Member.class);
CriteriaQuery<Member> memberCriteriaQuery = query.select(member).where(builder.equal(member.get("username"), "kim"));
entityManager.createQuery(memberCriteriaQuery).getResultList();

 

 

QueryDSL 소개

  • 문자가 아닌 자바 코드로 작성
    • 컴파일 시 문법 오류를 찾을 수 있음
  • 동적 쿼리 사용 편리
  • 단순하고 쉬움
  • 실무 사용 권장
QMember member = QMember.member;
List<Member> result = queryFactory
	.select(member)
	.from(member)
	.where(member.name.equal("kim"))
	.fetch();

 

 

Native SQL 소개

  • JPA가 제공하는 SQL을 직접 사용하는 기능
  • JPQL로 해결할 수 없는 특정 DB에 의존적인 기능
    • Oracle CONNECT BY 등
entityManager.createNativeQuery(
	"select MEMBER_ID, city from MEMBER"
).getResultList();

 

 

JDBC 직접 사용, SpringJdbcTemplate 등

  • JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, SpringJdbcTemplate, MyBatis 등을 함께 사용
  • 단, 영속성 컨텍스트를 적절한 시점에 강제로 Flush 필요
    • JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트에 수동 플러시 필요

 

 

JPQL(Java Persistence Query Language) - 기본 문법과 기능

JQPL 문법

// SELECT
select
from
[where]
[groupby]
[having]
[orderby]

// update
update [where]

// delete
delete [where]
  • 엔티티와 속성은 대소문자 구분한다 (Member, age)
  • JPQL 키워드는 대소문자 구분하지 않는다 (SELECT, FROM, WHERE ...)
  • 엔티티 이름 사용 (테이블 이름 X)
  • 별칭은 필수 (m) (as는 생략 가능)

 

 

 

집합과 정렬

select
	COUNT
	SUM
	AVG
	MAX
	MIN
from 

 

 

 

TypedQuery, Query

  • TypedQuery : 반환 타입이 명확할 때 사용
  • Query : 반환 타입이 명확하지 않을 때 사용
// Member 타입
TypedQuery<Member> typedQuery = entityManager.createQuery(
	"select m from Member m", Member.class
).getResultList();

// username은 String, age는 int
Query query = entityManager.createQuery(
	"select m.username, m.age from Member m"
);

 

 

 

결과 조회 API

  • query.getResultList() : 결과가 하나 이상일 때 리스트 반환
    • 결과가 없으면 빈 리스트 반환
    • Exception 발생하지 않음
  • query.getSingleResult() : 결과가 정확히 하나일 때 단일 객체 반환
    • 결과가 없으면 javax.persistence.NoResultException
    • 둘 이상이면 javax.persistence.NonUniqueResultException

 

 

파라미터 바인딩 - 이름 기준, 위치 기준

  • :username
  • ?1
Member result = entityManager.createQuery(
	"select m from Member m where m.username = :username", Member.class
)
    .setParameter("username", "member1")
    .getSingleResult();
System.out.println(result.getUsername());

Member result = entityManager.createQuery(
	"select m from Member m where m.username = ?1", Member.class
)
    .setParameter(1, "member1")
    .getSingleResult();
System.out.println(result.getUsername());

// member1

 

 

 

프로젝션(SELECT)

  • SELECT 절에 조회할 대상을 지정하는 것
  • 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)
  • SELECT m FROM Member m : 엔티티
    • 영속성 컨텍스트에 관리
  • SELECT m.team FROM Member m : 엔티티
  • SELECT m.address FROM Member m : 임베디드 타입

 

@Embeddable
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    private String city;
    private String street;
    private String zipcode;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
    }

    @Override
    public int hashCode() {
        return Objects.hash(city, street, zipcode);
    }
}

 

 

  • SELECT m.username, m.age FROM Member m : 스칼라 타입
  • DISTINCT로 중복 제거

 

 

프로젝션 - 여러 값 조회

  • SELECT m.username, m.age FROM Member m

1. Query 타입으로 조회

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

Object o = results.get(0);
Object[] result = (Object[]) o;

System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

 

2. Object[] 타입으로 조회

List<Obejct[]> results = em.createQuery("select m.username, m.age from Member m", Member.class).getResultList();

Object[] result = results.get(0);

System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

 

3. new 명령어로 조회 (DTO 사용)

List<MemberDTO> results = em.createQuery("select new MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList();

MemberDTO memberDTO = results.get(0);
System.out.println("username = " + memberDTO.getUsername());
System.out.println("age = " + memberDTO.getAge());

 

 

페이징

  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
for (int i = 0; i < 100; i++) {
	Member member = new Member();
    member.setUsername("member" + i);
    entityManager.persist(member);
}

entityManager.flush();
entityManager.clear();

List<Member> results = entityManager.createQuery("select m from Member m order by m.id desc", Member.class)
	.setFirstResult(1)
    .setMaxResults(10)
    .getResultList();

System.out.println("results.size() = " + results.size());
for(Member members : results) {
	System.out.println(members.getId() + " : " + members.getUsername());
}

transaction.commit();

//===========results===========
Hibernate: 
    /* select
        m 
    from
        Member m 
    order by
        m.id desc */ select
            member0_.MEMBER_ID as MEMBER_I1_2_,
            member0_.city as city2_2_,
            member0_.street as street3_2_,
            member0_.zipcode as zipcode4_2_,
            member0_.USERNAME as USERNAME5_2_ 
        from
            Member member0_ 
        order by
            member0_.MEMBER_ID desc limit ? offset ?
results.size() = 10
99 : member98
98 : member97
97 : member96
96 : member95
95 : member94
94 : member93
93 : member92
92 : member91
91 : member90
90 : member89
 
 
 

조인

  • 내부 조인
    • SELECT m FROM Member m [INNER] JOIN m.team t

 

// Member.java
@Entity
@Getter
@Setter
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

// Team.java
@Entity
@Getter
@Setter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

// main.java
Team team = new Team();
team.setName("TeamA");
entityManager.persist(team);

Member member = new Member();
member.setUsername("Member1");
member.setTeam(team);

entityManager.persist(member);

entityManager.flush();
entityManager.clear();

String sql = "select m from Member m inner join m.team t";
List<Member> results = entityManager.createQuery(sql, Member.class)
    .getResultList();

 

 

  • 외부 조인
    • SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
  • 세타 조인
    • SELECT count(m) FROM Member m, Team t WHERE m.username = t.name
    • 연관 관계가 없을 때 비교
    • cross join으로 쿼리가 나감

 

 

조인 - ON 절

  • JPA 2.1 부터 지원
  1. 조인 대상 필터링
  2. 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터 지원)

 

 

 

조인 대상 필터링

  • ex) 회원과 팀을 조인하면서, 팀 이름이 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'

 

 

 

연관관계 없는 엔티티 외부 조인

  • ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
  • JPQL
    • 아무 연관 관계가 없어도 LEFT JOIN 가능
    • 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)

 

*서브쿼리에서 m을 사용하지 않음

 

  • 한 건이라도 주문한 고객
SELECT m
FROM Member m
WHERE 
	(SELECT COUNT(o) 
	 FROM Order o
	 WHERE m = o.member) > 0

 

*서브쿼리에서 m을 사용 -> 성능이 별로 안나옴

 

 

 

서브쿼리 지원 함수

  • [NOT] EXISTS : 서브쿼리에 결과가 존재하면 참
    • {ALL | ANY | SOME}
    • ALL 모두 만족하면 참
    • ANY, SOME 조건을 하나라도 만족하면 참
  • [NOT] IN : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

 

 

 

서브쿼리 - 예제

  • 팀 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 서브 쿼리 한계

  • WHERE, HAVING 절에서만 서브 쿼리 가능 (JPA에서 지원)
  • SELECT 절도 가능 (하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
    • 조인으로 풀 수 있으면 풀어서 해결
    • 쿼리를 분해해서 2번 날리거나 Application에서 결과를 가져와 조립하거나

 

 

 

 

JPQL 타입 표현과 기타식

JPQL 타입 표현

  • 문자
    • 'HELLO', 'She''s'
  • 숫자
    • 10L(Long), 10D(Double), 10F(Float)
  • Boolean
    • TRUE, FALSE
  • ENUM
    • 패키지명 포함
    • e.g) where jpa-basic.MemberType.ADMIN
    • Parameter로 주로 사용
    • QueryDsl 사용하면 import 됨
  • Entity
    • TYPE(m) = Member (상속관계에서 사용)
    • DiscriminatorColumn, DiscriminatorValue (DTYPE)
String sql = "select i from Item i where type(i) = Book";

 

 

 

JPQL 기타

  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL
 
 
 
 

조건식 (CASE 등)

조건식 - CASE 식

  • 기본 CASE 식
SELECT
	CASE WHEN m.age <= 10 then '학생요금'
		 WHEN m.age >= 60 then '경로요금'
		 ELSE '일반요금'
    END
FROM Member m

 

  • 단순 CASE 식
SELECT
	CASE t.name
		WHEN 'A' then '110%'
		WHEN 'B' then '120%'
		ELSE '105%'
    END
FROM Team t

 

 

  • COALESCE : 하나씩 조회해서 null이 아니면 반환
    • m.username을 하나씩 조회해서 null(사용자 이름이 없음)이면 '이름 없는 회원'으로 return
    • 사용자 이름이 없으면 이름 없는 회원을 반환
SELECT COALESCE(m.username, '이름 없는 회원') FROM Member M

 

 

  • NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
    • 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
SELECT NULLIF(m.username, '관리자') FROM Member m
 
 
 

JPQL 기본 함수

  • CONCAT
SELECT CONCAT('a', 'b') FROM Member m
SELECT 'a' || 'b' FROM Member m

 

  • SUBSTRING
SELECT SUBSTRING(m.username, 2, 3) FROM Member m

 

  • TRIM
  • LOWER, UPPER
  • LENGTH
  • LOCATE
String sql = "SELECT LOCATE('de','abcdefg') FROM Member m";
Integer result = em.createQuery(sql, Integer.class).getResultList();
// 결과 
4

 

  • ABS, SQRT, MOD
  • SIZE, INDEX(JPA 용도)

 

 

 

사용자 정의 함수

  • 하이버네이트 사용 전 DB 방언에 저장해야 함
    • Dialect를 상속받아 새로 생성
public class MyH2Dialect extends H2Dialect {
	public MyH2Dialect() {
		registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
	}
}
// persistence.xml
...
<property name="hibername.dialect" value="dialect.MyH2Dialect"/>
String sql = "SELECT FUNCTION('group_concat', m.username) FROM Member m";