Spring Boot에서 JPA 쓸 때 작업 흐름
1) Entity 작성
@Entity
data class FranInfoEntity(
@Id val id: Long,
val name: String
)
이 엔티티의 val들이 있어야... findBy~같은게 동작하게 된다.
2) Repository 인터페이스 정의
interface FranInfoRepository : JpaRepository<FranInfoEntity, Long> {
fun findByName(name: String): FranInfoEntity?
@Query("select f from FranInfoEntity f where f.status = :status")
fun findActive(status: String): List<FranInfoEntity>
}
interface로 만들면 구현클래스를 자동으로 만들어준다. 누가? Spring Data JPA가.
얘는 또 다음과 같은 일을 한다.
- 메서드 이름으로 쿼리 자동 생성해주기
- @Query 어노테이션으로 JPQL 작성해서 쿼리 작동하기 (JPQL 안 쓰고 native query를 쓰는 방법도 있음! 하단 정리)
3) Spring Data JPA가 자동으로 구현체를 만들어줌
너는 구현 클래스를 만들 필요 없음.
선언한 메서드만 있으면 됨.
Hibernate는 JPA를 구현한 실제 ORM 엔진
Hibernate는 JPA를 직접 구현한 실제 라이브러리다.
- 엔티티를 DB와 매핑해주고
- SQL을 만들어 실행하고
- 영속성 컨텍스트를 관리하고
- 지연 로딩을 처리하고
- dirty checking을 처리한다
즉, 실제로 DB와 통신하는 주체는 Hibernate다.
Spring Boot JPA 스타터를 쓰면
기본 ORM 엔진이 Hibernate로 설정되어 있는 이유도 이것.
1) 개발자가 Repository 인터페이스 선언
(Sprint Data JPA 기능)
2) Spring Data JPA가 Repository 구현체 자동 생성
→ 내부에서 JPA API(EntityManager)를 사용함
3) JPA(EntityManager)가 동작 요청을 Hibernate에게 전달
→ Hibernate가 실제 SQL 생성 + DB 수행
4) Hibernate가 DB에서 조회 후 Entity 생성
→ JPA로 반환
→ Spring Data JPA Repository로 전달
Spring Data JPA는 또 다른 레이어이다
Spring Data JPA는 Hibernate가 아니다.
JPA도 아니다.
Spring Data JPA가 해주는 일:
✔ Repository 인터페이스만 만들면
자동으로 구현체를 만들어준다
interface FranInfoRepository : JpaRepository<FranInfoEntity, Long>
이걸 Spring이 자동으로 내부에서 구현체를 만들어 등록한다.
구현체란??
인터페이스에서 정의한 함수들을 실제로 구현한 클래스를 말한다.
한 문장으로 끝내면:
인터페이스 = 약속(함수만 정의) 구현체 = 그 약속을 실제 코드로 만든 클래스
✔ 메서드 이름으로 쿼리를 자동 생성
fun findByName(name: String): FranInfoEntity?
이 문법은 JPA가 아니라 Spring Data JPA 기능이다.
✔ @Query 지원
JPQL, Native Query를 repository에 붙일 수 있게 해주는 것도 Spring Data JPA다.
그럼 Spring Data JPA에서 말하는 “구현체 자동 생성”이란?
너는 Repository를 이렇게 인터페이스만 만든다:
interface FranInfoRepository : JpaRepository<FranInfoEntity, Long> {
fun findByName(name: String): FranInfoEntity?
}
너는 구현 클래스를 만들지 않았는데도 실행이 된다. 왜?
Spring Data JPA가 런타임에 이 인터페이스의 “구현체”를 자동으로 만들어주기 때문
즉, 내부적으로 다음 같은 클래스가 자동 생성되는 셈이다:
class FranInfoRepositoryImpl : FranInfoRepository {
override fun findByName(name: String): FranInfoEntity? {
// Hibernate/JPA 사용해서 실제 SQL 생성 + 실행
}
}
우리는 이 클래스를 직접 만들 필요 없음.
스프링이 자동으로 만들어서 빈으로 등록해준다.
>>> Spring Data JPA : Repository 인터페이스를 보고 구현체를 자동 생성하는 기술
JPA 메서드 네이밍 규칙과 엔티티 구조
엔티티의 키 정의 방식(단일 키 vs 복합키)
1. find << PK가 단일칼럼
- findNameAndPhoneByNameIdAndCreatedAt 라는 메서드가 정상 작동했다는 건,
엔티티 클래스의 필드에 nameId, createdAt 라는 속성이 직접 존재한다는 의미다.
즉 엔티티 구조가 대략 이런 식일 거다:
@Entity
class User(
@Id
val id: Long,
val nameId: String, // 직접 필드
val createdAt: LocalDateTime
)
이 경우 JPA는 nameId, createdAt 이라는 직접 필드명을 그대로 메서드 이름에서 사용 가능하다.
2. findById_ << PK가 복합칼럼 (이걸 복합키라고 함)
- findById_CreatedAtBetweenAndId_NameId 처럼 Id_ prefix를 붙여야 작동한다는 건,
해당 엔티티의 기본키(==DB의 PK)가 EmbeddedId 또는 @IdClass로 정의되어 있기 때문이다.
즉 엔티티가 이런 식일 가능성이 크다:
@Entity
class User(
@EmbeddedId
val id: UserId, // 복합키로 묶여있음
)
@Embeddable
class UserId(
val nameId: String,
val createdAt: LocalDateTime
)
이 경우 JPA 네이밍 규칙에서는 id라는 필드를 먼저 타고 들어가야 하므로 >>> 클래스 UserId의 nameId를 접근해야함
- UserId의 nameId를 접근하는 방법 :::: id.nameId 이죠??? 이걸 JPQL로 표현하면 Id_NameId 일케됨
- id.createdAt도 마찬가지로 Id_CreatedAt 이런 모양으로.
그래서 메서드 이름도
fun findById_CreatedAtBetweenAndId_NameId(...)
처럼 작성해야 작동
>>>> 이런 모양은 실제 MySQL DB에서는 이런 모양일 것이다................
PK가 nameID와 createdAt이라는 두 개의 칼럼을 합친 모양일 것 !
@Query에서 JPQL 안 쓰고 native Query쓰는 방법
@Repository
interface UserRepository : JpaRepository<User, Long> {
// nativeQuery = true 설정 필요
@Query(
value = "SELECT * FROM user u WHERE u.email = :email",
nativeQuery = true
)
fun findByEmail(@Param("email") email: String): User?
}
주의할 점
- 엔티티 매핑을 무시하고, DB 칼럼명을 그대로 써야 한다.
- DB 의존성이 강하므로, 이식성이 떨어질 수 있다.
- 결과를 엔티티가 아닌 DTO로 받고 싶다면 @SqlResultSetMapping 또는 Projection을 고려해야 한다.
Spring Data JPA의 비교(Query Derivation) 키워드
between
Spring Data JPA의 findBy…CreatedAtBetween 문법은 LocalDate(또는 LocalDateTime) 두 개를 받으면 자동으로 “between 조건”을 만들어서 SQL로 변환한다
fun findByCreatedAtBetween(start: LocalDate, end: LocalDate): List<Entity>
그러면 Spring Data JPA는 자동으로 아래 조건을 만든다:
created_at >= start
AND
created_at <= end
JPQL이라면 이런모습
select e from Entity e
where e.createdAt between :start and :end
Equal / NotEqual
Equal (기본 동작)
findByName(String name)
→ WHERE name = ?
Not
findByNameNot(String name)
→ WHERE name != ?
In / NotIn
fun findByIdIn(ids: List<Long>)
fun findByIdNotIn(ids: List<Long>)
GreaterThan / GreaterThanEqual
숫자, 날짜, 시간 모두 가능
→ created_at > time
→ created_at >= time
LessThan / LessThanEqual
→ created_at < time
→ created_at <= time
Before / After (날짜 전용)
LocalDate, LocalDateTime, Instant 주로 사용한다.
→ created_at < time
→ created_at > time
IsNull / IsNotNull
→ deleted_at IS NULL
Like / NotLike / StartingWith / EndingWith / Containing
문자열 검색에 사용된다.
→ "Kim%" 자동 적용
→ "%Kim" 자동 적용
→ "%Kim%" 자동 적용
True / False (Boolean 필드)
IgnoreCase
문자열 비교 시 대소문자를 무시한다.
fun findByNameContainingIgnoreCaseAndCreatedAtAfter(
name: String,
time: LocalDateTime
)
근데 그러면 이름이 너무 복잡하고 길어지잖아?
그래서 나온 게 QueryDSL이니...
QueryDSL
QueryDSL은 문자열 JPQL을 직접 쓰지 않고, 코드로 SQL/JPA를 작성할 수 있게 해주는 DSL이다.
1) 타입 안전(Type-safe)
문자열 "user.name" 대신
코드로 QUser.user.name 형태로 사용한다.
오타나 잘못된 컬럼명을 컴파일 시점에 잡아준다.
2) 동적 쿼리 작성이 쉽다
조건이 optional이어도
조건 있는 것만(not null인 것만) 누적해서 where 절에 넣으면 된다. >> 그래서 builder로 돌리면
val builder = BooleanBuilder()
if (name != null) {
builder.and(user.name.containsIgnoreCase(name))
}
if (start != null && end != null) {
builder.and(user.createdAt.between(start, end))
}
return queryFactory
.selectFrom(user)
.where(builder)
.fetch()
3) 짧고 명확한 JOIN 쿼리 가능
queryFactory
.select(order)
.from(order)
.join(order.member, member).fetchJoin()
.where(member.name.eq("kim"))
.fetch()
쿼리DSL의 예시를 봐보자
엔티티 예제가 이렇다면...
@Entity
class User(
@Id val id: Long,
val name: String,
val status: String,
val createdAt: LocalDateTime
)
쿼리 DSL은 이렇게 생겼다
val qUser = QUser.user
return queryFactory
.selectFrom(qUser)
.where(
qUser.name.containsIgnoreCase("kim"),
qUser.status.eq("ACTIVE"),
qUser.createdAt.after(LocalDateTime.now().minusDays(7))
)
.fetch()
네이밍이라면 이렇게된다... findByNameContainingIgnoreCaseAndStatusAndCreatedAtAfter
ㄷㄷ
적용은 이렇게
@Repository
class QueryRepository(
private val queryFactory: JPAQueryFactory
) {
fun fetchColumn(): List<ColumnView> {
val user = QUserEntity.userEntity
val fetch = queryFactory.select (
Projections.fields(
UserView::class.java,
user.id,
user.name
)
)
.from(user)
.fetch()
return fetch
}
'┝ DB' 카테고리의 다른 글
| outer join (0) | 2022.02.20 |
|---|
