Develop/spring-data

Spring Data JPA 시작 그리고 예제

에디개발자 2020. 11. 5. 07:23
반응형

작성된 모든 소스는 github에 있습니다.

이번글에서는 JPA 설정부터 테스트코드(Junit5) 까지 작성하였습니다.

다음글에서 JPA 테스트 코드 기준으로 작동 원리에 대해서 정리하겠습니다.

프로젝트 생성

Spring Initalizr 를 이용하여 생성

 

gradle

dependencies를 제외한 설정은 default값 그대로입니다.

 

application.yaml 

spring:
  profiles:
    active: local


--- #local
spring:
  profiles: local
  datasource:
    url: jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    username: root
    password: vnfmsqka0727!
    driver-class-name: com.mysql.jdbc.Driver

  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    database: mysql
    generate-ddl: false
    open-in-view: false
    show-sql: true
    hibernate:
      ddl-auto: create    # 항상 조심!!
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        use_sql_comments: true



logging:
  level:
    org:
      hibernate:
        SQL: DEBUG
        type:
          trace
  • datasource : database 설정값을 세팅한다.
  • jpa.database-platform : platform 설정
  • jpa.open-in-view : 영속성을 어느 범위까지 설정할지 결정. 참조
  • jpa.show-sql : 실행하는 쿼리 show
  • jpa.hibernate.ddl-auto: 톰캣 기동할 때 어떤 동작을 할지 결정
    • 해당 설정을 잘못하면 테이블이 drop될수 있다. 
    • 한 번 설정이 끝났다면 none, validate 로 설정하는 것을 추천한다.
  • jpa.properties.hibernate.format_sql : 쿼리를 이쁘게 보여준다.
  • logging.level.org.hibernate.SQL : 로그 레벨 설정

 

Configuration

package com.example.queyrdsl.configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class DataBaseConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}
  • EntityManagerFactory
    • 여기선 사용하지 않았다.
    • request가 올 때 EntityManager를 생성한다. 
  • JPAQueryFactory
    • JPAQuery를 만들어서 사용하는 방식
    • EntityManager를 통해서 질의가 처리된다.
    • 사용하는 쿼리문은 JPQL
    • 최종 목표는 JPA와 QueryDSL이기 때문에 사용
  • EntityManager
    • Transaction 단위로 생성된다.
    • DB connection pool을 사용한다.
    • Transaction이 끝나면 버린다. 다른 Thread와 공유하면 안된다.
  • PersistenceContext
    • persistence 뜻 : 지속하다.
    • 위 소스코드 기준으로는 EntityManager를 영구 저장하겠다. 로 쓰인다.

 

Database schema 생성

github 소스의 testdb-schema.sql 참조

-- auto-generated definition
create table store
(
    id      bigint auto_increment
        primary key,
    name    varchar(20)  null,
    address varchar(100) null
);

-- auto-generated definition
create table staff
(
    id       bigint auto_increment
        primary key,
    store_id bigint      null,
    name     varchar(10) null,
    age      int         null
);

 

Entity 생성

package com.example.queyrdsl.entity;

import lombok.*;

import javax.persistence.*;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Staff {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Integer age;

    @Builder
    public Staff(Long id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
}

 

package com.example.queyrdsl.entity;


import lombok.*;

import javax.persistence.*;
import java.util.Collection;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Store {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String address;

    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "store_id")
    private Collection<Staff> staff;

    @Builder
    public Store(Long id, String name, String address, Collection<Staff> staff) {
        this.id = id;
        this.name = name;
        this.address = address;
        this.staff = staff;
    }
}
  • Database Table마다 Entity를 생성한다.
  • Entity에는 Setter를 사용하지 않는다.
    • 객체의 일관성을 보장할 수 없다.
    • 객체를 생성할 때는 Builder를 사용하자.
Setter는 되도록이면 사용하지 않는 것이 좋다. 객체의 일관성을 유지하는데 문제가 생길 수 있다.
  • @NoArgsConstructor(access = AccessLevel.PROTECTED)
    • 생성자를 protected로 변경하면 new Store() 사용을 막을 수 있으므로 일관성을 유지하기 용이하다.
  • @Id
    • table column의 private key 설정
  • GeneratedValue(strategy = GenerationType.IDENTITY
    • 기본키 생성 방법
    • IDENTITY : 기본키 생성을 데이터베이스에 위임
    • SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본키 할당
    • TABLE : 키 생성 테이블을 사용
    • AUTO : 데이터베이스에 따라서 IDENTITY, SEQUENCE, TABLE 방법 중 하나를 자동으로 선택
  • @OneToMany
    • 일 대 다 관계 설정
    • @JoinColumn 기준으로 관계 설정
mappedBy를 사용해도 무관하다. 

 

Repository 생성

package com.example.queyrdsl.repository;

import com.example.queyrdsl.entity.Staff;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StaffRepository extends JpaRepository<Staff, Long> {
}
package com.example.queyrdsl.repository;

import com.example.queyrdsl.entity.Store;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StoreRepository extends JpaRepository<Store, Long> {
    Store findByName(String name);
}
  • Database table마다 생성 ( Entity와 1:1 )
  • JpaRepository를 상속받아서 사용한다.
    • <Entity, ENTITY_ID>

 

Test

package com.example.queyrdsl.jpa;

import com.example.queyrdsl.entity.Staff;
import com.example.queyrdsl.entity.Store;
import com.example.queyrdsl.repository.StoreRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;

@ActiveProfiles("local")
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class JpaTest {

    @Autowired
    private StoreRepository storeRepository;

    @Test
    void entity저장후조회() {
        //given
        final Long id = 1L;
        final String storeName = "스토어1";
        final String storeAddress = "주소1";

        Store store = Store.builder()
                .id(id)
                .name(storeName)
                .address(storeAddress)
                .build();
        storeRepository.save(store);

        //when
        Store resultStore = storeRepository.findByName(storeName);

        //then
        Assertions.assertEquals(resultStore.getName(), storeName);
    }

    @Test
    @DisplayName("초기 테스트 시 entity저장후조회 테스트 먼저 진행 후 테스트.")
    void entity수정() {
        //given
        final Long id = 1L;
        final String storeName = "스토어2";
        final String storeAddress = "주소2";
        Store store = Store.builder()
                .id(id)     // id 가 같으면 수정처리함
                .name(storeName)
                .address(storeAddress)
                .build();

        //when
        Store udpateStore = storeRepository.save(store);

        //then
        Assertions.assertEquals(udpateStore.getName(), storeName);
    }

    @Test
    @DisplayName("Store, Staff entity 저장")
    void entity저장() {
        //given
        final Long storeId = 2L;
        final String storeName = "store1234";
        final String storeAddress = "storeAddress";

        final Long staffId = 1L;
        final String staffName = "staff1234";
        final Integer staffAge = 30;

        Staff staff = Staff.builder()
                .id(staffId)
                .name(staffName)
                .age(staffAge)
                .build();

        Store store = Store.builder()
                .id(storeId)
                .name(storeName)
                .address(storeAddress)
                .staff(Arrays.asList(staff))
                .build();

        //when
        Store saveStore = storeRepository.save(store);

        //then
        Assertions.assertEquals(saveStore.getName(), storeName);

        Collection<Staff> staff1 = saveStore.getStaff();
        Iterator<Staff> iterator = staff1.iterator();
        Staff next = iterator.next();
        Assertions.assertEquals(next.getName(), staffName);
    }
}

다음글에서 JPA 테스트 코드 기준으로 작동 원리에 대해서 정리하겠습니다.

반응형