객체지향 쿼리 언어(JPQL)

twocowsong·2023년 5월 1일
0

김영한_jpa

목록 보기
11/13

소개

가장 단순한 조회 방법은 EntityManager.find() 이였습니다.
만약, 나이가 18살 이상인 회원을 모두 검색하고 싶다면?

JPQL

JPA를 사용하면 엔티티 객체를 중심으로 개발되며, 문제는 검색 쿼리입니다.
검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색합니다.
애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요합니다.

JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원합니다.


List<Member> resultList = em.createQuery(
	"select m from Member m where m.username like '%kim%'" // Member는 실제 엔티티 객체
	, Member.class
).getResultList();


실제 실행된 쿼리를 보면, AS가 지저분하지만 like조건이 잘 적용된걸 보실수있습니다.

  • 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
  • JPQL을 한마디로 정의하면 객체 지향 SQL

단, 문제가있다면 em.createQuery는 문자열이기 때문에 공백 또는 문자 1개라도 틀리게된다면 오류가 발생하게되며 동적 쿼리를 만들기 위해선 많은 문제가 발생하게되는데 이를 해결하기 위해서 Criteria를 지원합니다.

Criteria 소개

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);

Root<Member> m = query.from(Member.class); // from

CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "klm"));
List<Member> resultList = em.createQuery(cq).getResultList();

어렵게 보이지만.. 동적 쿼리를 만드는데는 효율적입니다.


	...
    if (username != null) {
    	cq = cq.where(cb.equal(m.get("username"), "klm")); //회원 이름에 입력값이 있을때만 검색
    }
    ...

이러한 방식으로 개발이 됩니다.
그러나 단점은 SQL 같지않아서 보기 힘듭니다..
그러니 실무에서 쓰기 힘들고 유지보수성이 낮습니다.
그냥 이런게 있다하고 넘어가시면 될듯합니다.

QueryDSL

//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;

List<Member> list =
 	query.selectFrom(m)
 	.where(m.age.gt(18))
 	.orderBy(m.name.desc())
 	.fetch();
  • 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
  • JPQL 빌더 역할
  • 컴파일 시점에 문법 오류를 찾을 수 있음
  • 동적쿼리 작성 편리함
  • 단순하고 쉬움
  • 실무 사용 권장

네이티브 SQL

em.createNativeQuery("SELECT * FROM MEMBER").getResultList();


통째로 쿼리가 실행된걸 확인 할 수 있습니다.

Member member1 = new Member();
member1.setUsername("Lee");
em.persist(member1);

// flush -> commit 또는 query 실행 시 flush가 실행됨

List<Member> resultList = em.createNativeQuery("SELECT * FROM MEMBER", Member.class).getResultList();
for (Member member : resultList) {
	System.out.println("member : " + member);
}

tx.commit();


em.createNativeQuery가 실행되 기전 flush가 실행되어 DB에 INSERT된걸 확인 할 수 있습니다.

JPQL

JPQL은 객체지향 쿼리 언어입니다. 따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리합니다. JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하지 않습니다.
JPQL은 결국에는 SQL로 변환됩니다.

하게 될 예제는 모두 같은 패키지 안에서 작성되기때문에, JQPL_테이블명으로 엔티티를 생성하겠습니다.

@Getter
@Setter
@Entity(name = "JPQL_MEMBER")
@Table(name = "JPQL_MEMBER")
public class JpqlMember {

	@Id @GeneratedValue
	@Column(name = "MEMBER_ID")
	private Long id;

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

	@Column(name = "AGE")
	private int age;

	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private JpqlTeam team;
}

@Getter
@Setter
@Entity(name = "JPQL_ORDER")
@Table(name = "JPQL_ORDER")
public class JpqlOrder {

	@Id @GeneratedValue
	@Column(name = "ORDER_ID")
	private Long id;

	@Column(name = "ORDRE_AMOUNT")
	private int orderAmount;

	@Embedded
	private JpqlAdress adress;

	@ManyToOne
	@JoinColumn(name = "PRODUCT_ID")
	private JpqlProduct product;

}
@Getter
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public class JpqlAdress {
	private String city;
	private String street;
	private String zipcode;

}
@Getter
@Setter
@Entity(name = "JPQL_PRODUCT")
@Table(name = "JPQL_PRODUCT")
public class JpqlProduct {

	@Id @GeneratedValue
	@Column(name = "PRODUCT_ID")
	private String id;

	@Column(name = "NAME")
	private String name;

	@Column(name = "PRICE")
	private int price;

	@Column(name = "STOCK_AMOUNT")
	private int stockAmount;

}
@Getter
@Setter
@Entity(name = "JPQL_TEAM")
@Table(name = "JPQL_TEAM")
public class JpqlTeam {

	@Id
	@GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;

	@Column(name = "NAME")
	private String name;

	@OneToMany(mappedBy = "team")
	private List<JpqlMember> members = new ArrayList<>();

}


