객체지향 쿼리 언어 1 - 기본문법

LeeKyoungChang·2022년 3월 16일
0
post-thumbnail

자바 ORM 표준 JPA 프로그래밍 - 기본편 수업을 듣고 정리한 내용입니다.

 

📚 1. 객체지향 쿼리 언어 소개

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

✏️ JPA가 지원하는 다양한 쿼리 방법

  • JPQL : 표준 문법
  • JPA Criteria, QueryDSL : 문자가 아닌 프로그래밍 코드로 JPQL 작성
  • 네이티브 SQL : 표준 SQL 문법을 벗어나는 쿼리를 작성할 수 있는 기능 제공
  • JDBC API 직접 사용, MyBatis와 같은 SQL 매퍼 프레임워크 사용

 

📖 A. JPQL

표준 문법이다!

✔️ 소개

  • 가장 단순한 조회는 다음 두 가지를 활용하면 된다!
    • EntityManager.find()
    • 객체 그래프 탐색 (a.getB().getC())
  • 하지만 검색을 할 때 문제가 발생한다.

 

✔️ 필요성

  • JPA를 사용하면 테이블이 아닌 엔티티 객체를 중심으로 개발한다.
    • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다!
  • 결국 애플리케이션이 필요한 데이터만 DB에서 불러오려면 검색 조건이 포함된 SQL이 필요하다.

 

✔️ 특징

  • JPA는 SQL을 추상화한 JPQL 이라는 객체 지향 쿼리 언어를 제공한다.
    • JPQL은 SQL과 문법이 유사하며, 엔티티 객체를 대상으로 쿼리한다.
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
  • JPQL을 한마디로 정의하면 객체 지향 SQL이다!

 

✔️ 예제

//검색
String jpql = "select m From Member m where m.username like '%kim%'";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
  • m : Member자체

Member는 테이블이 아닌 엔티티이다.

Hibernate: 
    /* select
        m 
    From
        Member m 
    where
        m.username like '%kim%' */ select
            member0_.MEMBER_ID as MEMBER_I1_4_,
            member0_.city as city2_4_,
            member0_.street as street3_4_,
            member0_.ZIPCODE as ZIPCODE4_4_,
            member0_.USERNAME as USERNAME5_4_ 
        from
            Member member0_ 
        where
            member0_.USERNAME like '%kim%'

 

📖 B - 1. Criteria (거의 사용하지 않는다.)

문자가 아닌 자바코드로 JPQL을 작성할 수 있다.

  • JPQL 빌더 역할
  • JPA 공식 기능
  • 단점으로 너무 복잡하고 실용성이 없다.
//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> resultList = em.createQuery(cq).getResultList()

코드가 많이 복잡하다.

 

📖 B - 2. QueryDSL

문자가 아닌 자바코드로 JPQL을 작성할 수 있으며, 실용성이 없는 Criteria 대신 오픈 소스 라이브러리인 QueryDSL을 사용한다.

  • JPQL 빌더 역할
  • 쿼리를 String 문자가 아닌 자바 코드로 작성하기 때문에 컴파일 시점에 문법 오류를 찾을 수 있다.
  • 동적쿼리 작성이 편리하고 코드가 단순하고 쉽다.
  • 실무에서 많이 사용되는 방식이다.
//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery queryFactory= new JPAQueryFactory(em); 
QMember m = QMember.member;
List<Member> list = queryFactory.selectFrom(m)
                        .where(m.age.gt(18))
                        .orderBy(m.name.desc())
                        .fetch()

QMemberMember 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스이다.

➡️ CriteriaQueryDSL을 비교하였을 때, QueryDSL가 편리하고 코드가 단순하고 쉽다. (훨씬 직관적이다.)

 

📖 C. 네이티브 SQL

JPA가 제공하는 SQL을 직접 사용하는 기능

  • JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능을 사용할 때 쓰인다.
  • ex) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'"; 
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

 

