스프링부트 JPA

dani Kim·2024년 8월 5일

EntityManager
JPA(Java Persistence API)의 핵심 인터페이스로, 엔티티를 데이터베이스와 연동하여 관리합니다. 주로 엔티티의 생명주기(생성, 읽기, 업데이트, 삭제)를 관리하고, 쿼리를 실행하며 트랜잭션을 처리하는 역할을 합니다.

  • 클라이언트당 하나씩 생성


  1. User DTO 작성
@Getter
@Setter
@ToString
@Entity
public class User {
    @Id
    private String id;
    private String password;
    private String name;
    private String email;
    private Date inDate;
    private Date upDate;
}
  1. EntityManager 를 이용한 DB 생성,조회,변경
@SpringBootApplication
public class TestApplication implements CommandLineRunner {
    @Autowired
    EntityManagerFactory emf;

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(TestApplication.class);
        app.setWebApplicationType(WebApplicationType.NONE);
        app.run(args);
    }

    @Override
    public void run(String... args) throws Exception {
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        User user = new User();
        user.setId("aaa");
        user.setPassword("1234");
        user.setName("Lee");
        user.setEmail("aaa@aaa.com");
        user.setInDate(new Date());
        user.setUpDate(new Date());

        /* 트랜잭션 시작 */
        tx.begin();

        /* 저장 */
        em.persist(user);
        /* 같은 엔티티를 여러번 저장해도 한번만 INSERT 됨 */
        em.persist(user);

        /* 변경 */
        user.setPassword("4321");
        user.setEmail("bbb@bbb.com");
        tx.commit();

        /* 조회 */
        /* DB에 있는 KEY , em에 있으면 DB 조회 안함 */
        User user2 = em.find(User.class, "aaa");
        System.out.println("user2 = " + user2);
        /* DB에 없는 KEY , em에 없으면 DB 조회. user3은 null 발생 */
        User user3 = em.find(User.class, "bbb");
        System.out.println("user3 = " + user3);

        /* 삭제 */
        tx.begin();
        em.remove(user);
        tx.commit();
    }
}
  1. Spring Data를 이용한 TDD
    Board Dto 생성
@Getter
@Setter
@ToString
@Entity
public class Board {
    @Id
    @GeneratedValue
    private Long bno;
    private String title;
    private String writer;
    private String content;
    private Long viewCnt;
    @Temporal(value = TemporalType.TIMESTAMP)
    private Date inDate;
    @Temporal(value = TemporalType.TIMESTAMP)
    private Date upDate;
}
  1. BoardRepository interface 생성후 CrudRepository 를 상속받아 CRUD TDD
import org.springframework.data.repository.CrudRepository;

/* Spring Data JPA 가 CrudRepository 를 알아서 구현해준다 */
public interface BoardRepository extends CrudRepository<Board, Long> {}

  1. Board CRUD TDD
