- 객체 지향 쿼리 소개
- JPQL
- Criteria
- QueryDSL
- 네이티브 SQL
- 객체지향 쿼리 심화
10.1 객체지향 쿼리 소개
- 식별자로 조회 EntityManager.find()
- 객체 그래프 탐색 ex. a.getB().getC()
- 데이터는 데이터베이스에 있으므로 SQL로 필요한 내용을 최대한 걸러서 조회해야 한다. 하지만 ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다. JPQL은 이런 문제를 해결하기 위해 만들어졌다.
- JPQL 특징
: 테이블이 아닌 객체 대상으로 검색하는 객체지향 쿼리
: SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
: JPA는 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.
- JPA 공식 지원 기능
: JPQL(Java Persistence Query Language)
: Criteria 쿼리: JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
: 네이티브 SQL: JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.
- JPA 공식 지원하는 기능은 아니지만 유용한 기능
: QueryDSL: Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크다.
: JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용: 필요하면 JDBC를 직접 사용할 수 있다.
10.1.1 JPQL 소개
- 엔티티 객체를 조회하는 객체지향 쿼리다.
- SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
- 데이터베이스 방언(Dialect)만 변경하면 JPQL을 수정하지 않아도 자연스럽게 데이터베이스를 변경할 수 있다.
- JPQL은 SQL보다 간결하다. 엔티티 직접 조회, 묵시적 조인, 다형성 지원으로 SQL보다 코드가 간결하다.
// 쿼리 생성
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
10.1.2 Criteria 쿼리 소개
- Criteria는 JPQL을 생성하는 빌더 클래스다. Criteria의 장점은 문자가 아닌 query.select(m).where(...)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다. 컴파일 시점에 오류를 발견할 수 있다.
- 문자로 작성한 JPQL보다 코드로 작성한 Criteria의 장점
: 컴파일 시점에 오류를 발견할 수 있다.
: IDE를 사용하면 코드 자동완성을 지원한다.
: 동적 쿼리를 작성하기 편하다.
- Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다. 따라서 사용하기 불편한 건 물론이고 Criteria로 작성한 코드도 한눈에 들어오지 않는다는 단점이 있다.
// Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
// 루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
// 쿼리 생성
CriteriQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"));
List<Member> resultList = em.createQuery<C1).getResultList();
10.1.3 QueryDSL 소개
- QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다.
- QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 그리고 작성한 코드도 JPQL과 비슷해서 한눈에 들어온다. QueryDSL과 Criteria를 비교하면 Criteria는 너무 복잡하다.
QueryDSL은 JPA 표준은 아니고 오픈소스 프로젝트다. 이것은 JPA뿐만 아니라 JDO, 몽고DB, Java Collection, Lucene, Hibernate Search도 거의 같은 문법으로 지원한다.
- QueryDSL도 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어야 한다. QMember는 Member 엔티티 클래스를 기반으로 생성한 QueryDSL 쿼리 전용 클래스다.
// 준비
JPAQuery query = new JPAQuery(em);
QMember member= QMEMBER.MEMBER;
// 쿼리, 결과조회
List<Member> members = query.from(member).where(member.username.eq("kim")).list(member);
10.1.4 네이티브 SQL 소개
- JPQ는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL이라 한다.
- SQL은 지원하지만 JPQL이 지원하지 않는 기능을 사용하고 싶을 때 네이비트 SQL 사용
- 네이티브 SQL의 단점은 특정 데이터베이스에 의존하는 SQL을 작성해야해서 데이터베이스를 변경하면 SQL도 수정해야한다.
- 네이티브 SQL은 em.createNativeQuery()를 사용하면 된고 나머지 API는 JPQL과 같다. 실행하면 직접 작성한 SQL을 대이터베이스에 전달한다.
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
10.1.5 JDBC 직접 사용, 마이바티스 같은 SQL 매퍼 프레임워크 사용
- JDBC 커넥션에 직접 접근하고 싶으면 JPA는 JDBC 커넥션을 획득하는 API를 제공하지 않으므로 JPA 구현체가 제공하는 방법을 사용
- 하이버네이트에서 직접 JDBC Connection을 획득하는 방법은 아래 코드 참고
Session session = entityManger.unwrap(Session.class);
session.doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
// work...
}
});
- JDBC나 마이바티스를 JPQ와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.
- JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.
10.2 JPQL
- JPQL 특징
: 객체지향 쿼리 언어다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
: SQL을 추상화새서 특정 데이터베이스에 SQL에 의존하지 않는다.
: JPQL은 결국 SQL로 변환된다.
10.2.1 기본 문법과 쿼리 API
- JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다. 참고로 엔티티를 저장할 때는 EntityManger.persist() 메소드를 사용하면 되므로 INSERT 문은 없다.
- SELECT 문
: 대소문자 구분 - JPQL 키워드는 대소문자를 구분하지 않는다.
: 엔티티 이름
: 별칭(식별 변수 Identification variable)은 필수
- TypeQuery, Query
: 작성한 JPQL을 실행하려면 쿼리 객체를 만들어야 한다.
: 쿼리 객체는 TypeQuery와 Query가 있는데 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 객체를 사용하고, 반환 타입을 명확하게 지정할 수 없으면 Query 객체를 사용하면 된다.
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m", Mebmer.class);
Query query = em.createQuery("SELECT m.username, m.age from Member m");
- 결과 조회
: query.getResultList()
: query.getSingleResult() - 결과가 정확히 하나일 때 사용. 결과가 없거나 1개보다 많으면 예외 발생
10.2.2 파라미터 바인딩
- 이름 기준 파라미터: 파라미터를 이름으로 구분하는 방법. 이름 기준 파라미터는 앞에 :를 사용한다.
- 위치 기준 파라미터: ? 다음 위치 값을 주면된다.
// 이름 기준 파라미터
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
.setParameter("username", usernameParam)
.getResultList();
// 위치 기준 파라미터
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
- JPQL을 수정해서 다음 코드처럼 파라미터 바인딩 방식을 사용하지 않고 직접 문자를 더해 만들어 넣으면 악의적인 사용자에 의해 SQL 인젝션 공격을 당할 수 있다. 또한 성능 이슈도 있는데 파라미터 바인딩 방식을 사용하면 파라미터 값이 달라도 같은 쿼리로 인식해서 JPA는 JPQL을 SQL로 파싱한 결과를 재사용할 수 있다. 그리고 데이터베이스 내부에서 실행한 SQL을 파싱해서 사용하는데 같은 쿼리는 파싱한 결과를 재사용할 수 있다. 결과적으로 애플리케이션과 데이터베이스 모두 해당 쿼리의 파싱 결과를 재사용할 수 있어서 전체 성능이 향상된다.
-> 결론: 파라미터 바인딩 방식은 선택이 아닌 필수다.
// 파라미터 바인딩 방식을 사용하지 않고 직접 JPQL을 만들면 위험하다.
"select m from Member m where m.username = '" + usernameParam + "'"
10.2.3 프로젝션
- SELECT 절에 조회할 대상을 지정하는 것을 프로젝션(projection)이라하고, [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다.
- 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입이 있다. 스칼라 타입은 숫자, 문자 등 기본 데이터 타입을 뜻한다.
- 엔티티 프로젝션: 조회한 엔티티는 영속선 컨텍스트에서 관리된다.
- 임베디드 타입 프로젝션: 조회의 시작점이 될 수 없다는 제약이 있다. 임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
- 스칼라 타입 프로젝션: 숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
- 여러 값 조회: 엔티티를 대상으로 조회하면 펀리하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다. 프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.
- NEW 명령어: 두 필드를 프로젝션해서 타입을 지정할 수 없으므로 TypeQuery를 사용할 수 없다. 실제 애플리케이션 개발시에는 Object[]를 직접 사용하지 않고 DTO처럼 의미 있는 객체로 변환해서 사용할 것이다.
TypeQuery<UserDTO> query = em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
- NEW 명령어 사용할 때 2가지 주의 사항
1) 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
2) 순서와 타입이 일치하는 생성자가 필요하다.
10.2.4 페이징 API
- JPA는 페이징을 다음 두 API로 추상화했다.
: setFistResult(int startPosition): 조회 시작 위치(0부터 시작한다)
: setMaxResults(int maxResult): 조회할 데이터 수
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
query.setFistResult(10);
query.setMaxResults(20);
query.getResultList();
- 데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언(Dialect) 덕분이다.
- 페이징 SQL을 더 최적화하고 싶다면 JPA가 제공하는 페이징 API가 아닌 네이티브 SQL을 직접 사용해야 한다.
10.2.5 집합과 정렬
- 집합은 집합 함수와 함께 통계 정보를 구할 때 사용한다.
- 집합 함수: COUNT, MAX, MIN, AVG, SUM
- GROUP BY 통계 데이터를 구할 때 특정 그룹끼리 묶어준다.
- HAVING 그룹화한 통계 데이터 기준으로 필터링
- ORDER BY, ASC, DESC
10.2.6 JPQL 조인
- 내부 조인: INNER JOIN 사용, INNER 생략 가능
- 외부 조인: LEFT OUTER JOIN, RIGH OUTER JOIN, OUTER 생략 가능
- 컬렉션 조인: 일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
- 세타 조인: WHERE 절을 사용해서 세타 조인을 할 수 있음. 세타 조인은 내부 조인만 지원한다.
- JOIN ON: ON 절을 사용하면 조인 대상을 필터링 하고 조인할 수 있다. 내부 조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용한다.
10.2.7 페치 조인
- 페치(fetch) 조인은 JPQL에서 성능 최적화를 위해 제공하는 기능이다. 이것은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어로 사용할 수 있다.
- JPA 표준 명세에 정의된 페치 조인 문법은 다음과 같다.
페치 조인 :: = [LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
- 엔티티 페치 조인: 페치 조인은 별칭 사용할 수 없음, 하이버네이트는 페치 조인에도 별칭 허용
- 지연 로딩으로 설정해도 페치 조인으로 조회하면 연관된 엔티티가 프록시가 아닌 실제 엔티티다. 프록시가 아닌 실제 엔티티이므로 회원 엔티티가 영속성 컨텍스트에서 분리되어 준영속 상태가 되어도 연관된 팀을 조회할 수 있다.
- 컬렉션 페치 조인: 일대다 관계인 컬렉션을 페치 조인
일대다 조인은 결과가 증가할 수 있지만 일대일, 다대일 조인은 결과가 증가하지 않는다.
- 페치 조인과 DISTINCT
- 페치 조인과 일반 조인의 차이: JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다. 단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
// 내부 조인 JPQL
select t
from Team t join t.members m
where t.name = '팀A'
// 실행된 SQL
SELECT T.* FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'
// 컬렉션 페치 조인 JPQL
select t
from Team t join ㄹㄷㅅ초 t.members
where t.name = '팀A'
// 실행된 SQL
SELECT T.*, M.*
FROM TEAM T
INNER JOIN MEMBER M ON T.ID = M.TEAM_ID
WHERE T.NAME = '팀A'
- 페치 조인의 특징과 한계
: 페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다. 페치 조인은 글로벌 로딩 전략보다 우선한다. 글로벌 로딩 전략은 될 수 있으면 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 것이 효과적이다.
: 페치 조인을 사용하면 연관된 엔티티를 쿼리 시점에 조회하므로 지연 로딩이 발생하지 않는다. 따라서 준영속 상태에서도 객체 그래프를 탐색할 수 있다.
: 페치 조인의 한계
1) 페치 조인 대상에는 별칭을 줄 수 없다.
2) 둘 이상을 컬렉션을 페치할 수 없다.
3) 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
10.2.8 경로 표현식
- 경로 표현식이란 .(점)을 찍어 객체 그래프를 탐색하는 것이다.
select m.username from Member m
join m.team t
join m.orders o
where t.name = '팀A'
- 경로 표현식의 용어 정리
: 상태 필드 - 단순히 값을 저장하기 위한 필드(필드 or 프로퍼티)
: 연관 필드 - 연관관계를 위한 필드, 임베디드 타입 포함(필드 or 프로퍼티), 단일 값 연관 필드, 컬렉션 값 연관 필드
- 경로 표현식과 특징
: 상태 필드 경로: 경로 탐색 끝. 더는 탐색 불가
: 단일 값 연관 경로: 묵시적으로 내부 조인. 단일 값 연관 경로는 계속 탐색 가능
: 컬렉션 값 연관 경로: 묵시적으로 내부 조인. 더는 탐색할 수 없다. 단 FROM 절에서 조인을 통해 별칭을 얻으면 별칭으로 탐색 가능
묵시적 조인
단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라 한다. 참고로 묵시적 조인은 모두 내부 조인이다.
- 명시적 조인
: JOIN을 직접 적어주는 것
: ex. SELECT m FROM Member m JOIN m.team t
- 묵시적 조인
: 경로 표현식에 의해 묵시적으로 조인이 일어나는 것, 내부 조인 INNER JOIN만 할 수 있다.
: ex. SELECT m.team FROM Member m
: 컬렉션 값 연관 경로 탐색 시작은 허락하지 않는다. 만약 컬렉션에서 경로 탐색을 하고 싶으면 조인을 사용해서 새로운 별칭을 획득해야 한다. 컬렉션 크기를 구할 수 있는 size라는 특별한 기능을 사용할 수 있다. size를 사용하면 COUNT 함수를 사용하는 SQL로 적절히 변환된다.
select t.member from Team t // 성공
select t.member.username from Team t // 실패
select m.username from Team t join t.members m
select t.member.size from Team t
- 경로 탐색을 사용한 묵시적 조인 시 주의사항
: 항상 내부 조인이다.
: 컬렉션은 경로 탐색의 끝이다. 컬렉션에서 경로 탐색을 하려면 명시적으로 조인해서 별칭을 얻어야 한다.
: 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM 절에 영향을 준다.
10.2.9 서브 쿼리
- 서브 쿼리를 WHERE, HAVING 절에서만 사용할 수 있고 SELECT, FROM 절에서는 사용할 수 없다.
- 서브 쿼리 함수
: [NOT] | EXISTS (subquery)
: {ALL | ANY | SOME} (subquery)
: [NOT] IN (subquery)
10.2.10 조건식
- 타입 표현
- 연산자 우선 순위
- 논리 연산과 비교식
- Between, IN, Like, NULL 비교
- 컬렉션 식: IS [NOT[ EMPTY, [NOT] MEMBER [OF]
- 스칼라식: 문자함수, 수학함수, 날짜함수
- CASE 식: 기본 CASE, 심플 CASE, COALESCE, NULLIF
10.2.11 다형성 쿼리
- TYPE
: TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.
// JPQL
select i from Item i
where type(i) IN (Book, Movie)
//SQL
SELECT i FROM Item i
WHERE i.DTYPE in ('B', 'M;)
- TREAT(JPA 2.1)
: TREAT은 JPA 2.1에 추가된 기능인데 자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
: JPQL을 보면 treat를 사용해서 부모 타입인 Item을 자식 타입인 Book으로 다룬다. 따라서 author 필드에 접근할 수 있다.
// JPQL
select i from Item i
where treat(i as Book).author = 'kim'
//SQL
SELECT i.* FROM Item i
WHERE i.DTYPE 'B' and i.author=kim'
10.2.12 사용자 정의 함수 호출(JPA 2.1)
- 문법: function_invocation::= FUNCTION(function_name {, function_arg}*)
- ex. select function('group_concat', i.name) from Item i
10.2.13 기타 정리
- enum은 == 비교 연산만 지원한다.
- 임베디드 타입은 비교를 지원하지 않는다.
- EMPTY STRING
- NULL 정의
10.2.14 엔티티 직접 사용
- 기본 키 값
: 객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별한다. 따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
select count(m.id) from Member m // 엔티티의 아이디를 사용
select count(m) from Member m // 엔티티를 직접 사용
// 실행되는 SQL
select count(m.id) as cnt from Member m
// 엔티티를 파라미터로 직접 받는 코드
String qlString = "select m from Member m where m = :member";
List resultList = em.createQuery(qlString).setParameter("member", member).getResultList();
// 실행되는 SQL
select m.* from Member m where m.id=?
- 외래 키 값
: 엔티티를 파라미터로 사용해도 외래 키와 매핑된다. m.team을 사용 하든 m.team.id를 사용하든 생성되는 SQL은 같다.
10.2.15 Named 쿼리: 정적 쿼리
- 동적 쿼리: em.createQuery("select ..") 처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다. 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.
- 정적 쿼리: 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다. Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리다. Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다. 따라서 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점에 있다. 그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에도 도움이 된다. Named 쿼리는 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 또는 XML 문서에 작성할 수 있다.
- Named 쿼리를 어노테이션에 정의
: Named 쿼리는 이름 그대로 쿼리에 이름을 부야해서 사용하는 방법
: 하나의 엔티티에 2개 이상의 Named 쿼리를 정의하려면 @NamedQueries 어노테이션을 사용하면 된다.
// @NamedQuery 어노테이션으로 Named 쿼리 정의
@Entity
@NamedQuery(
name = "Member.findByUsername", // 쿼리 이름 부여, Named 쿼리는 영속성 유닛 단위로 관리되므로 충돌 방지하기 위해 엔티티 이름을 앞에 주었다.
query = "Select m from Member m where m.username = :username" // 사용할 쿼리 입력
)
public class Member {
...
}
// @NamedQuery 어노테이션
@Target({TYPE})
public @interface NamedQuery {
String name(); // Named 쿼리 이름 (필수)
String query(); // JPQL 정의 (필수)
LockModeType lockMode() default NONE; // 쿼리 실행 시 락모드를 설정할 수 있다.
QueryHint[] hints() default {}; // JPA 구현체에 쿼리 힌트를 줄 수 있다. ex) 2차 캐시
}
- Named 쿼리를 XML에 정의
: 어노테이션을 사용하는 것이 직관적이고 편리하지만 Named 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다.
- 환경에 따른 설정
: XML과 어노테이션에 같은 설정이 있으면 XML이 우선권을 가진다. 예를 들어 같은 이름의 Named 쿼리가 있으면 XML에 정의한 것이 사용된다. 따라서 애플리케이션 운영 환경에 따라 다른 쿼리를 실행해야 한다면 각 환경에 맞춘 XML을 준비해 두고 XML만 변경해서 배포하면 된다.
'JPA > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
Chap.12 스프링 데이터 JPA (0) | 2021.07.19 |
---|---|
Chap.10 객체지향 쿼리 언어 - 2 (0) | 2021.07.05 |
Chap.9 값 타입 (0) | 2021.06.19 |
Chap.8 프록시와 연관관계 관리 (0) | 2021.06.03 |
Chap.7 고급 매핑 (0) | 2021.05.25 |