[자바 ORM 표준 JPA] JPA 객체지향 쿼리 언어 소개
[자바 ORM 표준 JPA] JPA 객체지향 쿼리 언어 소개
객체지향 쿼리 언어 (JPQL)
목차#
- 객체지향 커리 언어 소개
- JPQL
- 기본 문법과 기능
- 패치 조인
- 경로 표현식
- 다형성 쿼리
- 엔티티 직접 사용
- Named 쿼리
- 벌크 연산
객체지향 쿼리 언어 소개#
JPA에서 제공되는 다양한 쿼리 방법#
JPA에서는 실무에서 사용하기 위한 다양한 검색 조건과 Join을 사용하여 조회 등 복잡한 쿼리를 구현할 수 있도록 기능 다양한 쿼리 방법을 제공합니다.
- JPQL 엔티티 객체를 조회하는 객체지향 쿼리다.
- JPA Criteria
- QueryDSL Java로 코드를 작성하여 JPQL로 제너레이션 하여 빌드하게 도와주는 클래스의 모음입니다.
- 네이티브 SQL JPQL 외에 특정 데이터베이스에 종속적인 쿼리를 작성하고 사용해야할때 사용(MySql, Oracle.. 쿼리)
- JDBC API 직접사용, Mybatis, SpringJdbcTemplate 함께 사용
JPQL#
JPQL 소개#
- 가장 단순한 조회 방법
- EntityManager.find()
- 객체 그래프 탐색 (a.getB().getC())
- 나이가 18살 이상인 회원을 모두 검색 하고 싶다면 ?
JPQL -1#
- JPA를 사용하면 엔티티 객체를 중심으로 개발
- 문제는 검색 쿼리
- 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
- 에플리케이션이 필요한 데이터만 DB에서 불러오면 결국 검색 조건이 포함된 SQL이 필요
JPQL -2#
- JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
- SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
- JPQL은 엔티티 객체를 대상으로 쿼리
- SQL은 데이터베이스 테이블을 대상으로 쿼리
JPQL - 예제#
String jpql = "select m from Member m where m.name like '김%'";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
이전 소스#
Address.java
package relativemapping;
import javax.persistence.Embeddable;
import java.util.Objects;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
public Address() {
}
public Address(String city, String street, String zipcode) {
this.city = city;
this.street = street;
this.zipcode = zipcode;
}
public String getCity() {
return city;
}
private void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
private void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
private void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
}
AddressEntity.java
package relativemapping;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.security.PrivateKey;
@Entity
@Table(name = "ADDRESS")
public class AddressEntity {
public AddressEntity() {
}
public AddressEntity(String city, String street, String zipcode) {
this.address = new Address(city,street,zipcode);
}
@Id @GeneratedValue
private Long id;
private Address address;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
Album.java
package relativemapping;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
@DiscriminatorValue("A")
public class Album extends Item{
private String artist;
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
}
BaseEntity.java
package relativemapping;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
public abstract class BaseEntity {
@Column(name = "REG_ID")
private String createBy;
@Column(name = "REG_DT")
private LocalDateTime createDate;
@Column(name = "MOD_ID")
private String LastModifiedBy;
@Column(name = "MOD_DT")
private LocalDateTime LastModifiedDate;
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public LocalDateTime getCreateDate() {
return createDate;
}
public void setCreateDate(LocalDateTime createDate) {
this.createDate = createDate;
}
public String getLastModifiedBy() {
return LastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
LastModifiedBy = lastModifiedBy;
}
public LocalDateTime getLastModifiedDate() {
return LastModifiedDate;
}
public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
LastModifiedDate = lastModifiedDate;
}
}
Book.java
package relativemapping;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("B")
public class Book extends Item{
private String author;
private String isbn;
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
}
Child.java
package relativemapping;
import javax.persistence.*;
@Entity
public class Child {
public Child() {
}
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
Item.java
package relativemapping;
import javax.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
Member.java
package relativemapping;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
public class Member {
public Member(){
}
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Embedded
private Address homeAddress;
@ElementCollection
@CollectionTable(name = "FAVORITE_FOOD", joinColumns =
@JoinColumn(name = "MEMBER_ID") // JoinColumn을 세팅하면 해당 키를 외래키로 사용합니다.
)
@Column(name = "FOOD_NAME ")
private Set<String> favoriteFoods = new HashSet<>();
public Long getId() {
return id;
}
/*
@OrderColumn(name = "address_history_order")
@ElementCollection
@CollectionTable(name = "ADDRESS" , joinColumns =
@JoinColumn(name = "MEMBER_ID")
)
private List<Address> addressHistory = new ArrayList<>();
*/
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "MEMBER_ID")
private List<AddressEntity> addressHistory = new ArrayList<>();
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getHomeAddress() {
return homeAddress;
}
public void setHomeAddress(Address homeAddress) {
this.homeAddress = homeAddress;
}
public Set<String> getFavoriteFoods() {
return favoriteFoods;
}
public void setFavoriteFoods(Set<String> favoriteFoods) {
this.favoriteFoods = favoriteFoods;
}
public List<AddressEntity> getAddressHistory() {
return addressHistory;
}
public void setAddressHistory(List<AddressEntity> addressHistory) {
this.addressHistory = addressHistory;
}
}
MemberProduct.java
package relativemapping;
import javax.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "ORDERS")
public class MemberProduct {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
@Column(name = "ORDERAMOUNT")
private int orderAmount;
@Column(name = "ORDERCOUNT")
private int orderCount;
public Member getMember() {
return member;
}
@Column(name = "ORDERDATE")
private LocalDate orderDate;
public void setMember(Member member) {
this.member = member;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public void setId(Long id) {
this.id = id;
}
public Long getId() {
return id;
}
}
Movie.java
package relativemapping;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
private String director;
private String actor;
public String getDirector() {
return director;
}
public void setDirector(String director) {
this.director = director;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
}
Parent.java
package relativemapping;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Parent {
public Parent() {
}
@Id
@GeneratedValue
@Column(name = "parent_id")
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child){
childList.add(child);
child.setParent(this);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Child> getChildList() {
return childList;
}
public void setChildList(List<Child> childList) {
this.childList = childList;
}
}
Period.java
package relativemapping;
import javax.persistence.Embeddable;
import java.time.LocalDateTime;
@Embeddable
public class Period {
private LocalDateTime startDate;
private LocalDateTime endDate;
public Period() {
}
public Period(LocalDateTime startDate, LocalDateTime endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
public Boolean isIncumbent(){
LocalDateTime today = LocalDateTime.now();
/*
System.out.println(this.startDate);
System.out.println(today);
System.out.println(this.startDate.isEqual(today)); // 주어진 시간과 같은지
System.out.println(this.startDate.isBefore(today)); // 주어진 시간보다 이전인지
System.out.println(this.startDate.isAfter(today)); // 주어진 시간보다 이후인지
System.out.println(this.endDate);
System.out.println(today);
System.out.println(this.endDate.isEqual(today)); // 주어진 시간과 같은지
System.out.println(this.endDate.isBefore(today)); // 주어진 시간보다 이전인지
System.out.println(this.endDate.isAfter(today)); // 주어진 시간보다 이후인지
*/
if( ! this.startDate.isAfter(today) && this.endDate.isAfter(today)){
return true;
}else{
return false;
}
}
public LocalDateTime getStartDate() {
return startDate;
}
private void setStartDate(LocalDateTime startDate) {
this.startDate = startDate;
}
public LocalDateTime getEndDate() {
return endDate;
}
private void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
}
Product.java
package relativemapping;
import javax.persistence.*;
import java.util.List;
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Team.java
package relativemapping;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Team extends BaseEntity{
public Team(){
}
public Team(Long id, String username){
this.id = id;
this.name = name;
}
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@Column(name = "NAME")
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
public List<Member> getMembers() {
return members;
}
public void setMembers(List<Member> members) {
this.members = members;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
JpaMain.java
package relativemapping;
import org.hibernate.Hibernate;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
public class JpaMain {
//psvm 단축키로 생성 가능
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("relativemapping");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin(); // [트랜잭션] 시작
try{
Member member = new Member();
member.setName("member1");
member.setHomeAddress(new Address("home1", "street1", "10000"));
member.getFavoriteFoods().add("치킨");
member.getFavoriteFoods().add("피자");
member.getFavoriteFoods().add("햄버거");
/* member.getAddressHistory().add(new Address("old1", "street1", "10000"));
member.getAddressHistory().add(new Address("old2", "street1", "10000"));*/
member.getAddressHistory().add(new AddressEntity("old1", "street1", "10000"));
member.getAddressHistory().add(new AddressEntity("old2", "street1", "10000"));
em.persist(member);
em.flush();
em.clear();
System.out.println("===================================");
Member findMember = em.find(Member.class, member.getId());
// home1 -> new1
//findMember.getHomeAddress().setCity("new1"); // 값 타입의 set은 사이드이펙트가 발생할 문제가 있어 set X
/*Address oldAddress = findMember.getHomeAddress();
findMember.setHomeAddress(new Address("new1", oldAddress.getStreet(), oldAddress.getZipcode()));*/
// 치킨 -> 한식
/*findMember.getFavoriteFoods().remove("치킨");
findMember.getFavoriteFoods().add("한식");*/
/*findMember.getAddressHistory().remove(new Address("old1", "street1", "10000"));
findMember.getAddressHistory().add(new Address("new2", "street1", "10000"));*/
/*
remove 내부에서 equals()를 통하여 값이 완전 똑같은 객체를 지우게 되는데, equals와 hashcode를
== 비교에서, 값 전체를 비교해 같은 값을 가지는 지로 변경하지 않으면 값이 삭제 되지 않고
계속 추가가 되는 버그를 발생시킬 수 있습니다.
*/
tx.commit();
}catch (Exception e){
e.printStackTrace();
tx.rollback();
}finally {
em.close();
}
emf.close();
}
}
JpaMain.java
...
Member member1 = new Member();
member1.setName("member1");
member1.setHomeAddress(new Address("home1", "street1", "10000"));
member1.getFavoriteFoods().add("치킨");
member1.getFavoriteFoods().add("피자");
member1.getFavoriteFoods().add("햄버거");
member1.getAddressHistory().add(new AddressEntity("old1", "street1", "10000"));
member1.getAddressHistory().add(new AddressEntity("old2", "street1", "10000"));
em.persist(member1);
em.flush();
em.clear();
System.out.println("===================================");
List<Member> members = em.createQuery(
"select m from Member m where m.name like '%m%' "
, Member.class
).getResultList();
for (Member member: members){
System.out.println("member "+ member);
}
tx.commit();
...
테이블을 대상으로 쿼리를 작성하는게 아니라 엔티티를 대상으로 쿼리를 작성하기 때문에, 컬럼의 username이 아니라 엔티티의 name 으로 조건을 작성, 결과는 테이블의 컬럼명 username 으로 만들어짐.
console
Hibernate:
/* select
m
from
Member m
where
m.name like '%김%' */ select
member0_.id as id1_6_,
member0_.city as city2_6_,
member0_.street as street3_6_,
member0_.zipcode as zipcode4_6_,
member0_.USERNAME as username5_6_
from
Member member0_
where
member0_.USERNAME like '%m%'
문법이 sql과 거의 비슷, * 대신 객체의 Alias인 m 으로 전체 조회를 할 수 있습니다.
또 m.name, m.id … 등 각각 접근하여 사용할 수 있습니다.
JPQL과 실행된 SQL#
String jpql = "select m from Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
실행된 SQL
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
Criteria#
Criteria 소개#
JPQL은 단순한 String 이기 때문에 동적 쿼리를 생성하기 힘이 듭니다.
예를 들면
String jpql = "select m from Member";
String username;
if(username != null){
String sWhere = "where m.name like '%m%'";
jpql += sWhere;
}
List<Member> result = em.createQuery(jpql, Member.class).getResultList();
간단한 조건 하나 추가하여도, 지저분해지는 것을 알 수 있습니다.
실무에서 사용하기에 버그도 많이 발생하고 불편한 점이 있습니다.
Ibatis나 MyBatis는 동적쿼리를 작성하는데 장점이 있습니다.
그래서 동적쿼리 뿐만 아니지만, 대안으로 자바 표준으로 Criteria가 나오게 되었습니다.
// 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("name"), "kim hun")); // 엔티티의 속성명 name
List<Member> resultList = em.createQuery(cq).getResultList();
JpaMain.java - Criteria 사용
// 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("name"), "member1"));
List<Member> resultList = em.createQuery(cq).getResultList();
tx.commit();
console
Hibernate:
/* select
generatedAlias0
from
Member as generatedAlias0
where
generatedAlias0.name=:param0 */ select
member0_.id as id1_6_,
member0_.city as city2_6_,
member0_.street as street3_6_,
member0_.zipcode as zipcode4_6_,
member0_.USERNAME as username5_6_
from
Member member0_
where
member0_.USERNAME=?
자바 소스로 쿼리를 작성하기 때문에 오타로 인한 오류는 IDE에서 알려주거나 오류로 알 수 있는 장점이 있고 또한 동적쿼리를 작성할때 편리함을 얻을 수 있습니다.
하지만 Criteria 방식은 복잡해 지면 좀더 분석하기 어려워집니다.
CriteriaQuery<Member> cq = query.select(m);
String username = "member1";
if (!username.equals(null)){
cq.where(cb.equal(m.get("name"), username));
}
실무에서는 쿼리를 작성했던 사람도 보기 힘들 정도로 직관성이 떨어지고, 유지 보수가 힘들어지기 때문에 사용하지 않는다고 합니다.
Criteria 정리#
- 문자가 아닌 자바 코드로 JPQL을 작성할 수 있음
- JPQL 빌더 역활
- JPA 공식 기능
- 단점 : 너무 복잡하고 실용성이 없다.
- Criteria 대신에 QueryDSL 사용 권장
QueryDSL#
QueryDSL 소개#
// JPQL
// select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAFactoryQuery(em);
QMember m = QMember.member;
List<Member> list =
query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
SQL과 비슷한 형태로 되어있는 것을 확인 할 수 있습니다.
- 문자가 아닌 나바코드로 JPQL을 작성할 수 있음
- JPQL 빌더 역할
- 컴파일 시점에 문법 오류를 찾을 수 있음
- 동적쿼리 작성 편함
- 단순하고 쉬움
- 실무 사용 권장
queryDSL은 설정이 힘든만큼 SQL과 비슷하여 작성이 쉽고 오류도 컴파일러에서 검출해주며, 동적쿼리에도 강력한 기능을 보여줍니다.
이후 queryDSL 강의에서 좀더 깊이 배우고 정리하도록 하겠습니다.
Native SQL#
Native SQL 소개#
- JPA가 제공하는 SQL을 직접 사용하는 기능
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능
- 예) 오라클 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();
console
Hibernate:
/* dynamic native SQL query */ select
*
from
Member
where
username like '%m%'
relativemapping.Member@4cafa9aa
네이티브 쿼리를 사용한다면, 네이티브 쿼리를 사용하는 것보다 다음 SpringJdbcTemplate 를 사용하거나 마이바티스 사용 추천
JDBC 직접사용, SpringJdbcTemplate 등#
JDBC 직접사용, SpringJdbcTemplate 등 소개#
- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스 등을 함께 사용가능
- 단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요
- 예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
주의해야 할 사항으로는 이런 JDBC나 Template, 마이바티스 같은 라이브러리들은 JPA와 관련이 없기 때문에
영속성 컨텍스트의 기능과 별개, 그렇기 때문에 commit 된 상태가 되어야 데이터를 조회할 수 있어서 영속성 컨텍스트를 플러시 해줘야
등록된 데이터가 사용가능하다.
커밋을 하거나, JPA의 기능을 이용한 쿼리(JPQL, Criteria, NativeQuery…) 등은 자동 플러시