📖 D. JDBC 직접 사용, SpringJdbcTemplate 등

JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, SpringJdbcTemplate, MyBatis 등을 함께 사용할 수 있다.

  • 단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시가 필요하다.
  • ex) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시

 

📚 2. 기본 문법과 쿼리 API

JPQL(Java Persistence Query Language)

📖 A. JPQL 소개

  • JPQL은 객체지향 쿼리 언어이다. → 테이블이 아닌, 엔티티 객체를 대상으로 쿼리한다.
  • JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
  • JPQL은 결국 SQL로 변환된다.
스크린샷 2022-03-15 오후 4 45 34 스크린샷 2022-03-15 오후 4 45 42

Member

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private int age;

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

 

Team

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
// getter, setter
	...
	}
}

 

Product

@Entity
public class Product {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
    private int stockAmount;
	// getter, setter
	...
	}
}

 

Order

@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;
	// getter, setter
	...
	}
}

 

Address

@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;
	// getter, setter
	...
	}
}

 

📖 B. JPQL 문법

select_문 :: = 
	select_절
	from_절 
	[where_절] 
	[groupby_절] 
	[having_절] 
	[orderby_절]
	
update_문 :: = update_절 [where_절] 
delete_문 :: = delete_절 [where_절]
  • select m from Membebr as m where m.age > 18
    • Member는 테이블이 아닌 엔티티
  • 엔티티와 속성은 대소문자 구분한다. (Member, age)
  • JPQL 키워드는 대소문자 구분하지 않는다. (SELECT, FROM, where)
  • 테이블 이름이 아닌 엔티티 이름을 사용해야 한다. (Member)
  • 별칭은 필수이다. (m) (as는 생략 가능)

 

✔️ 집합과 정렬

select
	COUNT(m), //회원수
	SUM(m.age), //나이 합
	AVG(m.age), //평균 나이 
	MAX(m.age), //최대 나이 	
	MIN(m.age) //최소 나이
from Member m

 

📖 C. TypeQuery, Query

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

//Query
Query query2 = em.createQuery("select m.username, m.age from Member m");
  • TypeQuery : 반환 타입이 명확할 때 사용한다. (두 번쨰로는 보통 엔티티를 준다.)
  • Query : 반환 타입이 명확하지 않을 때 사용한다. (호출하는 필드 값들의 타입이 각각 다를 때)

 

📖 D. 결과 조회 API

  • query.getResultList() : 결과가 하나 이상일 때, 리스트 반환
    • 결과가 없으면 빈 리스트 반환
  • query.getSingleResult() : 결과가 정확히 하나일 때, 단일 객체 반환 (값이 보장)
    • 결과가 없으면 : javax.persistence.NoResultException
    • 둘 이상이면 : javax.persistence.NonUniqueResultException

 

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

✔️ 이름 기준

SELECT m FROM Member m where m.username=:username 
query.setParameter("username", usernameParam);

 

✔️ 위치 기준

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

위치 기준은 사용하지 않는 것이 좋다.

 

📚 3. 프로젝션

프로젝션 : SELECT 절에 조회할 대상을 지정하는 것

  • 프로젝션 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)
    (1) SELECT m FROM Member m → 엔티티 프로젝션
           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();

            // 엔티티 프로젝션을 하면, `"select m from Member m"`로 인해 여러가지 데이터가 나올 수 있는데
            // 나온 데이터들이 모두 영속성 컨텍스트에 반영된다. → 엔티티 프로젝션
            Member findMember = result.get(0);
            findMember.setAge(20);

            tx.commit();

결과
스크린샷 2022-03-29 오후 5 20 48

업데이트 된 것을 확인할 수 있다.

  • Member 엔티티에서 조회하고 엔티티가 반환되었다.
  • 엔티티 프로젝션을 하면, "select m from Member m"로 인해 여러가지 데이터가 나올 수 있는데 나온 데이터들이 모두 영속성 컨텍스트에 반영된다. → 엔티티 프로젝션 (결과로 보면 20으로 나이가 업데이트 되었다.)

 

