Light Blue Pointer
본문 바로가기
Developing/TIL(Develop)

JPA in Spring Boot

by Greedy 2023. 11. 15.

JPA in Spring Boot

SpringBoot 환경에서의 JPA

메모장 프로젝트 JPA 설정

build.gradle : spring-boot-starter-data-jpa 추가

// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

application.properties : Hibernate 설정

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

show_sql, format_sql, use_sql_comments 옵션

  • Hibernate가 DB에 요청하는 모든 SQL을 보기좋게 출력

ddl-auto

  • create : 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
  • create-drop : create와 같으나 종료시점에 테이블을 DROP
  • update : 변경된 부분만 반
  • validate : Entity와 테이블이 정상 매핑되었는지만 확인
  • none: 아무것도 하지 않음

Memo Entity

package com.sparta.memo.entity;

import com.sparta.memo.dto.MemoRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Getter
@Setter
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
@NoArgsConstructor
public class Memo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)//autoincrement
    private Long id;
    @Column(name = "username", nullable = false)
    private String username;
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;

    public Memo(MemoRequestDto requestDto) {
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }

    public void update(MemoRequestDto requestDto) {
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }
}

SpringBoot 환경에서는 EntityManagerFactory와 EntityManager가 자동으로 생성됨

  • application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory가 생성됨

Java환경에서는 META-INF폴더에 persistence.xml에 정보들을 줬었음

데이터베이스 접근 정보 DTL auto 옵션주기 등등 Memo entity 위치도 알려줬었다고 함

@PersistenceContext
EntityManager em;
  • @PersistenceConext 애너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용

Spring의 트랜잭션

  • Spring 프레임워크에서는 DB의 트랜잭션 개념을 애플리케이션에 적용할 수 있도록 트랜잭션 관리자를 제공
  • 검색 → SimpleJpaRepository → Classes
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
						...
			
		@Transactional
		@Override
		public <S extends T> S save(S entity) {
		
			Assert.notNull(entity, "Entity must not be null");
		
			if (entityInformation.isNew(entity)) {
				em.persist(entity);
				return entity;
			} else {
				return em.merge(entity);
			}
		}

						...
}
  • 예시 코드 처럼 @Transactional 애너테이션을 클래스 상단이나 메서드에 추가하면 쉽게 트랜잭션 개념을 적용할 수 있다!
  • 메서드가 호출되면, 해당 메서드 내에서 수행되는 모든 DB 연산 내용은 하나의 트랜잭션으로 묶임
  • 이때, 해당 메서드가 정상적으로 수행되면 트랜잭션을 커밋하고, 예외가 발생하면 롤백
  • 클래스에 선언한 @Transactional은 해당 클래스 내부의 모든 메서드에 트랜잭션 기능을 부여
  • 맨 위의 SimpleJpaRepository에 @Tracsactional(readOnly=true)가 걸려있음 → 모든 transactional에 그 옵션을 부여
  • 이때, save 메서드는 @Transactional 애너테이션이 추가되어있기 때문에 readOnly = true 옵션인 @Transactional을 덮어쓰게 되어 readOnly = false 옵션으로 적용됨

readOnly = true 옵션

  • 트랜잭션에서 데이터를 읽기만 할 때 사용됨
  • 이 속성을 사용하면 읽기 작업에 대한 최적화를 수행할 수 있음
  • 만약, 해당 트랜잭션에서 데이터를 수정하려고 하면 예외가 발생하기 때문에 주의해야함
  • 읽기만 해서는 쓰기를 할 수가 없어서 save에서는 덮어써줌!

@Transactional

@Test
@Transactional 
@Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
@DisplayName("메모 생성 성공")
void test1() {
    Memo memo = new Memo();
    memo.setUsername("Robbert");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}

@Transactional

ex) 트랜잭션 테스트 성공

