본문 바로가기
BackEnd/JPA

[JPA] 객체지향 쿼리 언어1 - 기본 문법(1)

by 개발 Blog 2024. 7. 19.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

소개

JPQL 소개

- 가장 단순한 조회 방법이다.

  • EntityManager.fin()
  • 객체 그래프 탐색(a.getB(). getC())

- JPA를 사용하면 엔티티 객체를 중심으로 개발하게 된다.
- 그러나 검색 쿼리를 작성할 때 문제가 발생한다.
- 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 한다.
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다.
- 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다.

- JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다.
- JPQL은 SQL과 문법이 유사하며, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등의 기능을 지원한다.
JPQL은 엔티티 객체를 대상으로 쿼리를 수행한다.
SQL은 데이터베이스 테이블을 대상으로 쿼리를 수행한다.

 

예시코드

 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);
    }

- 주석 부분의 JPQL이 실제 SQL로 변환 후 실행된다.

 

 Criteria

- JPQL을 작성할 때 문자가 아닌 자바 코드로 작성할 수 있다.
- 이는 JPQL 빌더 역할을 한다.
- JPA의 공식 기능이다.
- 하지만 단점으로는 너무 복잡하고 실용성이 떨어진다.
- Criteria 대신에 QueryDSL 사용을 권장한다. 

//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"));
em.createQuery(cq).getResultList();

- 지금은 단순한 쿼리라서 쉬워 보이지만 복잡해지면 끝도 없이 복잡해진다. 실용성이 없으니 사용하지 말자.

 

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

- JPA는 SQL을 직접 사용할 수 있는 기능을 제공한다.
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능을 사용할 수 있다.
- 예를 들어, 오라클의 CONNECT BY 기능이나 특정 데이터베이스에서만 사용하는 SQL 힌트를 사용할 수 있다.

String sql =
	“SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
    
List<Member> resultList =
			em.createNativeQuery(sql, Member.class).getResultList();

 

JDBC 직접 사용, SpringJdbcTemplate 등

- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스 등을 함께 사용할 수 있다.
- 하지만 영속성 컨텍스트를 적절한 시점에 강제로 플러시 해야 한다.
- 예를 들어, JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해야 한다.

 

기본 문법과 쿼리 API

 JPQL 실습을 위한 엔티티 구조다.

코드로 구현해 보자

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;
    private String username;
    private int age;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    ...
}
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
	
    ...
}
@Entity
@Table(name = "ORDERS")
public class Order {
    @Id @GeneratedValue
    private Long id;
    private int orderAmount;

    @Embedded
    private Address address;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
}
@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
    
    ...
}
@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
    private int stockAmount;
    ...
}

테이블 생성을 모두 완료했다. 

 

JPQL 문법

- select m from Member as m where m.age > 18

- 엔티티와 속성은 대소문자를 구분한다. (예: Member, age)
- JPQL 키워드는 대소문자를 구분하지 않는다. (예: SELECT, FROM, where)
- JPQL에서는 테이블 이름이 아닌 엔티티 이름을 사용한다. (예: Member)
- 별칭 사용은 필수다. (예: m) as 키워드는 생략 가능하다.

 

집합과 정렬

- GROUP BY, HAVING, ORDER BY 사용가능

 

TypeQuery, Query

TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2 = em.createQuery("select m.username from Member m", String.class);

- TypeQuery : 반환 타입이 명확할 때 사용한다.

- Member를 Type으로 지정하거나, name을 넘기기 위해 String으로 지정할 수 있다.

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

- Query : 반환 타입이 명확하지 않을 때 사용한다.

- name과 age는 String과  int로 타입이 명확하지 않다. 이럴 때 Query를 사용한다.

 

결과 조회 API

TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query.getResultList();

for (Member member1 : resultList) {
    System.out.println("member1 = " + member1);
}

- query.getResultList(): 결과가 하나 이상일 때, 리스트를 반환한다. 결과가 없으면 빈 리스트를 반환한다.

 

TypedQuery<Member> query = em.createQuery("select m from Member m where m.id = 10", Member.class);

Member result = query.getSingleResult();
System.out.println("result = " + result);

- query.getSingleResult(): 결과가 정확히 하나여야 한다(단일 객체 반환)

  • 결과가 없으면: javax.persistence.NoResultException
  • 둘 이상이면: javax.persistence.NonUniqueResultException

파라미터 바인딩 

 

이름기준

TypedQuery<Member> query = em.createQuery("select m from Member m where m.username =:username", Member.class);
query.setParameter("username", "member1");