package com.example.test;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Date;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
/* order 애너테이션을 사용하기위해 작성 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class BoardRepositoryTest {
    @Autowired
    private BoardRepository boardRepo;

    @Test
    @Order(4)
    public void deleteTest(){
        boardRepo.deleteById(1L); /*1 번 게시물 삭제 */

        /* 못찾을경우 null 반환 */
        Board board = boardRepo.findById(1L).orElse(null);
        /* 삭제되었으니까 null 이여야함 */
        assertTrue(board==null);
    }

    @Test
    @Order(3)
    public void updateTest(){
        Board board = boardRepo.findById(1L).orElse(null);
        assertTrue(board!=null);

        board.setTitle("modified Title");
        boardRepo.save(board);
        Board board2 = boardRepo.findById(1L).orElse(new Board());
        assertTrue(board.getTitle().equals(board2.getTitle()));
    }

    @Test
    @Order(2) /* 2번째로 Test 실행 */
    public void selectTest(){
        /* 값이 없을때 예외 발생*/
        /*Board board = boardRepo.findById(1L).get();*/
        /* 값이 없을때 null 반환 */
        Board board = boardRepo.findById(1L).orElse(null);
        assertTrue(board!=null);
    }

    @Test
    @Order(1) /* 1번째로 Test 실행 */
    /* Insert 진행후 select 진행 */
    public void insertTest(){
        Board board = new Board();
        board.setBno(1L);
        board.setTitle("Test Title");
        board.setContent("This is Test");
        board.setWriter("aaa");
        board.setViewCnt(0L);
        board.setInDate(new Date());
        board.setUpDate(new Date());

        boardRepo.save(board);
    }

}
쿼리종류의미
JPQLDB 테이블이 아닌 entity 대상으로 쿼리를 작성. SQL과 유사하다. (JPA + SQL)
"SELECT b FROM Board b Where b.title =?1" (대소문자구별함)
쿼리메서드메서드 이름으로 JPQL을 자동생성
LIST list = BoardRepofindByTitleAndwriter("title","writer1");
JPA CriteriaJPQL을 메서드의 조합으로 작성 (JPA표준. 긹고 읽기어려워 불편함) API
cq.select(b).where(cb.equal(b.get("title"),
QuerydslJPQL을 메서드의 조합으로 작성. Criteria보다 간결. 오픈소스
List list = queryFactory.selectFrom(board).where(board.title.eq("title1")).fetch();
Native SQLJPQL 대신 SQL을 직접 작성. 복잡한 SQL 작성가능 @Query

쿼리 메서드

Repository에 메서드이름을 규칙에 맞게 작성하면 Spring Data가 메서드 이름만보고 자동으로 쿼리를 만들어준다. (= 메서드이름이 규칙)
예) find + (entity명) + by + 컬럼이름

  1. BoardRepository 인터페이스에 코드 추가 작성 (쿼리메서드방식)
public interface BoardRepository extends CrudRepository<Board, Long> {

    /* SELECT COUNT(*) FROM BOARD WHERE WRITER = :writer */
    int countAllByWriter(String writer);

    /* SELECT * FROM BOARD WHERE WRITER = :writer */
    List<Board> findByWriter(String writer);

    /* SELECT * FROM BOARD WHERE TITLE = :title AND WRITER = :writer */
    List<Board> findByTitleAndWriter(String title, String writer);

    /* DELETE FROM BOARD WHERE WRITER = :writer */
    /* 여러 사용자가 동시에 데이터를 삭제하려고 할 때 충돌이 발생할 수 있다 */
    /* 여러 행을 삭제하는 작업 중 일부가 성공하고 나머지가 실패하면 데이터 불일치가 발생할수있다 */
    @Transactional /* Tx 필수 */
    int deleteByWriter(String writer);
}
  1. 두번째 TDD 작성 (쿼리메서드방식)
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class BoardRepositoryTest2 {
    @Autowired
    BoardRepository boardRepo;

    /* 매 테스트마다 테스트 데이터를 넣는다 */
    @BeforeEach
    public void testData(){
        for(int i=1; i<=100; i++){
            Board board = new Board();
            board.setBno((long)i);
            board.setTitle("title"+i);
            board.setContent("content"+i);
            board.setWriter("writer"+ (i%5)); /* writer0~4 까지*/
            board.setViewCnt((long)(Math.random()*100)); /* cnt는 0~99까지 */
            board.setInDate(new Date());
            board.setUpDate(new Date());
            boardRepo.save(board);
        }
    }

    @Test
    void countAllByWriter() {
        /* writer1이 작성한 게시글을 count */
        assertTrue(boardRepo.countAllByWriter("writer1")==20);
    }

    @Test
    void findByWriter() {
        /* writer1을 찾는다 */
        List<Board> list = boardRepo.findByWriter("writer1");
        assertTrue(list.size()==20);
        list.forEach(System.out::println);
    }

    @Test
    void findByTitleAndWriter() {
    }

    @Test
    void deleteByWriter() {
        assertTrue(boardRepo.deleteByWriter("writer1")==20);
        List<Board> list = boardRepo.findByWriter("writer1");
        assertTrue(list.size()==0);
    }
}

JPQL

  • 주로 @Query 로 작성



  1. BoardRepository 코드 추가작성
  public interface BoardRepository extends CrudRepository<Board, Long> {
    @Query("SELECT b fROM Board b") /* JPQL 명칭 대소문자 구분에 주의 */
    List<Board> findAllBoard(); /* 메서드 이름은 아무거나해도 상관없음 */

    @Query("SELECT b FROM Board b WHERE b.title=:title AND b.writer=:writer") /* 매개변수 이름 으로 조회 */
    List<Board> findByTitleAndWriter2(String title, String writer);

    @Query(value = "SELECT * FROM BOARD", nativeQuery = true) /* SQL문 */
    List<Board> findAllBoardBySQL();

    @Query(value = "SELECT title, writer FROM BOARD", nativeQuery = true) /* SQL문 일부만 SELECT */
    List<Object[]> findAllBoardBySQL2();
  1. TDD 작성
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class BoardRepositoryTest3 {
    @Autowired
    public EntityManager em;
    @Autowired
    BoardRepository boardRepo;

    /* 매 테스트마다 테스트 데이터를 넣는다 */
    @BeforeEach
    public void testData(){
        for(int i=1; i<=100; i++){
            Board board = new Board();
            board.setBno((long)i);
            board.setTitle("title"+i);
            board.setContent("content"+i);
            board.setWriter("writer"+ (i%5)); /* writer0~4 까지*/
            board.setViewCnt((long)(Math.random()*100)); /* cnt는 0~99까지 */
            board.setInDate(new Date());
            board.setUpDate(new Date());
            boardRepo.save(board);
        }
    }

    @Test
    @DisplayName("@Query로 JPQL작성 테스트")
    public void queryAnnoTest() {
        List<Board> list = boardRepo.findAllBoard();
        assertTrue(list.size()==100);
    }

    @Test
    @DisplayName("@Query로 JPQL작성 테스트 - 매개변수 이름")
    public void findByTitleAndWriter2() {
        List<Board> list = boardRepo.findByTitleAndWriter2("title1", "writer1");
        assertTrue(list.size()==1);
    }

    @Test
    @DisplayName("@Query로 네이티브 쿼리(SQL)작성 테스트")
    public void nativeQueryTest() {
        List<Board> list = boardRepo.findAllBoardBySQL();
        assertTrue(list.size()==100);
    }

    @Test
    @DisplayName("@Query로 네이티브 쿼리(SQL)작성 테스트 - 일부만 SELECT")
    public void nativeQueryTest2() {
        List<Object[]> list = boardRepo.findAllBoardBySQL2();
        assertTrue(list.size()==100);
    }

    @Test
    @DisplayName("createQuery로 JPQL작성 테스트")
    public void createQueryTest() {
        String query = "SELECT b FROM Board b"; /* Board를 b로 저장하고 b의 전부를 조회 */
        TypedQuery<Board> tQuery = em.createQuery(query, Board.class);
        List<Board> list = tQuery.getResultList();

        assertTrue(list.size()==100);
    }
}

Querydsl




1. pom.xml 파일에 dependency 의존성 추가, 플러그인 추가

  
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>Test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Test</name>
    <description>Test</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- querydsl S-->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>5.0.0</version>
            <classifier>jakarta</classifier>
        </dependency>
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
            <version>5.0.0</version>
            <classifier>jakarta</classifier>
        </dependency>
        <!-- querydsl E-->

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                          <!-- 코드를 해제할경우 QUser와 QBoard가 두번생성됨 -->
<!--                            <outputDirectory>target/generated-sources/java</outputDirectory>-->
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
  1. maven Test > comfile 더블클릭해서 플러그인 설치

  2. 프로젝트 구조로 이동 > generated-source를 파란색인 소스폴더로 바꾼다

  3. 아래처럼 QBoard와 QUser가 정상적으로 불러왔는지 체크

  4. TDD 코드 작성

  package com.example.test;

import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import jakarta.persistence.EntityManager;
import java.util.Date;

import static com.example.test.QBoard.board;

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class BoardRepositoryTest4 {
    @Autowired
    public EntityManager em;
    @Autowired
    BoardRepository boardRepo;

    /* 매 테스트마다 테스트 데이터를 넣는다 */
    @BeforeEach
    public void testData(){
        for(int i=1; i<=100; i++){
            Board board = new Board();
            board.setBno((long)i);
            board.setTitle("title"+i);
            board.setContent("content"+i);
            board.setWriter("writer"+ (i%5)); /* writer0~4 까지*/
            board.setViewCnt((long)(Math.random()*100)); /* cnt는 0~99까지 */
            board.setInDate(new Date());
            board.setUpDate(new Date());
            boardRepo.save(board);
        }
    }

    @Test
    @DisplayName("querydsl로 쿼리 작성 테스트1 - 간단한 쿼리 작성")
    public void querydslTest1() {
//        com.example.test.QBoard board = com.example.test.QBoard.board;
        /* 1. JPAQueryFactory를 생성 */
        JPAQueryFactory qf = new JPAQueryFactory(em);
        /* 2. 쿼리 작성 */
        /* qf.selectForm(QBoard board) */
        JPAQuery<Board> query = qf.selectFrom(board)
                /* board의 title이 title1 과 equals 한지 */
                .where(board.title.eq("title1"));
        /* 3.쿼리 실행 */
        List<Board> list = query.fetch();
        list.forEach(System.out::println);
    }
}
profile
파라랑푸~~~

0개의 댓글