@Test
@Transactional 
@Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
@DisplayName("메모 생성 성공")
void test1() {
    Memo memo = new Memo();
    memo.setUsername("Robbert");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
package com.sparta.memo;

import jakarta.persistence.PersistenceContext;

public class TransactionTest {

    @PersistenceContext//스프링부트에서 자동으로 생성해주는 EntityManager를 주입받아옴
    EntityManager em;
}

→ java.lang.NullPointerException: Cannot invoke "jakarta.persistence.EntityManager.persist(Object)" because "this.em" is null

에러남 SpringBootTest annotation 추가해줘야 한다고 함

@SpringBootTest//중요
public class TransactionTest {

    @PersistenceContext//스프링부트에서 자동으로 생성해주는 EntityManager를 주입받아옴
    EntityManager em;
package com.sparta.memo;

import com.sparta.memo.entity.Memo;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
public class TransactionTest {

    @PersistenceContext//스프링부트에서 자동으로 생성해주는 EntityManager를 주입받아옴
    EntityManager em;

    @Test
    @Transactional
    @Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
    @DisplayName("메모 생성 성공")
    void test1() {
        Memo memo = new Memo();
        memo.setUsername("Robbert");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }

}
  • 트랜잭션이 적용되어 DB 작업이 성공
Hibernate:
/* insert for
com.sparta.memo.entity.Memo */insert
into
memo (contents,username)//autoincrement가 걸려있어서 id 따로 안 넣어줌
values
(?,?)

ex) 트랜잭션 테스트 실패

@Test
@DisplayName("메모 생성 실패")
void test2() {
    Memo memo = new Memo();
    memo.setUsername("Robbie");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}

jakarta.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call 데이터를 수정할때는 무조건 transaction환경이 필요한데 tracsaction 환경이 잡혀있지 않은 상태에서 persist를 호출해서 오류가 발생

@Disabled //옵션을 걸면 Tests ignored 라고 뜸

영속성 컨텍스트와 트랜잭션의 생명주기

스프링 컨테이너 환경) 영속성 컨텍스트의 생명주기 == 트랜잭션의 생명주기

트랜잭션이 유지되는 동안 영속성 컨텍스트도 계속 유지가 되기 때문에 영속성 컨텍스트의 기능을 사용할 수 있다

→ 앞에서 작성한 테스트 코드 메서드에 트랜잭션이 적용되지 않았기 때문에 영속성 컨텍스트가 유지되지 못해 오류가 발생, persist call이 이루어지지 못함

Spring은 어떻게 Service 부터 Repository 까지 Transaction을 유지할 수 있는 걸까요?

Service의 트랜잭션이 적용된 메서드에서 Repository의 메서드를 호출할 때 무언가 처리되고 있는 것이 있는 걸까요?

Spring에서는 이러한 상황에서 트랜잭션을 제어할 수 있도록 트랜잭션 전파 기능을 제공하고 있습니다.

트랜잭션 전파

@Transactional에서 트랜잭션 전파 옵션을 지정할 수 있습니다.

트랜잭션 전파의 기본 옵션은 REQUIRED 입니다.

REQUIRED 옵션은 ****부모 메서드에 트랜잭션이 존재하면 자식 메서드의 트랜잭션은 부모의 트랜잭션에 합류하게됩니다.

트랜잭션 전파 테스트

ex) 트랜잭션 전파

MemoRepository에 다음 함수 추가

@Transactional
public Memo createMemo(EntityManager em) {
    Memo memo = em.find(Memo.class, 1);
    memo.setUsername("Robbie");
    memo.setContents("@Transactional 전파 테스트 중!");

    System.out.println("createMemo 메서드 종료");
    return memo;
}

TransactionTest 복붙

package com.sparta.memo;

import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
public class TransactionTest {

    @PersistenceContext
    EntityManager em;

    @Autowired
    MemoRepository memoRepository;

    @Test
    @Transactional
    @Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
    @DisplayName("메모 생성 성공")
    void test1() {
        Memo memo = new Memo();
        memo.setUsername("Robbert");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }

    @Test
    @Disabled
    @DisplayName("메모 생성 실패")
    void test2() {
        Memo memo = new Memo();
        memo.setUsername("Robbie");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }

    @Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("트랜잭션 전파 테스트")
    void test3() {
        memoRepository.createMemo(em);
        System.out.println("테스트 test3 메서드 종료");
    }
}
  • 실행 결과 자식 메서드 createMemo가 종료될 때 update가 실행되는 것이 아니라 부모 메서드에 트랜잭션이 합류되면서 부모 메서드가 종료된 후 트랜잭션이 커밋될 때 update가 실행된 것 확인가능
  • 부모 메서드 test3의 @Transactional @Rollback(value = false)을 주석 처리하고 다시 한번 update를 시도해보면 합류할 부모 트랜잭션이 없기 때문에
  • 자식 메서드가 종료된 후 트랜잭션이 커밋되면서 update가 실행되었음

자식 메서드/ 부모 메서드

class TransactionTest에서 MemoRepository를 주입받아와서 사용하고있음

@Autowired
MemoRepository memoRepository;
@Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("트랜잭션 전파 테스트")
    void test3() {
        memoRepository.createMemo(em);
        System.out.println("테스트 test3 메서드 종료");
    }

test3에서 MemoRepository에 있는 createMemo를 부르고 있음

test3 method : 부모 메서드