Member singleResult = query.getSingleResult();
System.out.println("singleResult = " + singleResult);


----> 메서드 체이닝 

Member result = em.createQuery("select m from Member m where m.username =:username", Member.class)
        .setParameter("username", "member1")
        .getSingleResult();
System.out.println("singleResult = " + result.getUsername());

 

위치 기준

SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);

- 예를 들어 1, 2, 3 이 있을 때 중간에 숫자를 삽입하면 순서가 밀려서 장애가 발생한다. 따라서 사용을 추천하지 않는다.

 

프로젝션(SELECT)

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

- 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)

SELECT m FROM Member m -> 엔티티 프로젝션

SELECT m.team FROM Member m -> 엔티티 프로젝션

SELECT m.address FROM Member m -> 임베디드 타입 프로젝션

SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션

DISTINCT 중복 제거

Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);

em.flush();
em.clear();

List<Member> result = em.createQuery("select m from Member m", Member.class)
        .getResultList();

Member findMember = result.get(0);
findMember.setAge(20);

tx.commit();

- 이 코드에서 age값이 10 -> 20으로 변경되면 영속성 컨텍스트에서 관리되는 거고 변경되지 않으면 아닌 것이다.

- 20으로 변경되었다.

 

엔티티 프로젝션으로 조회하는 대상은 전부 영속성 컨텍스트에서 관리된다.

 

묵시적 조인, 명시적 조인

 List<Team> result = em.createQuery("select m.team from Member m", Team.class).getResultList();

- JPQL만 보고 INNER조인을 할 것이라는 것을 한눈에 파악하기 힘들다.

 

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

- 이렇게 JPQL을 보고 JOIN을 할 것이라는 게 예상이 되도록 쿼리를 작성해야 한다.

 

임베디드 타입 프로젝션

em.createQuery("select o.address from Order o", Address.class).getResultList();

- Order 안의 값 타입인 Address를 조회하는 것이기 때문에 조인이 필요 없다.

- From절에 Address를 적을 수는 없다. 값 타입은 어딘가에 소속되어 있기 때문에 어디 소속인지 엔티티를 명시해주어야 한다.

 

스칼라 타입 프로젝션

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

- 일반 SQL 프로젝션이랑 거의 똑같다.

 

여러 값 조회

Query 타입으로 조회

List resultList = em.createQuery("select distinct 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]);

- Query는 반환타입이 명확하지 않을 때 사용한다.

- Query는 일반적으로 Object 배열이나 List를 반환한다.

- 내용을 출력하기 위해 Object 배열로 타입 캐스팅 한다.

 

Object [] 타입으로 조회

 List<Object[]> resultList = em.createQuery("select distinct 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]);

- List <Object []>로 지정하면 위의 타입 캐스팅 과정을 생략할 수 있다.

 

new 명령어로 조회

MemberDTO 생성

public class MemberDTO {

    private String username;
    private int age;

    public MemberDTO() {
    }

    public MemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
List<MemberDTO> result = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList();

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

- 단순 값을 DTO로 바로 조회할 수 있다. 

- 생성자를 호출하듯이 new를 써주고 패키지명을 포함한 클래스명을 써준다.

 

페이징

JPA는 페이징을 다음 두 API로 추상화한다.

- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)

- setMaxResults(int maxResult) : 조회할 데이터 수

 

페이징 API 예시코드

@Entity
public class Member {

	...

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", age=" + age +
                '}';
	}	
}

- 결과를 보기 위해 toString()을 생성한다. 여기서 team은 제거한다.(무한루프)

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(1)
        .setMaxResults(10)
        .getResultList();

System.out.println("result.size = " + result.size());
for (Member member1 : result) {
    System.out.println("member1 = " + member1);
}

tx.commit();

- 총 10개의 결과를 확인할 수 있다.

- 오라클을 사용하면 페이징 쿼리가 기본 deth 3까지 나오는 긴 쿼리가 발생한다.

- 따라서 추상적인 설계만 하고 구체적인 라이브러리는 프레임워크에게 위임해서 코드를 짜면 된다.

- setFirstResult와 setMaxResults 두 API로 간단하게 페이징 쿼리를 처리할 수 있다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 객체지향 쿼리 언어2 - 중급 문법(1)  (0) 2024.07.21
[JPA] 객체지향 쿼리 언어1 - 기본 문법(2)  (0) 2024.07.20
[JPA] 값 타입(2)  (0) 2024.07.18
[JPA] 값 타입(1)  (0) 2024.07.17
[JPA] 영속성 전이와 고아 객체  (0) 2024.07.16