본문 바로가기
IT/오픈소스

JPA - 다양한 쿼리방법

by 모띠 2024. 12. 23.

 

 
 

본 포스팅은 총 4편으로 구성되어 있습니다.

2024.01.30 - [IT/오픈소스] - JPA - 기본 개념

2024.12.23 - [IT/오픈소스] - JPA - 고급 매핑

2024.12.23 - [IT/오픈소스] - JPA - 다양한 쿼리방법

2024.12.24 - [IT/오픈소스] - JPA - 스프링 연동

 

 

지금까지는 조건이 없는 간단한 쿼리를 조회하였다.


Member m = em.find(Member.class, 3);

하지만, 조건이 복잡한 쿼리는 결국에 SQL을 사용해야한다.

String jpql = "select m from Member m where m.id = 3 and m.age = 30";
Member m = em.createQuery(jpql, Member.class).getSingleResult();

ORM을 사용하면 테이블이 아닌 엔티티 객체를 대상으로 개발한다. 하지만, SQL은 테이블대상으로 조회하므로 엔티티 객체를 대상으로 검색하는 방법이 필요하다.

→ 엔티티 객체를 대상으로 조회하는 JPQL이 등장.

JPA는 복잡한 검색 조건을 사용해서 엔티티 객체를 조회할 수 있는 다양한 쿼리 기술을 지원한다.

  1. JPQL - 객체지향 쿼리
  2. Criteria - JQPL을 편하게 작성하도록 도와주는 빌더 클래스 모음
  3. QueryDSL (공식지원x) - Criteria처럼 JQPL을 편하게 작성하도록 도와주는 빌더 클래스 모음. 비표준 오픈소스 프레임워크
  4. 네이티브 쿼리 - SQL을 직접 사용

Criteria나 QueryDSL은 결국 JPQL을 편하게 작성하도록 도와주는 빌더 클래스일뿐이므로 JPQL이 가장 중요.

 

 

 


 

 

JPQL

 

JPQL은 엔티티 객체를 조회하는 객체지향 쿼리다. 테이블을 대상으로 쿼리하는것이 아니라 엔티티를 대상으로 쿼리

  • SQL과 거의 유사
  • JPQL은 객체지향 쿼리이므로 특정 데이터베이스에 의존하지않는다.
  • 방언만 변경하면 JPQL을 수정하지않아도 해당 데이터베이스에 맞는 적절한 SQL함수가 실행된다.
  • 엔티티 직접 조회, 묵시적 조인, 다형성 지원
String jpql = "select m from member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();


select 
		m.id as id,
		m.age as age,
		m.USERNAME as USERNAME,
		m.TEAM_ID as TEAM_ID
from
		Member m
where
		m.age > 18		

 

기본 문법과 쿼리 API

  • 대소문자 구분
  • 엔티티 이름을 사용, 테이블 이름(X)
  • 별칭 필수 - 별칭 미사용시 에러발생

TypeQuery - 반환할 타입을 명확하게 지정할 수 있을때 (타입을 변환할 필요가없음)

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

List<Member> resultList = query.getResultList();
for(Member m : resultList){
	System.out.println("member = " + member);
}

 

 

 

Query - 반환타입이 명확하지않을때 (타입을 변환해주어야함)

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

List<Object[]> resultList = query.getResultList(); // 결과가 둘 이상이면 object배열 반환
for(Object row : resultList){
	System.out.println("username = " + row[0]);
	System.out.println("age = " + row[1]);
}

 

결과 조회

query.getResultList() : 결과가 하나이상, 리스트반환 / 결과가 없을때는 빈 컬렉션 반환

query.getSingleResult() : 결과가 정확히 하나. 하나가아니면 예외반환 / 결과가 없을때 NoResultException 예외발생

 

파라미터 바인딩

  • 이름기준 : select m from member m where m.username=:username
  • 위치기준 : select m from member m where m.username=?1 ← 비추천방식