create method: 자식 메서드

코드 살펴보기

@Transactional
public Memo createMemo(EntityManager em) {
    Memo memo = em.find(Memo.class, 1);
    memo.setUsername("Robbie");
    memo.setContents("@Transactional 전파 테스트 중!");

    System.out.println("createMemo 메서드 종료");
    return memo;
}
Hibernate: 
    select
        m1_0.id,
        m1_0.contents,
        m1_0.username 
    from
        memo m1_0 
    where
        m1_0.id=?
createMemo 메서드 종료
테스트 test3 메서드 종료
Hibernate: 
    /* update
        for com.sparta.memo.entity.Memo */update memo 
    set
        contents=?,
        username=? 
    where
        id=?

createMemo 메서드 종료 → 메서드가 끝났는데 트랜잭션이 종료되면 commit되면서 쿼리가 날라가야 하는거 아닌가?

test3로 날아와서 종료된 다음에야 업데이트가 날아가서 찍힌다고 함

자식 메서드의 transaction이 부모 메서드의 transaction에 합쳐졌다고 함

왜 이렇게 될 수 있냐면 @Transactional 에는

@Transactional(propagation = Propagation.REQUIRED)//이라는 게 존재함!

Propagation.*REQUIRED* : 디폴트, 부모 메서드에 tracsactional이 존재한다면 자식 메서드의 transaction은 부모 메서드의 transaction에 합류함!

디폴트 알아내기!

ctrl + Transactional 클릭 → find : propagation → default Propagation.*REQUIRED* 라고 써 있다

ex) 트랜잭션 not 전파, 자식은 transactional이 있는데 부모는 없는 경우

  	@Test
    //@Transactional(propagation = Propagation.REQUIRED)
    //@Rollback(value = false)
    @DisplayName("트랜잭션 전파 테스트")
    void test3() {
        memoRepository.createMemo(em);
        System.out.println("테스트 test3 메서드 종료");
    }
    @Transactional
    public Memo createMemo(EntityManager em) {
        Memo memo = em.find(Memo.class, 1);
        memo.setUsername("Madison");
        memo.setContents("@Transactional not 전파 테스트 중!");

        System.out.println("createMemo 메서드 종료");
        return memo;
    }
Hibernate: 
    select
        m1_0.id,
        m1_0.contents,
        m1_0.username 
    from
        memo m1_0 
    where
        m1_0.id=?
createMemo 메서드 종료
Hibernate: 
    /* update
        for com.sparta.memo.entity.Memo */update memo 
    set
        contents=?,
        username=? 
    where
        id=?
테스트 test3 메서드 종료

→ 자식 메서드 끝나고 바로 transaction이 일어남

더 이상 사용 안 할 테스트는 커밋 전에 Disabled를 걸어줌

@Test
    @Disabled
    //@Transactional(propagation = Propagation.REQUIRED)
    //@Rollback(value = false)
    @DisplayName("트랜잭션 전파 테스트")
    void test3_() {
        //memoRepository.createMemo(em);
        System.out.println("테스트 test3 메서드 종료");
    }

Spring Data JPA 란 무엇일까?

Spring Data JPA

JPA를 쉽게 사용할 수 있게 만들어놓은 하나의 모듈

  • JPA를 추상화시킨 Repository 인터페이스를 제공
  • Hibernate와 같은 JPA구현체를 사용해서 ****구현한 클래스로 Repository 인터페이스를 사용가능
  • 개발자들은 Repository 인터페이스를 통해 JPA를 간편하게 사용

Spring Data JPA의 SimpleJpaRepository

  • Spring Data JPA에서는 JpaRepository 인터페이스를 구현하는 클래스를 자동으로 생성해줌
    • Spring 서버가 뜰 때 JpaRepository 인터페이스를 상속받은 인터페이스가 자동으로 스캔이 되면,
    • 해당 인터페이스의 정보를 토대로 자동으로 SimpleJpaRepository 클래스를 생성해 주고, 이 클래스를 Spring ‘Bean’으로 등록함
  • 따라서 인터페이스의 구현 클래스를 직접 작성하지 않아도 JpaRepository 인터페이스를 통해 JPA의 기능을 사용할 수 있다

Spring Data JPA 사용방법

JpaRepository 등록

  • JpaRepository<**"@Entity 클래스", "@Id 의 데이터 타입">**를 상속받는 interface 로 선언
    • Spring Data JPA에 의해 자동으로 Bean 등록됨
    • 제네릭스의 @Entity 클래스 위치에 Memo Entity를 추가했기 때문에 해당 MemoRepository는 DB의 memo 테이블과 연결되어 CRUD 작업을 처리하는 인터페이스가 됨

