왕초보일지

240202 TIL | validation / spring aop / querydsl

다시은 2024. 2. 2. 21:05

 🚩Validation 

읽고 다시 해보기 : https://mangkyu.tistory.com/174


 

 

 🚩Spring AOP

부가기능 모듈화. 

spring aop 는 runtime 에 프록시를 통해 객체에 접근한다.

 

dependency

		implementation("org.springframework.boot:spring-boot-starter-aop") // aop

@EnableAspectJAutoProxy

@EnableAspectJAutoProxy
@SpringBootApplication
class ToDoListApplication

 

어노테이션 설정

@Target(AnnotationTarget.FUNCTION) // 적용될 대상
@Retention(AnnotationRetention.RUNTIME) // 어느시점까지 사용?
annotation class StopWatch()

 

AOP 설정

@Aspect

정의할 때. 클래스에 붙여준다.

@Around

적용 시점. 

-@Around : 메서드 실행 전후

-@Before : 메서드 호출 전

-@Afrer : 메서드 결과와 관계없이 완료되면 실행

-@AfterReturning : 메서드가 정상적으로 반환 했을 때만

-@Afterthrowing : 메서드가 예외를 발생시킬 때 

 

joinPoint.procced() : 메소드 실행시킴

PointCut Expression : execution(* ...) / within(* ...)

위 코드는, 잘 동작 하지만, 한가지 오류가 있습니다. @StopWatch 어노테이션이 붙은 메소드에서 빈 DTO를 응답하는 문제가 있어요. 이에 대한 해결은, 마지막 테스트 강의에서 다룰 예정입니다.굉장히 간단한 문제이니 직접해결해보셔도 좋습니다!

 

 


 🚩QueryDSL

: 직관적인 쿼리를 작성하면서 동적인 쿼리 작성 가능, 컴파일 타임에 오류 잡아줌

결과 또한 JPA 의 Entity 로 받을 수 있다.

 

동적쿼리?

여러 필터 조건 등등 if 문이 길어짐

상황에 따라 형태가 많이 달라진다.

 

gradle 설정

plugins {
...
kotlin("kapt") version "1.8.22" // 어노테이션을 분석해서 querydsl 에 알려줌.
...
}
val queryDslVersion = "5.0.0" // 버전 명시
dependencies {
...
implementation("com.querydsl:querydsl-jpa:$queryDslVersion:jakarta")
kapt("com.querydsl:querydsl-apt:$queryDslVersion:jakarta")
...
}

=> Entity 를 추가하거나 변경할 때 compileKotlin 으로 QClass 업데이트 할 것

 

 

 

 ❗QueryDSL의 문법

QueryDsl을 컴파일하면 자동으로 Entity 를 기반으로 한 QClass 가 생성되고 이를 이용한다.

val post = QPost.post // 기본 instance
val post = QPost("p") // 별칭 지정

그러면 queryfactoy 에서 post 를 호출할 수 있다.

 

● 기본 조회

queryFactory.select(post).from(post).fetch()      // 리스트 조회
queryFactory.select(post).from(post).fetchFirst() // 단건 조회 (Not Null)
queryFactory.select(post).from(post).fetchOne()   // 단건 조회 (Nullable)

 

  검색 조건