(2) SELECT m.team FROM Member m → 엔티티 프로젝션

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

            // 영속성 컨텍스트 비우기
            em.flush();
            em.clear();


            // 엔티티에서 조회하고 엔티티가 반환되었다.
            List<Team> result = em.createQuery("select m.team from Member m", Team.class).getResultList();
			// 조인을 사용해도 된다.
			List<Team> result = em.createQuery("select t from Member m join m.team t", Team.class).getResultList();

실행 결과

스크린샷 2022-03-29 오후 5 23 39
  • Member 테이블을 대상으로 내부조인이 일어난다.
  • Team이 내부조인됨

 

(3) SELECT m.address FROM Member m → 임베디드 타입 프로젝션

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

            // 영속성 컨텍스트 비우기
            em.flush();
            em.clear();


            // 엔티티에서 조회하고 엔티티가 반환되었다.
            em.createQuery("select o.address from Order o", Address.class).getResultList();

실행 결과

스크린샷 2022-03-29 오후 5 28 14
  • Order에서 Address 관련된 필드 값만 가져온다.
  • 이를 임베디드 타입 프로젝션이다. (Address가 임베디드)
스크린샷 2022-03-29 오후 5 29 57

 

(4) SELECT m.username, m.age FROM Member m → 스칼라 타입 프로젝션

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

            // 영속성 컨텍스트 비우기
            em.flush();
            em.clear();

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

스크린샷 2022-03-29 오후 5 32 17
  • 앞에 DISTINCT를 붙여서 중복을 제거할 수도 있다.
  • 어 그런데 여러 개 값을 조회하는데 타입을 어떻게 가져오는걸까?

 

✔️ 여러 값 조회

SELECT m.username, m.age FROM Member m
  1. Query 타입으로 조회
            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("result[0] = " + result[0]);
            System.out.println("age = " + result[1]);
  1. Object[] 타입으로 조회
            List<Object[]> resultList = em.createQuery("select m.username, m.age from Member m").getResultList();
            
            Object[] result = resultList.get(0);
            System.out.println("result[0] = " + result[0]);
            System.out.println("age = " + result[1]);
  1. new 명령어로 조회 (이게 제일 깔끔함, 많이 사용한다.)

MemberDTO.class 생성

public class MemberDTO {

    private String username;
    private int age;

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

 

JpaMain

            List<MemberDTO> resultList = em.createQuery("select new 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());
            
  • 단순 값을 DTO로 바로 조회 SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
  • 패키지 명을 포함한 전체 클래스명을 입력한다.
  • 순서와 타입이 일치하는 생성자가 필요하다.

 

📚 4. 페이징

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

  • setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수

 

✔️ 페이징 API 예시

//페이징 쿼리
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)         
        .setFirstResult(1)
        .setMaxResults(10)
        .getResultList();

1번째부터 10개를 가져올 것이다.

 

H2Dialect 표준 방언 : limit ? offset ?

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 ?, ?
  • mysql은 마지막에 LIMIT ?, ?로 나온다.

 

Oracle 방언

SELECT * FROM
	( SELECT ROW_.*, ROWNUM ROWNUM_
	FROM
		( 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
		 ) ROW_
		WHERE ROWNUM <= ?
		)
WHERE ROWNUM_ > ?
  • SELECT문이 여러 번 나온다.

JPA 방언으로 지정된 데이터베이스의 종류에 따라 MySQLOracle 코드가 다르다!
스프링 JPA가 페이징을 제공하므로 실제 db 쿼리에 입력하지 않아도 되니 되게 편하다는 것을 알 수 있다.