파라미터 방식을 이용하지않고 직접 문자를 더해 만들지 않도록 주의

  • sql 인젝션 공격에 취약 / 같은 쿼리로 인식하지못하여 파싱결과 재사용불가(성능 이슈)
"select m from member m where m.username = '" + username + "'"

 

프로젝션

select 절에 조회할 대상을 지정하는것을 프로젝션이라고 한다.

프로젝션 대상은 엔티티, 엠비디드 타입, 스칼라 타입이 있다.

 

List<Member> query = em.createQuery("select m from Member m", Member.class).getResultList(); // 엔티티 타입 조회

List<String> query = em.createQuery("select m.username from Member m", String.class).getResultList(); // 스칼라 타입 조회

여러데이터를 조회한 후에 dto로 반환하고 싶을때 new 명령어를 사용하면 편하다.

  • 패키지명을 포함한 전체 클래스 명을 입력해야한다.
  • 순서와 타입이 일치하는 생성자가 필요하다
Query query = em.createQuery("select m.username, m.age from Member m");

List<Object[]> resultList = query.getResultList(); // 결과가 둘 이상이면 object배열 반환
for(Object row : resultList){
	userDto dto = new UserDto((String)row[0], (String)row[1]);
}


TypeQuery<UserDto> query = em.createQuery("select new jpabook.jqpl.userDto(m.username, m.age) from Member m", UserDto.class);
List<UserDTO> resultList = query.getResultList(); // 알아서 DTO로 변환

 

페이징 API

데이터베이스마다 페이징 처리하는 SQL문법이 다르지만 JPA는 페이징을 두 API로 추상화했다.

  • setFirstReust(int startPosition) : 조회 시작위치(0부터시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
List<Member> query = em.createQuery("select m from Member m", Member.class)
query.setFirstResult(10);
query.setMaxResults(20);
query.getResultList();


//mysql
select 
	m.id as id,
	m.age as age,
	m.team_id as team_id,
	m.name as name
from
	member m
order by
	m.name desc limit ?, ?

//oracle  <- select절 3번써야함
select * from
	(select ROW_.*, ROWNUM ROWNUM_
	from
		(select
			m.id as id,
			m.age as age,
			m.team_id ad team_id,
			m.name as name
		from member m
		order by m.name
		) ROW_
	where rownum <= ?
	)
where rownum_ > ?

 

집합 정렬

select COUNT(m), SUM(m.age), MAX(m.age) from Member m
select COUNT(m), SUM(m.age), MAX(m.age) from Member m LEFT JOIN m.team t GROUP BY t.name
select t.name, COUNT(m.age) from Member m LEFT JOIN m.team t GROUP BY t.name ORDER BY cnt

 

 

조인

  • 내부조인

내부조인은 INNER JOIN을 사용한다. INNER 생략가능.

JPQL 조인의 가장 큰 특징은 연관 필드를 사용한다는 점. FROM Member m JOIN Team t 로 사용시 에러발생 주의.

List<Member> query = em.createQuery("select m from Member m JOIN m.team t", Member.class).getResultList();

SELECT
	M.ID AS ID,
	M.AGE ..
FROM
	MEMBER M JOIN TEAM T ON M.TEAM_ID = T.ID

 

  • 외부조인

외부조인은 OUTER JOIN을 사용한다. 보통 OUTER는 생략해서 LEFT JOIN을 사용

List<Member> query = em.createQuery("select m from Member m LEFT JOIN m.team t", Member.class).getResultList();

SELECT
	M.ID AS ID,
	M.AGE ..
FROM
	MEMBER M LEFT OUTER JOIN TEAM T ON M.TEAM_ID = T.ID
  • 컬렉션 조인

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

[회원 → 팀]으로의 조인은 다대일 조인이면서 단일 값 연관필드(m.team)을 사용

[팀 → 회원]으로의 조인은 일대다 조인이면서 컬렉션 값 연관필드(t.members)을 사용

 

List<Object[]> query = em.createQuery("select t, m from Team t LEFT JOIN t.members m").getResultList();

 

  • 세타조인
select COUNT(m) from Team t, Member m

SELECT COUNT(M.ID)
FROM 
	MEMBER M CROSS JOIN TEAM T

 

  • JOIN ON
select m, t from Member m left join m.team t on t.name ='A'

SELECT m.*, t.* FROM Member m
LEFT JOIN Team t ON m.TEAM_ID = t.id and t.name ='A';

 

  • 페치 조인
// 회원을 조회할때 팀도 함께 조인
List<Member> members = em.createQuery("select m from Member m join fetch m.team", Member.class).getResultList();

for(Member m : members){
	m.getTeam().name(); // 지연로딩 발생하지않음
}


SELECT
	M.*, T.*
FROM MEMBER M
INNER JOIN TEAM T ON M.TEAM_ID=T_ID;

select m으로 회원 엔티티만 선택했지만 실행된 sql을 보면 회원과 연관된 팀 엔티티도 함께 조회되었다.

일대다 관계인 컬렉션 페치 조인을 하는경우에는 distinct를 사용해야한다.

 

// 팀 -> 회원 조회
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) { // distinct를 사용하지않을경우 팀A가 2번 조회됨.
	
	for (Member member : team.getMembers()) {
		...
	}

}