메모장 프로젝트 Spring Data JPA 적용

  • public class MemoRepository →
public interface MemoRepository extends JpaRepository<Memo, Long> {

}

SimpleJpaRepository에 @Repository 가 달려있어서 여기는 안 달아도 된다고 함

Memo service 원래 코드

package com.sparta.memo.service;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class MemoService {
    private final MemoRepository memoRepository;

    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);
        // DB 저장
        Memo saveMemo = memoRepository.save(memo);

        // Entity -> ResponseDto
        MemoResponseDto memoResponseDto = new MemoResponseDto(memo);

        return memoResponseDto;
    }

    public List<MemoResponseDto> getMemos() {
        // DB 조회
        return memoRepository.findAll();
    }

    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = memoRepository.findById(id);
        if (memo != null) {
            // memo 내용 수정
            memoRepository.update(id, requestDto);
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
        return id;
    }

    public Long deleteMemo(Long id) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = memoRepository.findById(id);
        if (memo != null) {
            memoRepository.delete(id);
            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }
}

createMemo

public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);
        // DB 저장
        Memo saveMemo = memoRepository.save(memo);

        // Entity -> ResponseDto
        MemoResponseDto memoResponseDto = new MemoResponseDto(memo);

        return memoResponseDto;
    }

수정할 게 전혀 없음

save는 구현을 안 했는데 SimpleRepository에 있는 save를 그냥 가져다가 쓰는 거라고 함

getMemos

public List<MemoResponseDto> getMemos() {
        // DB 조회
        return memoRepository.findAll();
    }

public List<MemoResponseDto> getMemos() {
        // DB 조회
        return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
    }

stream에서 memo가 하나씩 빠져나올거고 map에서 변환이 될 거라고 함

updateMemo

type 이 Optional이면 null check를 해줘야 한다고 한다

// 해당 메모가 DB에 존재하는지 확인
        Memo memo = memoRepository.findById(id).orElseThrow(()->
            new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
        );

이렇게 하거나 아니면 Optional타입으로 받으면 되는데 그냥 한번에 Memo 타입으로 받으려고 저렇게 했다고 함 이제 null check하는 부분 필요 없어서 뺌

public Long updateMemo(Long id, MemoRequestDto requestDto) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = memoRepository.findById(id).orElseThrow(()->
            new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
        );
        // memo 내용 수정
		    memo.update(requestDto);
        
        return id;
    }

겹치는 부분 먼저 함수로 뺌

private Memo findMemo(Long id){
        Memo memo = memoRepository.findById(id).orElseThrow(()->
                new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
        );
        return memo;
    }

최종 수정

@Transactional
    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = findMemo(id);
        // memo 내용 수정
		    memo.update(requestDto);

        return id;
    }

@Transactional 을 꼭 해줘야 변경 감지가 일어난다고 함

Memo의 update 함수

public void update(MemoRequestDto requestDto) {
        this.username = requestDto.getUsername();
        this.contents = requestDto.getContents();
    }

deleteMemo

public Long deleteMemo(Long id) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = findMemo(id);
        if (memo != null) {
            memoRepository.delete(id);
            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }

public Long deleteMemo(Long id) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = findMemo(id);
        memoRepository.delete(memo);
        return id;
    }

JPA Auditing

메모가 저장될때마다 시간을 보여줄 수 있도록 구현을 하고싶대

데이터의 생성(created_at), 수정(modified_at) 시간은 포스팅, 게시글, 댓글 등 다양한 데이터에 매우 자주 활용되는데 각각의 Entity의 생성 수정 시간을 매번 작성하는건 너무 비효율적임

  • Spring Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능인 JPA Auditing을 제공
  • @MappedSuperclass
    • JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 createdAt, modifiedAt 처럼
    • 추상 클래스에 선언한 멤버변수를 컬럼으로 인식할 수 있다.
    • 자바 객체지향에서 상속받으면 상위클래스 필드도 가지는거랑 똑같은 개념
  • @EntityListeners(AuditingEntityListener.class)
    • 해당 클래스에 Auditing 기능을 포함시켜줌
    • 이걸 달아줘야 자동으로 시간 넣는 기능이 수행됨
  • @CreatedDate
    • Entity 객체가 생성되어 저장될 때 시간이 자동으로 저장됨
    • 최초 생성 시간이 저장되고 그 이후에는 수정되면 안되기 때문에 updatable = false 옵션을 추가함
  • @LastModifiedDate
    • 조회한 Entity 객체의 값을 변경할 때 변경된 시간이 자동으로 저장됨
    • 처음 생성 시간이 저장된 이후 변경이 일어날 때마다 해당 변경시간으로 업데이트됨
    • ←updatable = false 옵션을 추가하지 않아서!
  • @Temporal
    • 날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용함
    • DB에는 Date(날짜), Time(시간), Timestamp(날짜와 시간)라는 세 가지 타입이 별도로 존재함
      • DATE : ex) 2023-01-01
      • TIME : ex) 20:21:14
      • TIMESTAMP : ex) 2023-01-01 20:22:38.771000

