Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 코드로 시작하는 자바 첫걸음
- 타임리프 - 기본기능
- 스프링 mvc1 - 스프링 mvc
- 스프링 mvc2 - 타임리프
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch3
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch2
- 게시글 목록 api
- 스프링 db2 - 데이터 접근 기술
- jpa 활용2 - api 개발 고급
- 스프링 mvc2 - 검증
- @Aspect
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch1
- 스프링 입문(무료)
- 자바의 정석 기초편 ch14
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch13
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch12
- 스프링 mvc1 - 서블릿
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch6
Archives
- Today
- Total
나구리의 개발공부기록
객체지향 쿼리 언어 - 기본 문법, 소개, 기본 문법과 쿼리 API, 프로젝션(SELECT), 페이징 본문
인프런 - 스프링부트와 JPA실무 로드맵/자바 ORM 표준 JPA 프로그래밍 - 기본편
객체지향 쿼리 언어 - 기본 문법, 소개, 기본 문법과 쿼리 API, 프로젝션(SELECT), 페이징
소소한나구리 2024. 10. 15. 17:28출처 : 인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편(유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 객체지향 쿼리 언어 소개
1) JPQL
(1) JPA의 한계
- JPA를 사용하면 엔터티 객체를 중심으로 개발하고 테이블은 매핑만 하는데 검색을 할 때도 테이블이 아닌 엔터티 객체를 대상으로 검색을 해야 함
- 그러나 모든 DB데이터를 객체로 변환해서 검색하는 것은 불가능하기 때문에 애플리케이션이 필요한 데이터만 DB에서 불러오려면 검색 조건이 포함된 SQL필요함
(2) 문제 해결
- 이런 문제를 해결하기 위해 JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어를 제공함
- SQL과 문법이 유사하며, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등,, ANSI SQL이 지원하는 문법은 전부 지원함
- SQL은 데이터베이스 테이블을 대상으로 쿼리하는 것이고 JPQL은 엔터티 객체를 대상으로 쿼리하는 것이 가장 큰 차이이며 JPQL로 쿼리를 짜면 SQL로 번역이 되어 실행됨
(3) 사용 방법
- 테이블이아닌 엔터티를 대상으로 쿼리를 작성하며 작성문법이 SQL과 상당히 유사하여 SQL을 다룰줄 안다면 쉽게 적용할 수 있음
- getResultList()메소드로 호출하면 쿼리 결과를 List형태로 받을 수 있으며 해당 값을 반복문으로 꺼내서 활용할 수 있음
List<Member> result = em.createQuery("select m from Member m where m.username like '%kim%'",
Member.class).getResultList();
for (Member member : result) {
System.out.println("member = " + member);
(4) JPQL 정리
- 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이며 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않을 수 있음
- 즉, 객체지향 SQL이라고 이해하면 되며 JPA에서 발생하는 대부분의 문제를 JPQL로 해결할 수 있음
2) Criteria
(1) JPQL의 한계
- JPQL은 대부분의 문제를 해결할 수 있지만 쿼리가 단순히 String 타입이기 때문에 동적 쿼리를 만들기 어려움
- 작성이 불가능한것은 아니지만 여러줄의 쿼리를 입력하게되면 +와 띄어쓰기를 계속 신경써야하기에 버그가 발생할 확률이 매우 높아짐
- ORM은 아니지만 마이바티스(과거의 아이바티스)는 이러한 동적쿼리를 편하게 짤 수 있다는 장점이 존재하는데 JPA에서도 이를 해결하기위해 문법을 개발함
(2) 동적쿼리를 위한 JPA 표준 문법
- 쿼리를 문자가 아닌 자바 코드로 작성하기 때문에 IDE도움을 받을 수 있고 에러를 미리 방지할 수 있는 장점이 있음
- JPQL의 빌더 역할을 하며 JPQL에 비해서 동적쿼리를 작성하기가 편리함
- 실행을 해보면 criteria로 쿼리가 실행되었다는 로그를 확인할 수 있음
(3) 사용 방법
// Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
// 루트 클래스 -> 조회를 시작할 클래스
Root<Member> m = query.from(Member.class);
// 쿼리생성
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
// 결과 반환
List<Member> members = em.createQuery(cq).getResultList();
(4) Criteria 한계
- 그러나 작성된 코드를 보면 개발자에게 익숙한 SQL처럼 구성이 되어있지않아 처음부터 다시 배워야하며 작성된 코드가 어떤 쿼리를 실행하는지 알아보기가 쉽지 않음
- JPA표준이지만 실무에서는 유지보수가 너무 어려워서 사용하지 않는 것을 권장함 -> QueryDSL사용을 권장함
3) QueryDSL
(1) JPA 동적쿼리 대안
- 오픈소스 라이브러리이며 Criteria처럼 자바코드로 JPQL을 작성할 수 있어 IDE도움을 받아 쿼리작성 및 에러 방지를 할 수 있으며 동적쿼리를 매우 편리하게 작성할 수 있으며 자바코드이기 때문에 재사용도 가능함
- 마찬가지로 JPQL의 빌더 역할을 하며 Criteria는 가독성이 좋지 않은 반면에 작성된 코드를 보면 SQL과 흡사하여 단순하고 쉬우며 가독성이 좋음
- 실무에서는 JPA에서 동적쿼리를 작성할 때 QueryDSL를 사용해야함
- QueryDSL은 공식 사이트에서 가이드가 상세히 나와있기 때문에 JPQL만 제대로 알고 있다면 QueryDSL은 매우 쉽게 다룰 수 있음
(2) 사용예시
- QueryDSL을 사용하기위한 설정을 적용한 뒤 EntityManager를 JPAQueryFactory의 생성자에 입력하여 객체를 생성하면 QueryDSL을 사용할 수 있음
- 주석의 JPQL과 QueryDSL로 작성한 쿼리의 구조가 흡사하여 가독성이 좋음
//JPQL
//select m from Member m where m.age > 18
JPAQueryFactory 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을 사용하기 위해서는 세팅이 좀 필요하며, 직접 일일히 세팅하면 조금 번거롭지만 스프링부트를 활용하면 의존성 추가로 쉽게 세팅을 할 수 있어 한번 세팅을 해두면 해당 프로젝트에서 쉽게 동적쿼리를 작성할 수 있음
- 객체지향 쿼리 언어 - 기본문법, 중급문법파트에서는 JPA표준 기술에 대해서 설명하기 때문에 이후에 별도의 강의로 다룸
4) 네이티브 SQL
(1) SQL을 직접 사용
- JPA가 SQL을 직접 사용할 수 있도록 제공하는 기능
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능을 사용하고자 할 때 사용함
- ex) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트 등
- 네이티브 SQL을 사용하지 않아도 하이버네이트 방언 설정 등으로 특정 데이터베이스의 일부 기능을 사용할 수 있기 때문에 사용 빈도가 많지는 않음
(2) 사용방법
- JPQL을 작성하는 방법과 동일한 방법으로 SQL을 직접 작성할 수 있으며 실행하면 dynamic native SQL query로 실행했다는 로그를 확인할 수 있음
// 네이티브 SQL
List<Member> resultList = em.createNativeQuery("select MEMBER_ID, city, street, zipcode, USERNAME from" +
" MEMBER where NAME = 'kim'",
Member.class).getResultList();
for (Member member : resultList) {
System.out.println("member = " + member);
}
5) JDBC 직접 사용, Spring JdbcTemplate 등
- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스 등을 함께 사용할 수 있기 때문에 문제가 발생했을때 해당 기술들을 사용해서 문제를 해결해도 됨
- 주의해야 할 것은 해당 기술들은 JPA와 관련이 없기때문에 DB에서 값을 제대로 조회하기 위해서는 영속성 컨텍스트를 적절한 시점에 강제로 플러시를 해주어야함
- 즉 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시한 뒤에 다른 기술의 SQL로 접근해야 함
** 참고
- JPQL을 잘 알고 있으면 QueryDSL도 배우기 쉽기 때문에 무조건 숙지 해야하며 실무에서 JPA를 사용하며 겪는 대부분의 문제는 JPQL + QueryDSL로 95%정도 해결이 가능함
- 나머지 5%의 정말 복잡한 쿼리는 스프링 JdbcTemplate이나 마이바티스 등으로 네이티브 쿼리로 해결하면 됨
2. 기본 문법과 쿼리 API
1) JPQL(Java Persistence Query Language) - 기본 문법과 기능
- 객체지향 쿼리 언어이며 테이블 대상이 아닌 엔터티를 대상으로 쿼리함
- 특정 SQL에 의존하지 않으며 JPQL은 SQL로 변환되어 작동함
2) 예제 모델
- JPQL예제를 위한 별도의 프로젝트를 생성(JPA, H2 DB설정은 동일하나 url은 jpqltest로 새로 생성하여 접속)
(1) Member와 Team의 양방향 다대일 연관관계 매핑
package hellojpa.jpql;
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// getter, setter
}
package hellojpa.jpql;
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// getter, setter
}
(2) Order와 Product 단방향 다대일 연관관계 매핑 및 Order필드에 Address 값타입 추가
- Order의 테이블명은 예약어때문에 생성이 안되는 경우가 많아 ORDERS로 많이 사용함
package hellojpa.jpql;
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
private int orderAmount;
@Embedded
private Address address;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PRODUCT_ID")
private Product product;
// getter, setter
}
package hellojpa.jpql;
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
private int stockAmount;
// getter, setter
}
package hellojpa.jpql;
@Embeddable
public class Address {
private String city;
private String street;
private String zipCode;
// getter, setter
}
3) JPQL 문법
(1) 기본 구조
- select문: select_절 from절 [where_절] [group by_절] [having_절] [orderby_절]
- update문: update_절 [where_절]
- delete문: delete_절 [where_절]
- select m from Member as m where m.age > 18 처럼 SQL과 동일한 형태의 구조를 가지고 있음
- 엔터티의 속성은 대소문자 구분하며 JPQL 키워드는 대소문자 구분하지 않음(select, from 등등)
- 테이블의 이름이 아닌 엔터티의 이름을 사용하며 별칭은 필수임(as 생략 가능)
(2) 집합과 정렬
- ANSI SQL이 지원하는 COUNT, SUM, AVG, MAX, MIN, GROUP BY, HAVING, ORDER BY모두 지원하며 사용법도 SQL과 동일하기 때문에 어려움 없이 사용할 수 있음
- ex) select count(m), sum(m.age), avg(m.age), max(m.age), min(m.age) from Member m
(3) TypedQuery, Query
- TypedQuery: 반환 타입이 명확할 때 사용
- Query: 반환 타입이 명확하지 않을 때 사용
// 두번째 인수로 타입 정보를 입력하면 반환타입이 TypedQuery
TypedQuery<Member> typedQuery = em.createQuery("select m from Member m", Member.class);
// JPQL의 검색 대상이 username(String), age(int) 두개의 타입이므로 타입을 지정할 수 없을 때는 Query로 반환됨
Query query = em.createQuery("select m.username, m.age from Member m");
(4) 결과 조회 API
- query.getResultList() : 결과가 하나 이상일 때 사용하며 리스트를 반환타입으로 사용하고 결과가 없으면 빈 리스트를 반환함
- query.getSingleResult() : 결과가 정확히 하나일 때 사용하며 단일 객체를 반환함, 공식 스펙으로는 결과가 없으면 NoResultException, 둘 이상이면 NonUniqueResultException 예외가 터짐
- getSingleResult는 결과가 없어도 예외를 발생시키기 때문에 사용하기가 부담스러워 실무에서는 스프링 데이터 JPA가 제공하는 반환메서드를 사용하는데, 해당 메서드도 내부적으로 보면 try-catch를 사용하여 getSingleResult가 발생하는 예외를 잡아서 null이나 Optional로 반환하는 로직으로 구현되어 있음
// getResultList를 사용하며 쿼리 결과를 반환하면 List로 반환됨
List<Member> findMembers = em.createQuery("select m from Member m", Member.class).getResultList();
// getSingleResult를 사용하며 쿼리 결과를 반환하면 단일 객체과 반환됨
Member findMember = em.createQuery("select m from Member m", Member.class).getSingleResult();
(5) 파라미터 바인딩 - 이름 기준, 위치 기준
- 이름과 위치기준으로 파라미터를 바인딩 할 수 있으나 무조건 실무에서는 :username 처럼 무조건 이름을 기준으로 파라미터를 바인딩 하는 것을 사용해야함
- 위치 기반의 파라미터 바인딩을 하는 방법은 JPQL을 떠나서 모든 곳에서 사용하는 것을 권장하지 않는데, 쿼리나 조회 대상 등이 변경이 되어 위치가 바뀌어버리면 대안으로 해결할 수 있었던 문제를 겉잡을 수 없는 심각한 버그로 이어질 확률이 높기 때문임
// 이름을 기준으로 파라미터 바인딩을 적용
Object findMember1 = em.createQuery("select m from Member m where m.username=:username")
.setParameter("username", "member1")
.getSingleResult();
// 위치로도 파라미터 바인딩을 적용할 수 있으나 실무에서 사용하지 말 것
// 이런식으로 위치로 바인딩을 적용하는 것은 언제든지 쿼리가 변경될 수 있기 때문에 심각한 문제를 일으킬 수 있음
Object findMember2 = em.createQuery("select m from Member m where m.username=?1")
.setParameter(1, "member1")
.getSingleResult();
3. 프로젝션(SELECT)
1) 설명
- SELECT 절에 조회할 대상을 지정하는 것
- 프로젝션 대상으로 엔터티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 대상이 됨
- SELECT 절에 DISTINCT로 중복을 제거할 수 있음
2) 프로젝션 종류
(1) SELECT m FROM Member m
- 대상의 엔터티를 지정하여 조회
- 가장 기본적인 조회쿼리이며 지정된 대상으로 가장 일반적인 select 쿼리가 전송됨
- 엔터티 프로젝션으로 대상을 지정하면 반환값이 모두 영속성 컨텍스트에서 관리되어 persist 없이 해당 값이 수정만 하여도 값이 변경이됨
List<Member> result = em.createQuery("select m from Member m", Member.class)
.getResultList();
Member findMember = result.get(0);
findMember.setUsername("member2"); // 조회된 findMember를 변경하는 것만으로도 DB에 update쿼리가 전송됨
(2) SELECT m.team FROM Member m
- Member 엔터티와 연관관계를 맺은 m.team 필드를 조회하는 것도 엔터티 프로젝션임
- 이렇게 JPQL을 작성하여 조회하면 SQL 입장에서는 Member와 Team을 조인하여 연관된 Team정보를 찾아야 하기 때문에 조인 쿼리가 전송됨
- 이후에 경로표현식이라는 곳에서 자세히 배우지만 이렇게 명시적으로 조인을 작성하지 않았는데 조인 쿼리가 나가는것을 묵시적 조인이라고 하는데 가능하면 JPQL과 SQL과 비슷하게 작성하는 것이 좋음
- 조인이라는 것은 튜닝 등의 방법으로 성능에 영향을 줄 수 있는 것이 많기 때문에 한눈에 보이는 것이 좋으며, 유지보수 및 운영을 용이하게 하기위해 JPQL쿼리만 보고 번역될 SQL을 예측할 수 있도록 작성하는 것이 좋음
// 연관관계를 맺은 테이블을 조회하면 조인 쿼리가 전송됨
List<Member> result = em.createQuery("select m.team from Member m", Member.class)
.getResultList();
// 가급적 번역되는 SQL과 비슷하게 join을 적용하여 JPQL을 작성하는 것이 좋음
List<Team> result = em.createQuery("select t from Member m join m.team t", Team.class)
.getResultList();
(3) SELECT m.address FROM Member m
- m.address 처럼 엔터티의 임베디드 타입을 지정된 것을 임베디드 타입 프로젝션이라고 함
- 임베디드 타입은 엔터티에 소속되어있기 때문에 그 자체만으로 별칭을 가지고 조회할 수 없고 엔터티로부터 조회를 해야하는 한계를 가지고 있음
em.createQuery("select o.address from Order o", Address.class).getResultList();
(4) SELECT m.username, m.age FROM Member m
- 원하는 값을 m.usename, m.age 처럼 지정된 것을 스칼라 타입 프로젝션이라고 함
- 조회 대상의 타입이 여러가지가 있을 수 있으므로 타입을 조회해야 할 수 있음
em.createQuery("select m.username, m.age from Member m").getResultList();
3) 여러 값 조회
- 스칼라 타입 프로젝션으로 여러 타입의 대상을 조회할 때 방법이 3가지가 있음
(1) Query 타입으로 조회
- Query는 타입이 없어서 쿼리의 조회된 값들을 List로 반환 후 get(0)으로 꺼내면 Object 타입으로 반환됨
- Object[]로 형변환하여 쿼리문의 조회대상을 작성한 순서대로 배열의 인덱스를 0부터 지정하면 값을 꺼낼 수 있음
// Member에 age와 username을 입력 후 조회
List resultList = em.createQuery("select m.username, m.age from Member m").getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
// 출력 결과
// username = member1
// age = 30
(2) Object[] 타입으로 조회
- 쿼리결과의 반환값 타입에 제네릭스로 Object[]을 지정하면 형변환 코드를 생략하여 조회된 값을 꺼낼 수 있음
List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object[] result = resultList.get(0);
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
// 출력 결과
// username = member1
// age = 30
(3) new 명령어로 조회
- DTO 클래스를 만들어 조회된 값을 DTO 타입으로 지정하여 new의 명령어로 생성한DTO클래스에 생성자를 사용하여 조회
- 생성자를 사용하기 때문에 쿼리의 순서와 타입이 일치하는 생성자가 필요함
- 가장 권장되는 방법이나 JPQL만 사용하면 쿼리가 문자이기 때문에 MemberDTO클래스의 전체경로를 패키지를 포함하여 입력해주어야 함 -> 이후에 QueryDSL을 사용하면 자바 코드로 작성하기 때문에 경로를 Import가 가능해짐
package hellojpa.jpql;
// MemberDTO 클래스 생성
public class MemberDTO {
private String username;
private int age;
// 생성자 및 getter, setter
}
// 프로그램을 실행하는 클래스
List<MemberDTO> resultList = em.createQuery("select new hellojpa.jpql.MemberDTO(m.username, m.age) from Member m",
MemberDTO.class)
.getResultList();
MemberDTO memberDTO = resultList.get(0);
System.out.println("memberDTO.getUsername() = " + memberDTO.getUsername());
System.out.println("memberDTO.getAge() = " + memberDTO.getAge());
// 출력 결과
// memberDTO.getUsername() = member1
// memberDTO.getAge() = 30
4. 페이징 API
- JPA는 페이징을 두개의 API로 추상화 해놓았음
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
(1) 적용 예시
- Member 엔터티의 값을 반복문으로 100개를 생성해놓고 JPA의 페이지네이션을 적용하면 데이터베이스 방언 설정에 따라 SQL쿼리가 DB에 반영되면서 조회결과가 출력됨
- H2 데이터베이스는 offset row가 적용됨
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
em.persist(member);
}
em.flush();
em.clear();
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(9) // 조회 시작 위치를 지정(0부터 시작)
.setMaxResults(10) // 시작 위치부터 조회할 데이터의 수를 지정
.getResultList();
System.out.println("result.size() = " + result.size());
for (Member m : result) {
System.out.println("m = " + m); // Member에 toString 오버라이딩 적용
}
// 출력 결과
result.size() = 10
m = Member{id=91, username='member90', age=90}
m = Member{id=90, username='member89', age=89}
m = Member{id=89, username='member88', age=88}
m = Member{id=88, username='member87', age=87}
m = Member{id=87, username='member86', age=86}
m = Member{id=86, username='member85', age=85}
m = Member{id=85, username='member84', age=84}
m = Member{id=84, username='member83', age=83}
m = Member{id=83, username='member82', age=82}
m = Member{id=82, username='member81', age=81}