SELECT
	T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T_ID=M.TEAM_ID
WHERE T.NAME = '팀A';
  • 페치 조인을 사용하면 SQL한번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL호출 횟수를 줄여 성능에 최적화
  • 페치 조인은 글로벌 로딩 전략보다 우선시
  • 별칭 사용불가
  • 둘 이상의 컬렉션에 페치 불가
  • 컬렉션을 페치 조인하면 페이징 사용불가

페치 조인을 남발하면 불필요한 엔티티를 로딩하여 성능에 악영향을 주므로,

글로벌 로딩전략은 지연로딩으로 사용하고 최적화가 필요하면 페치 조인을 사용하는것이 효과적

 

경로표현식

경로표현식은 .(점)을 찍어 객체 그래프를 탐색하는것을 의미.

경로표현식은 3가지로 표현

  • 상태 필드 : 단순히 값을 저장하기 위한 필드 - 경로탐색의 끝
  • 단일값연관필드 : 연관관계를 위한 필드. 대상이 엔티티 - 계속 탐색 가능 (묵시적 조인 발생)
  • 컬렉션값연관필드 : 연관관계를 위한 필드. 대상이 컬렉션 - 계속 탐색 불가. 다만 조인을 통해 별칭을 얻으면 탐색가능 (묵시적 조인 발생)

 

@Entity
public class Member{
	@Id
	private long id;
	
	private String username; // 상태필드
	private String age; // 상태필드

	@ManyToOne(..)
	private team team; // 단일 값 연관필드
	
	@OneToMany(..)
	private List<Order> orders; // 컬렉션 값 연관필드
	
}
  • 상태 필드 경로 탐색

 

select m.username, m.age from Member m

select m.name, m.age
from Member m
  • 단일 값 연관 경로 탐색

단일값 연관 필드로 경로 탐색을 하면 sql내부에서 조인이 일어나는데, 이것을 묵시적 조인이라고 한다 (묵시적 조인은 모두 내부조인)

 

select m.team from Member m

select t.*
from Member m
inner join Team t on m.team_id=t.id;

명시적 조인 : join을 직접 지정

 

select m from Member m JOIN m.team t

 

묵시적조인 : 경로 표현식에 의해 일어나는 조인

 

select m.team from Member m
  • 컬렉션 값 연관 경로 탐색

컬렉션 값에서는 경로탐색을 시도할 수 없다.

select t.members from Team t // 성공
select t.members.username from Team t // 실패