queryFactory.selectFrom(post)
.where(post.title.eq("a").and(post.view.gt(10).or(post.author.like("%a%"))
.fetch()

 

  정렬

queryFactory.selectFrom(post).orderBy(post.createdAt.desc()).fetch()

 

  페이징 - offset, limit

queryFactory.selectFrom(post)
.where(post.title.contains("a))
.orderBy(post.createdAt.desc())
.offset(10)
.limit(20)

 

  카운트 - count() , Wildcard.count

// SELECT COUNT(post.id) FROM post
val count1: Long? = queryFactory
   .select(post.count())
   .from(post)
   .fetchOne()

// SELECT COUNT(*) FROM post
val count2: Long? = queryFactory
   .select(Wildcard.count)
   .from(post)
   .fetchOne()

assert(count1 == count2)

 

 

조인

: 둘 이상의 테이블을 연결해서 데이터를 검색하는 방법

적어도 하나의 컬럼을 공유하고 있어야 한다!!

 

  연관관계 조인

val comment = QComment.comment

// 기본 조인 (innerJoin과 동일)
queryFactory.select(post)
.from(post)
.join(post.commet, comment)
.where(comment.content.contains("아니"))
.fetch()

// LEFT JOIN
queryFactory.select(post)
.from(post)
.leftJoin(post.commet, comment)
.where(comment.content.contains("아니"))
.fetch()

// RIGHT JOIN
queryFactory.select(post)
.from(post)
.rightJoin(post.commet, comment)
.where(comment.content.contains("아니"))
.fetch()

// FETCH JOIN - 뒤에서 설명 예정
queryFactory.select(post)
.from(post)
.leftJoin(post.commet, comment)
.fetchJoin()
.where(comment.content.contains("아니"))
    .fetch()

 

inner join

: 공통된 것만

right join

: 조인 기준 오른쪽에 있는 것은 다

left join

: 조인 기준 왼쪽에 있는 것은 다

 

 

  연관관계 X 조인

val user = QUser.user

queryFactory.select(post)
.from(post)
.join(user).on(post.authorName.eq(user.name))
.fetch()

post 에서 불러올 필요 X, on 절을 기반으로 가져옴

 

 

  서브쿼리 - JPAExpression 

val p = QPost("postSub")

queryFactory.select(post)
	.from(post)
	.where(post.numLike.eq(
	JPAExpressions.select(p.numLike.max()).from(m))
  	  )
  	  .fetch()

 

쿼리 안의 쿼리?

 

 

  동적 쿼리에의 활용

-Boolean builder 

fun serarchUser(email: String?, nickname: String?): List<User> {
	val builder = new BooleanBuilder()
    	email?.let { builder.and(user.email.contains(it)) }
        nickname?.let { builder.and(user.nickname.contains(it) }
    
    return queryFactory.selectFrom(user)
        .where(builder)
        .fetch()
}

BooleanBuilder() 객체 생성해서 조건마다 추가해준다. 그리고 Where 절에 인자로 넣어준다.

 

 

  QueryProjection

: Entity 가 아닌 다른 타입으로 반환받을 수 있다.!!

 

-Projections.constructor

data class UserDto( val id: Long, val email: String )
val userDtos = queryFactory
.select(Projections.constructor(
            UsertDto::class.java,
            user.id, // 순서 중요!
            user.email // 순서 중요!
	))
           .from(user)
           .fetch()

생성자 사용 =>객체 파라미터 순서 주의

 

  @QueryProjection 

data class UserDto @QueryProjection constructor( val id: Long, val email: String)
val userDtos = queryFactory
	.select(QUserDto(
	id = user.id, 
	email = user.email
	))
	.from(user)
	.fetch()

데이터 클래스 생성자 앞에 어노테이션 붙여줌으로써 해당 클래스의 Q클래스를 생성

 

 

  QueryDsl 벌크 수정, 삭제

: queryFactory 에서 수정, 삭제

모든 Entity 에서 변경사항을 감지해야하는 JPA 의 Dirty Checking 에 비해 월등히 개선된 성능

BUT  영속성 컨텍스트 내의 Entity 와 별개로 실행되는 것이라서 이 쿼리를 실행한 후의 영속성 컨텍스트와 DB의 상태가 서로 다르게 된다 때문데 쿼리를 실행 후 반드시 em.clear() 를 통해 영속성 컨텍스트 초기화를 해주어야 한다.

val updatedCount = queryFactory
			.update(user)
			.set(user.nickname, "TEST_NICKNAME")
			.where(user.id.lt(10L))
			.execute()
val updatedCount = queryFactory
			.delete(user)
			.where(user.id.lt(10L))
			.execute()

 

 

 

 ❗Query Factory 만들자!

class QueryDslSupport {
    @PersistenceContext
    protected lateinit var entityManager: EntityManager

    protected val queryFactory: JPAQueryFactory
        get() {
            return JPAQueryFactory(entityManager)
        }
}

@PersistenceContect

: JPA 엔티티 매니저 주입

 

get()

: JPAQueryFactory 를 생성할 때 엔티티 매니저를 사용하여 인스턴스를 반환

 

❓왜 queryFacotry 를 불러올 때 그냥 불러와서 사용하는게 아니고 매번 초기화해서 앤티티매니저를 주입받는거지?

jpa 엔티티매니저의 트랜잭션 내에서 동작하도록, 여러 개의 앤티티매니저가 있을 경우 명시적으로 지정해서 일관성을 보장