<aside> 💡 @SpringBootApplication 이 있는 class에 @EnableJpaAuditing 추가!

  • JPA Auditing 기능을 사용하겠다는 정보를 전달해주기 위해 @EnableJpaAuditing 을 추가해야함 </aside>
  1. entity 디렉토리 → Timestamped 클래스 생성
package com.sparta.memo.entity;

import jakarta.persistence.*;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}
  1. @EnableJpaAuditing 애너테이션을 추가한다.

MemoApplication(main method있는 클래스) 가서

@EnableJpaAuditing//이거 추가해줌!
@SpringBootApplication
public class MemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(MemoApplication.class, args);
    }

}
  1. 적용하고자하는 Entity 클래스에서 Timestamped를 상속받는다.
public class Memo extends Timestamped{
  1. 반환할 때 시간도 같이 반환해줘야 해서 MemoResponseDTO 수정한다
package com.sparta.memo.dto;

import com.sparta.memo.entity.Memo;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class MemoResponseDto {
    private Long id;
    private String username;
    private String contents;

    public MemoResponseDto(Memo memo) {
        this.id = memo.getId();
        this.username = memo.getUsername();
        this.contents = memo.getContents();
    }
}

package com.sparta.memo.dto;

import com.sparta.memo.entity.Memo;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class MemoResponseDto {
    private Long id;
    private String username;
    private String contents;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    public MemoResponseDto(Memo memo) {
        this.id = memo.getId();
        this.username = memo.getUsername();
        this.contents = memo.getContents();
        this.createdAt = memo.getCreatedAt();
        this.modifiedAt = memo.getModifiedAt();
    }
}

Query Methods란 무엇일까?

Query Methods란

  • Spring Data JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공
  • JpaRepository 인터페이스에서 해당 인터페이스와 매핑되어있는 테이블에 요청하고자 하는 SQL을 메서드 이름을 사용하여 선언할 수 있다.

MemoRepository

package com.sparta.memo.repository;

import com.sparta.memo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemoRepository extends JpaRepository<Memo, Long> {
    List<Memo> findAllByOrderByModifiedAtDesc();
}
  • SimpleJpaRepository 클래스가 생성될 때 위처럼 직접 선언한 JpaRepository 인터페이스의 모든 메서드를 자동으로 구현해줌
    • JpaRepository 인터페이스의 메서드 즉, Query Methods는 개발자가 이미 정의 되어있는 규칙에 맞게 메서드를 선언하면 해당 메서드 이름을 분석하여 SimpleJpaRepository에서 구현됨
    • 따라서 우리는 인터페이스에 필요한 SQL에 해당하는 메서드 이름 패턴으로 메서드를 선언 하기만 하면 따로 구현하지 않아도 사용할 수 있음
  • findAllByOrderByModifiedAtDesc : Memo 테이블에서 ModifiedAt 을 기준(OrderByModifiedAt)으로 전체 데이터를 내림차순(Desc)으로 select 해오는(find) SQL을 실행하는 메서드를 생성
  • List<Memo> findAllByUsername(String username);: select * from … where username = …

ByUsername 에 값을 전달해줘야하기 때문에 파라미터에 해당 값의 타입과 변수명을 선언

Query Methods 는 메서드의 파라미터를 통해 SQL에 필요한 값을 동적으로 받아 처리!

메모장 프로젝트 Query Methods 적용

최신 메모가 가장 상단에 나올 수 있도록 수정

MemoService

public List<MemoResponseDto> getMemos() {
        // DB 조회
        return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
    }

public List<MemoResponseDto> getMemos() {
    // DB 조회
    return memoRepository.findAllByOrderByModifiedAtDesc().stream().map(MemoResponseDto::new).toList();
}

'Developing > TIL(Develop)' 카테고리의 다른 글

인증과 인가  (2) 2023.11.20
Spring Bean  (0) 2023.11.20
[JPA Core]  (0) 2023.11.15
IoC와 DI  (5) 2023.11.09
3 Layer Architecture 역할 분리  (1) 2023.11.09