컬렉션 값에서도 경로탐색을 하고싶다면 조인을 사용하여 별칭을 획득해야한다.

 

select m.username from Team t join t.members m

select t.members.size form Team t // 컬렉션 크기를 구할수있는 특별한 기능 제공 = count

경로 탐색을 사용하면 묵시적 조인이 발생하여 sql에서 내부 조인이 일어날 수 있다.

  • 항상 내부조인이다.
  • 컬렉션은 경로 탐색의 끝이다.

조인이 성능상 차지하는 부분은 매우 크다. 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기가 어렵다는 단점이 있다.

따라서 묵시적 조인보다는 명시적 조인을 사용하는 것을 권장한다.

 

엔티티 직접사용

엔티티를 직접 사용하여도 기본 키값으로 변환해준다.

select count(m.id) from Member m;
select count(m) from Member m; //엔티티 직접사용
// 아래와 동일한 결과
List<Member> resultList = 
	em.createQuery("select m from Member m where m.username = :username", Member.class)
		.setParameter("username", member); // 엔티티를 직접 넣어도 기본키값을 사용하도록 변환된다.
		.getResultList();


List<Member> resultList = 
	em.createQuery("select m from Member m where m.username = :username", Member.class)
		.setParameter("username", "abc"); 
		.getResultList();

 

Named 쿼리 : 정적 쿼리

 

JPQL 쿼리는 크게 동적쿼리와 정적쿼리로 나눌 수 있다.

  • 동적 쿼리 : em.createQuery(”select …”) 처럼 JPQL을 문자로 완성해서 직접 넘기는 쿼리. 런타임에 특정 조건에 따라 JQPL을 동적으로 구성할 수 있다.
  • 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할때 사용하는 쿼리. 한번 정의하면 변경 불가

정적쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱하여 재사용하므로 오류의 빠른 확인 및 성능상의 이점이 있다.

Named쿼리를 사용할때는 createNamedQuery()메소드에 Named 쿼리 이름을 입력하면된다.

복수의 쿼리 입력시 @NamedQueries 사용

@Entity
@NamedQuery(
	name = "Member.findByUsername",
	query = "select m from Member m where m.username = :username")
public class Member{ ... }
List<Member> resultList = 
	em.createNamedQuery("Member.findByUsername", Member.class)
		.setParameter("username", "회원1"); 
		.getResultList();

Named 쿼리를 XML에도 정의할 수 있다. 만약 어노테이션과 XML에 같은 설정이 있다면 우선권은 XML이 가져간다.

서브쿼리, 조건식, 사용자 정의함수, 모두 가능

 

 

 

Criteria

criteria 쿼리는 JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 api다.

criteria를 사용하면 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일 단계에서 파악할 수 있다. 또한, 문자 기반의 JPQL보다 동적 쿼리도 안전하게 생성할 수 있다.

하지만 직관적으로 이해가 어렵다.

 

"SELECT * FROM MEMBERR WHERE MEMBER_ID = '100'" // 이런 잘못된 문법을 잡을수가없음
//JQPL
// select m from Member m
// where m.username='회원1'
// order by m.age desc

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class); // 생성 반환타입 지정
Root<Member> m = cq.from(Member.class); // from절 생성
Predicate usernameEqual = cb.equal(m.get("username"), "회원1");
javax.persistence.criteria.Order ageDesc = cb.desc(m.get("age"));

cq.select(m)
	.where(usernameEqual)
	.orderBy(ageDesc);
	
List<Member> resultList = em.createQuery(cq).getResultList();

반환 타입을 지정할 수 없거나 둘 이상이면 Object로 반환

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object> cq = cb.createQuery(); // 생성 반환타입 지정불가

그룹함수, 정렬, 조인, 서브 쿼리 등 가능하지만 생략

 

파라미터 정의

//JQPL
// select m from Member m
// where m.username= : usernameParam

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class); 
Root<Member> m = cq.from(Member.class);