  • setFirstResult(int startPosition)setMaxResults(int maxResult) 만 지정하면 나머지는 JPA가 처리해준다.

 

📚 5. 조인

✔️ 내부 조인

SELECT m FROM Member m [INNER] JOIN m.team t
            Team team = new Team();
            team.setName("teamA");
            em.persist(team);

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



            // 영속성 컨텍스트 비우기
            em.flush();
            em.clear();


            String jpql = "select m from Member m inner join m.team t";
            List<Member> resultList = em.createQuery(jpql, Member.class)
                    .getResultList();

            tx.commit();

 

실행 결과
스크린샷 2022-03-29 오후 6 01 48

 

✔️ 외부 조인

SELECT m FROM Member m LEFT [OUTER] JOIN m.team t

            Team team = new Team();
            team.setName("teamA");
            em.persist(team);

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



            // 영속성 컨텍스트 비우기
            em.flush();
            em.clear();


            String jpql = "select m from Member m left join m.team t";
            List<Member> resultList = em.createQuery(jpql, Member.class)
                    .getResultList();

            tx.commit();
스크린샷 2022-03-29 오후 6 05 35

 

✔️ 세타 조인 - 연관관계가 없는 것을 조인하고 싶을 때 사용한다.

select count(m) from Member m, Team t where m.username = t.name

            Team team = new Team();
            team.setName("teamA");
            em.persist(team);

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



            // 영속성 컨텍스트 비우기
            em.flush();
            em.clear();


            String jpql = "select m from Member m, Team t where m.username = t.name";
            List<Member> resultList = em.createQuery(jpql, Member.class)
                    .getResultList();

            System.out.println("resultList.size() = " + resultList.size());
            tx.commit();
스크린샷 2022-03-29 오후 6 09 27

 

✔️ JOIN - ON 절을 활용한 조인
JPA 2.1 부터 지원한다.
(1) 조인 대상 필터링
(2) 연관관계 없는 엔티티 외부 조인 (하이버네이트 5.1부터)

 

📖 A. 조인 대상 필터링

ex) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인

✔️ JPQL

SELECT m, t FROM 
Member m LEFT JOIN m.team t on t.name = 'A' 
  • 회원과 팀을 조인한다.
  • on : join할 때 조건이다.
  • 팀이름이 a인 친구만 조인하고 싶다.

 

✔️ SQL

SELECT m.*, t.* FROM 
Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A' 
  • pk : m.TEAM_ID=t.id
  • t.name이 A인 친구 조인하고 싶다.

 

         Team team = new Team();
            team.setName("teamA");
            em.persist(team);

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



            // 영속성 컨텍스트 비우기
            em.flush();
            em.clear();


            String jpql = "select m from Member m left join m.team t on t.name='teamA'";
            List<Member> resultList = em.createQuery(jpql, Member.class)
                    .getResultList();

            System.out.println("resultList.size() = " + resultList.size());
            tx.commit();
스크린샷 2022-03-29 오후 6 21 19

 

📖 B. 연관관계 없는 엔티티 외부 조인

ex) 회원의 이름과 팀 이름이 같은 대상 외부 조인

✔️ JPQL

SELECT m, t FROM 
Member m LEFT JOIN Team t on m.username = t.name
  • on m.username = t.name : 조인 조건

 

✔️ SQL

SELECT m.*, t.* FROM 
Member m LEFT JOIN Team t ON m.username = t.name
  • sql 실행할 때 조인 조건이 그대로 들어간다.

연관관계가 전혀 없는 테이블을 LEFT JOIN하고 싶을 때는 on절에 삽입하면 된다.

            Team team = new Team();
            team.setName("teamA");
            em.persist(team);

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



            // 영속성 컨텍스트 비우기
            em.flush();
            em.clear();


            String jpql = "select m from Member m left join Team t on m.username = t.name";
            List<Member> resultList = em.createQuery(jpql, Member.class)
                    .getResultList();

            System.out.println("resultList.size() = " + resultList.size());
            tx.commit();
스크린샷 2022-03-29 오후 6 23 28

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글