정상적으로 테이블이 생성된걸 확인할 수있습니다. JQPL_ORDER테이블에도 값 타입이 정상적으로 들어간걸 확인 할수 있습니다.


JPQL은 기본 ANSI표준을 따르고있습니다.

select m from Member as m where m.age > 18
• 엔티티와 속성은 대소문자 구분O (Member, age)
• JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)
• 엔티티 이름 사용, 테이블 이름이 아님(Member)
• 별칭은 필수(m) (as는 생략가능)

TypeQuery, Query

• TypeQuery: 반환 타입이 명확할 때 사용
• Query: 반환 타입이 명확하지 않을 때 사용

// 리턴 타입 Member 지정
TypedQuery<Member> query1 =
	em.createQuery("select m from JPQL_MEMBER m", Member.class);

// 리턴 타입 -> select username 이니깐, String 지정
TypedQuery<String> query2 =
	em.createQuery("select m.username from JPQL_MEMBER m", String.class);

// 리턴 타입 지정 X -> Query로 리턴
Query query3 =
	em.createQuery("select m.username, m.age from JPQL_MEMBER m");

• query.getResultList(): 결과가 하나 이상일 때

  • query.getResultList() -> 결과가 없으면 빈 리스트 반환
TypedQuery<JpqlMember> query1 =
em.createQuery("select m from JPQL_MEMBER m", JpqlMember.class);
List<JpqlMember> resultList = query1.getResultList();
for (JpqlMember member1 : resultList) {
	System.out.println(member1);
}

• query.getSingleResult(): 결과가 정확히 하나

  • query.getSingleResult() -> 없거나, 2개이상일경우 무조건 에러 -> SpringJPA를 쓰면 try해줌
    -- 결과가 없으면: javax.persistence.NoResultException
    -- 둘 이상이면: javax.persistence.NonUniqueResultException
TypedQuery<JpqlMember> query1 =
em.createQuery("select m from JPQL_MEMBER m where m.age = 10", JpqlMember.class);
JpqlMember singleResult = query1.getSingleResult();
System.out.println(singleResult);

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

// 이름 기준으로 값 바인딩
JpqlMember singleResult =
em.createQuery("select m from JPQL_MEMBER m where m.username = :username", JpqlMember.class)
		.setParameter("username", "member1")
		.getSingleResult();
System.out.println("username : " + singleResult.getUsername());

프로젝션(SELECT)

SELECT 절에 조회할 대상을 지정하는 것을 말합니다.
프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)

• SELECT m FROM Member m -> 엔티티 프로젝션
• SELECT m.team FROM Member m -> 엔티티 프로젝션
• SELECT m.address FROM Member m -> 임베디드 타입 프로젝션 // Adress 값 타입을 사용
• SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
• DISTINCT로 중복 제거

List<JpqlMember> resultList =
	em.createQuery("select m from JPQL_MEMBER m", JpqlMember.class)
	.getResultList();

resultList.get(0).setAge(20);

resultList는 영속성컨텍스트에 관리되기 때문에, setAge를 통해 UPDATE 쿼리가 실행되게 됩니다.

List<JpqlTeam> resultList =
	em.createQuery("select t from JPQL_MEMBER m join m.team t", JpqlTeam.class)
	.getResultList();

리턴 타입 객체를 select절에 맞게 변경도 가능하며, 당연히 이러한 조인문도 정상 작동합니다.

List<JpqlAdress> resultList =
	em.createQuery("select o.adress  from JPQL_ORDER o", JpqlAdress.class)
	.getResultList();

이러한 값 타입들도 당연히 정상 작동합니다.

em.createQuery("select m.username, m.age  from JPQL_MEMBER m").getResultList();

하지만 이런 스칼라 타입 같은 경우에는, 타입을 지정하지 못하니 다른방법으로 값을 도출해야합니다.

스칼라타입 - 조회

List resultList = em.createQuery("select m.username, m.age  from JPQL_MEMBER m")
					.getResultList();

Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println(result[0]); // member1
System.out.println(result[1]); // 10

타입을 연계하지 못하니깐, Object에서 배열로 받아 1개씩 뽑아내게 되면 값들이 출력되게 됩니다.

List<Object[]> resultList = em.createQuery("select m.username, m.age  from JPQL_MEMBER m")
					.getResultList();

Object[] result = resultList.get(0);
System.out.println(result[0]);
System.out.println(result[1]);

제네릭 타입으로도 한줄로 표현이 가능합니다.
Dto를 이용한 방법도 있습니다!

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class JpqlMemberDto {
	private String username;
	private int age;

}

먼저 JpqlMemberDto를 생성합니다.

// new jpql.entity.JpqlMemberDto 패키지명을 풀로 적어야합니다.
List<JpqlMemberDto> resultList = em.createQuery(
		"select new jpql.entity.JpqlMemberDto(m.username, m.age)  " +
		"from JPQL_MEMBER m"
		, JpqlMemberDto.class
).getResultList();

System.out.println(resultList.get(0).getUsername());
System.out.println(resultList.get(0).getAge());