cq.select(m)
	.where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParam")));
	
List<Member> resultList = em.createQuery(cq).setParameter("usernameParam", "회원1").getResultList();

 

메타 모델 API

 

적용시, 엔티티 내부의 문자열도 코드기반으로 변경 가능 (pom.xml추가)

cq.select(m)
	.where(cb.equal(m.get("username"), "abc"));
	
cq.select(m)
	.where(cb.equal(m.get(Member_.username), "abc"));	

 

동적 쿼리

List<Predicate> criteria = new ArrayList<>();
if(age != null) criteria.add(cb.equal(m.<Integer>get("age"), cb.paramter(Integer.class, "age")));
if(username != null) criteria.add(cb.equal(m.<String>get("username"), cb.paramter(String.class, "username")));
if(teamName != null) criteria.add(cb.equal(m.<String>get("teamName"), cb.paramter(String.class, "teamName")));

cp.where(cb.and(criteria.toArray(new Predicate[0])));

TypeQuery<Member> query = em.createQuery(cq);
if(age != null) query.setParameter("age", age);
if(username != null) query.setParameter("username", username);
if(teamName != null) query.setParameter("teamName", teamName);

List<Member> resultList = query.getResultList();

criteria 로 동적 쿼리를 구성하면 최소한 공백, 문자열 더하기나 위치변경으로 인한 에러는 없다.

하지만 너무 장황하고 읽기 복잡하여 어떤 JPQL이 생성될지 한눈에 파악이 안된다.

→ QueryDSL 사용을 권장

 

 


 

 

 

QueryDSL

 

SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API, 오픈소스

쿼리를 문자가 아닌 코드로 작성하여 쉽고 간결하여 criteria를 사용함으로써 생기는 복잡함을 해결.

Criteria대신 QueryDSL을 쓰는것을 권장

 

pom.xml

<!-- QueryDSL JPA라이브러리 -->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>5.0.0</version>
</dependency>

<!-- 쿼리타입(Q)를 생성하기 위한 라이브러리 -->
<dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>5.0.0</version>    
    <scope>provided</scope>
</dependency>

<!-- target/generated-sources 위치에 QMember.java 처럼 쿼리타입 생성 -->
<build>
    <plugins>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>  
    </plugins>
</build>

 

QUERY DSL 사용

//JPQL SELECT m from Member m where m.age > 18

EntityManager em = emf.createEnityManager();

JPAQuery query = new JPAQuery(em);
QMember q = QMember.member(m); // 생성되는 jpql의 별칭이 m

List<Member> list = query.from(q).where(q.age.gt(18)).list(Member);

기본 Q 생성

쿼리타입(Q)는 사용하기 편하도록 기본 인스턴스를 보관하고있다.

QMember qMember = new QMember("m"); // 별칭(o)
QMember qMember = QMember.member; // 별칭(x)

결과 조회

  • uniqueResut() : 조회결과가 1건일때 사용 / 결과가 없으면 null리턴 / 결과가 2건이상이면 예외 발생
  • list() : 결과가 하나 이상일때 사용 / 값이 없으면 빈 컬렉션 리턴

 

조인

  • innerJoin, leftJoin, rightJoin, fullJoin, fetch 사용가능

join(조인 대상, 별칭)

JPAQuery query = new JPAQuery(em);
QMember m = QMember.member; 
QTeam t = QTeam.team;

// innerJoin
List<Member> list = query.from(m).join(m.team, t).list(m);

// leftJoin
List<Team> list = query.from(t).leftJoin(t.members, m).on(m.count.get(2)).list(t);

// fetch
List<Member> list = query.from(m).join(m.team, t).fetch().list(m);

List<Member> list = query.from(order, member).where(order.member.eq(member)).list(order);

페이징

JPAQuery query = new JPAQuery(em);
QMember m = QMember.member;

List<Member> list = query.from(m).orderBy(m.age.desc()).offset(10).limit(20).fetch().list(m);

프로젝션

  • Tuple = Map과 비슷한 내부타입
QMember m = QMember.member; 

// 조회 대상이 하나
List<String> list = query.from(m).list(m.name); // select name from Member

// 조회 대상이 여러 컬럼
List<Tuple> list = query.from(m).list(m.name, m.age); // select name, aget from Member
List<Tuple> list = query.from(m).list(new QTuple(m.name, m.age)); // select name, aget from Member

결과 빈 생성

쿼리 결과를 엔티티가 아닌 특정 객체로 받고 싶다면 빈생성 기능을 사용

  • 프로퍼티 접근
  • 필드 직접 접근
  • 생성자 사용
QMember m = QMember.member; 

class MemberDto{
	private String username;
	private int age;
	..
}

// 프로퍼티 접근
List<MemberDto> list = query.from(m).list(Projections.bean(MemberDto.class, m.name.as("username"), m.age);

// 필드 직접 접근 - private이여도 동작
List<MemberDto> list = query.from(m).list(Projections.fields(MemberDto.class, m.name.as("username"), m.age);

// 생성자 사용
List<MemberDto> list = query.from(m).list(Projections.constructor(MemberDto.class, m.name.as("username"), m.age);

수정 삭제 배치 쿼리

querydsl도 수정, 삭제 배치 쿼리를 지원한다. jpql과 마찬가지로 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리함에 유의

QItem item = QItem.item;
JPAUpdateClause update = new JPAUpdateClause(em, item);
long count = update.where(item.name.eq("abc")).set(item.price, item.price.add(100).execute();

동적 쿼리

BooleanBuilder를 사용하여 동적 쿼리를 편하게 생성 가능.

String name = "member";
int age = 9;

QMember m = QMember.member;
BooleanBuilder builder = new BooleanBuilder();

if (name != null) { // null일때 조건이 실행되지않도록 할수있음
	builder.and(m.name.contains(name));
}
if (age != 0) {
	builder.and(m.age.gt(age);
}

List<Member> list = query.from(m).where(builder).list(m);


// 메소드 위임 기능을 이용하여 조건을 미리 정할수도있음.
return query.selectFrom(coupon)
		.where(coupon.type.eq(typeParam),
					isServiceable())  // 이런식으로 조건을 따로 정의해놔서 재사용하면서 조립이 가능함
		.fetch();

@QueryDelegate(String.class)
private BooleanExpression isServiceable() { 
		return coupon.status.eq("LIVE")
		.and(marketing.viewCount.lt(markting.maxCount));
}

정렬, 그룹, 서브 쿼리 모두 사용가능

 

 


 

 

 

네이티브 SQL

JPQL은 표준 SQL이 지원하는 대부분의 문법과 함수를 지원하지만 특정 데이터베이스에 종속적인 기능은 지원하지않는다.

  • 인라인뷰, UNION, INTERSECT
  • 특정 데이터베이스만 지원하는 함수, SQL 쿼리 힌트
  • 스토어드 프로시저

이런 경우에는 JPQL을 사용하지않고 SQL을 직접 사용하도록 지원한다.

네이티브 SQL을 사용하면 엔티티를 조회할 수 있고, JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 활용할수있다. (JDBC API와는 다름)

// 결과 타입 정의불가
public Query createNativeQuery(String sqlString);

// 결과 타입 정의
public Query createNativeQuery(String sqlString, Class resultClass);

// 결과 매핑 사용
public Query createNativeQuery(String sqlString, String resultSetMapping);

결과 타입 정의

String sql = "SELECT ID, NAME, AGE FROM MEMBER WHERE AGE > ?";

Query nativeQuery = em.createNativeQuery(sql, Member.class) // 결과 타입 정의

List<Member> members = nativeQuery.setParameter(1, 19).getResultList();

members.forEach(System.out::println);

결과 타입 정의불가

String sql = "SELECT ID, NAME, AGE FROM MEMBER WHERE AGE > ?";

Query nativeQuery = em.createNativeQuery(sql); // 결과 타입 미지정

List<Object[]> members = nativeQuery.setParameter(1, 19).getResultList(); // Object로 받음

결과 매핑 사용 - @SqlResultSetMapping 참고

 

Name 네이티브 SQL

 

JPQL처럼 네이티브 SQL로 정적 sql 사용이 가능하다. (JPQL이 아닌 SQL로 작성한 것이 차이점)

@Entity
@NamedNativeQuery(name = "Member.memberGtAge",
		query = "SELECT M.MEMBER_ID, M.NAME, M.AGE FROM MEMBER M WHERE M.AGE > ?",
		resultClass = Member.class)
public class Member { ... }



List<Member> members = em.createNamedQuery("Member.memberGtAge", Member.class) // 정의해둔 정적SQL 바로 호출
	.setParameter(1, 19)
	.getResultList();

네이티브 SQL은 관리하기 쉽지않고 자주 사용하면 특정 데이터베이스에 종속적인 쿼리가 증가해서 이식성이 떨어진다.

가능한 표준 JPQL을 사용하고 기능이 부족할시 네이티브 SQL 사용을 권장

 


 

 

객체지향 쿼리 심화

 

벌크 연산

엔티티를 수정하려면 영속성 컨텍스트의 변경 감지기능이나 병합을 사용하고, 삭제하려면 remove()를 사용한다.

Product product = em.find(Product.class, 1L); // 영속성 컨텍스트에서 저장 후 반환
product.setPrice(product.getPrice()*2); // 수정
tx.commit(); // 변경 감지

하지만 이 방법으로 수백개의 엔티티를 하나씩 처리하기는 오래걸리므로 벌크연산을 사용한다.

// 삭제 & 수정
int resultCount = em.createQuery("update Product p set p.price = p.price * 2 where p.price < 2000").executeUpdate();

단, 벌크연산은 영속성 컨텍스트를 무시하고 테이터베이스에 직접 쿼리하므로 불일치 문제를 조심해야한다.

불일치 문제 해결방법

  1. 벌크 연산을 수행한 직후 em.refresh()로 다시 조회
  2. 벌크 연산을 먼저실행
  3. 벌크 연산 수행후 영속성 컨텍스트 초기화

 

find() vs JPQL

em.find() 메소드는 엔티티를 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다. 따라서 해당 엔티티가 컨텍스트에 있으면 성능상 이점.

JPQL은 항상 데이터베이스에서 SQL을 실행해서 결과를 조회.

데이터를 조회한 후에 영속성 컨텍스트에 없으면 저장하고, 있다면 가져온 데이터를 버리고 이미 존재하는 값을 반환한다.

 

JPQL과 플러시 모드

JPQL은 2가지의 플러시 모드가 존재한다.

em.setFlushMode(FlushModeType.AUTO); // 커밋 OR 쿼리 실행직전에 FLUSH (기본값)
em.setFlushMode(FlushModeType.COMMIT); // 커밋시에만 FLUSH

JPQL은 영속성컨텍스트를 고려하지않고 데이터베이스에서 데이터를 조회하므로 싱크가 맞지않는 현상이 있는데,

JPA는 해당 현상을 방지하기위해 JPQL을 실행하기전에 자동으로 플러쉬 되도록 설정되어있다.

// 가격을 1000 -> 2000 변경
product.setPrice(2000);

// 가격이 2000원인 상품조회
Product product = em.createQuery("select p from Product p where p.price = 2000", Product.class).getSingleResult();

원래라면 조회가 되지않지만 쿼리실행전에 자동으로 플러시되므로 조회가 가능.

 

 

 

 

 

참고

https://lordofkangs.tistory.com/404

 

댓글