정상적으로 값이 바인딩 된걸 확인할 수 있습니다.
패키지명이 길어지면 길어질수록 불편한점이있습니다.

페이징

  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
List<JpqlMember> resultList =
em.createQuery("select m from JPQL_MEMBER m order by m.age DESC ", JpqlMember.class)
				.setFirstResult(0) // 시작 페이지
				.setMaxResults(10) // 가져올 갯수
				.getResultList();

for (JpqlMember jpqlMember : resultList) {
	System.out.println("member age : " + jpqlMember.getAge() + ", member name : " + jpqlMember.getUsername());
}


아주 기가 막히게 페이징 처리를 할수 있게됩니다..
개인적으로 MariaDb에서 페이징처리를 할때 여러번 감싼게 스트레스였는데, 참 편하게되서 놀랐습니다.

조인

  • 내부 조인: SELECT m FROM Member m [INNER] JOIN m.team t
List<JpqlMember> resultList =
	em.createQuery("select m from JPQL_MEMBER m join m.team t",
					JpqlMember.class).getResultList();
  • 외부 조인: SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
List<JpqlMember> resultList =
	em.createQuery("select m from JPQL_MEMBER m left join m.team t",
					JpqlMember.class)
	.getResultList();
  • 세타 조인: select count(m) from Member m, Team t where m.username = t.name
List<JpqlMember> resultList =
em.createQuery("select m from JPQL_MEMBER m, JPQL_TEAM t where m.username = t.name",
				JpqlMember.class)
				.getResultList();

조인 ON절

  • 조인 대상 필터링
List<JpqlMember> resultList =
	em.createQuery("SELECT m, t FROM JPQL_MEMBER m LEFT JOIN m.team t on t.name = 'A'"
	).getResultList();
List<JpqlMember> resultList =
em.createQuery("SELECT m, t FROM JPQL_MEMBER m LEFT JOIN JPQL_TEAM t on m.username = t.name").getResultList();

서브쿼리

  • 나이가 평균보다 많은 회원
    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

JPA 서브 쿼리 한계

  • JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능
  • SELECT 절도 가능(하이버네이트에서 지원)
  • FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
    -> 하이버네이트6에서는 해결 됨
  • 조인으로 풀 수 있으면 풀어서 해결

JPQL 타입 표현

  • 문자: ‘HELLO’, ‘She’’s’
  • 숫자: 10L(Long), 10D(Double), 10F(Float)
  • Boolean: TRUE, FALSE
  • ENUM: jpabook.MemberType.Admin (패키지명 포함)
// MemberType Enum을 생성 후 
public enum MemberType {
	ADMIN, USER
}
	// JpqlMember엔티티에 MemberType추가하여
	...for...JpqlMember...
    @Enumerated(EnumType.STRING)
    private MemberType type;
    ...
List<Object[]> resultList =
em.createQuery("select m.username, 'HELLO', true from JPQL_MEMBER m " +
				" where m.type = jpql.entity.MemberType.USER"
				).getResultList();

for (Object[] objects : resultList) {
	System.out.println(" objects : " + objects[0] + ", " + objects[1] + ", " + objects[2]);
}

where에 조건문에 Enum을 조건으로 추가하여 사용이 가능합니다.

List<Object[]> resultList =
em.createQuery("select m.username, 'HELLO', true from JPQL_MEMBER m " +
				" where m.type = :userType"
).setParameter("userType", MemberType.ADMIN).getResultList();

setParameter형식으로 개발이 가능합니다.

  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)

표준 SQL은 모두 지원합니다.

  • JPQL 기타
  • SQL과 문법이 같은 식
  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <>
  • BETWEEN, LIKE, IS NULL

조건식 - CASE 등등


기본 CASE식은 조건에 따른 분기이며, 단순 CASE식은 값이 일치할때 분기입니다.

List<Object[]> resultList =
em.createQuery("select case when m.age <= 10 then '학생요금' " +
		 "                     when m.age >= 60 then '경로요금' " +
		 "                else '일반요금' end , m.username " +
		 "from JPQL_MEMBER m").getResultList();

for (Object[] objects : resultList) {
	System.out.println(" objects : " + objects[0] + ", " + objects[1]);
}

case 문이 정상적으로 작동되는걸 확인할 수 있습니다.

  • COALESCE: 하나씩 조회해서 null이 아니면 반환
List<Object[]> resultList =
em.createQuery("select COALESCE(m.username, '이름없는 회원'), m.age from JPQL_MEMBER  m")
.getResultList();

for (Object[] objects : resultList) {
	System.out.println(" objects : " + objects[0] + ", " + objects[1]);
}

  • NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환

JPQL 기본 함수

기본 Ansi를 따르기 때문에 아래에 함수들을 사용하시면됩니다.
• CONCAT
• SUBSTRING
• TRIM
• LOWER, UPPER
• LENGTH
• LOCATE
• ABS, SQRT, MOD
• SIZE, INDEX(JPA 용도)

profile
생각하는 개발자

0개의 댓글