JPA 사용하기

duckbill413·2023년 11월 10일
0

Spring JPA

목록 보기
1/7
post-thumbnail

JPA

  • 어플리케이션과 데이터베이스는 ORM이라는 개념이다.
  • JPA는 자바 ORM의 표준 스택으로 인터페이스 형태로 제공하여 준다.
  • JPA의 실제 구현 클래스를 모아 놓은 것이 Hibernate 그중 스프링에서 자주 사용되는 것을 모아놓은 것이 Spring data jpa이다.

영속성(Persistence)

  • 데이터를 생성한 프로그램이 종료되더라도 사라지지 않는 데이터의 특성
  • 영속성을 갖지 않는 데이터는 단지 메모리에서만 존재하기 때문에 프로그램을 종료하면 모두 잃어버리게 된다.
  • Object Persistence(영구적인 객체)
    • 메모리 상의 데이터를 파일 시스템, 관계형 데이터베이스 혹인 객체 데이터베이스 등을 활용하여 영구적으로 저장하여 영속성을 부여한다. https://gmlwjd9405.github.io/images/database/orm-persistence.png
    • 데이터를 데이터베이스에 저장하는 3가지 방법
      1. JDBC (java에서 사용)
      2. Spring JDBC (ex. JdbcTemplate)
      3. Persistence Framework (ex. Hibernate, Mybatis 등)

ORM 이란

ORM이란
Object Relational Mapping, 객체-관계 매핑

  • 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 것을 말한다.
    • 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다.
    • 객체 모델과 관계형 모델 간에 불일치가 존재한다.
    • ORM을 통해 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 불일치를 해결한다.
  • 데이터베이스 데이터 ← 매핑 → Object 필드
    • 객체를 통해 간접적으로 데이터베이스 데이터를 다룬다.
  • Persistant API라고도 할 수 있다. ex) JPA, Hibernate

ORM의 장단점

장점

  • 객체 지향적인 코드로 인해 더 직관적이고 비즈니스 로직에 더 집중할 수 있게 도와준다.
    • ORM을 이용하면 SQL Query가 아닌 직관적인 코드(메서드)로 데이터를 조작할 수 있어 개발자가 객체 모델로 프로그래밍하는 데 집중할 수 있도록 도와준다.
    • 선언문, 할당, 종료 같은 부수적인 코드가 없거나 급격히 줄어든다.
    • 각종 객체에 대한 코드를 별도로 작성하기 때문에 코드의 가독성을 올려준다.
    • SQL의 절차적이고 순차적인 접근이 아닌 객체 지향적인 접근으로 인해 생산성이 증가한다.
  • 재사용 및 유지보수의 편리성이 증가한다.
    • ORM은 독립적으로 작성되어있고, 해당 객체들을 재활용 할 수 있다.
    • 때문에 모델에서 가공된 데이터를 컨트롤러에 의해 뷰와 합쳐지는 형태로 디자인 패턴을 견고하게 다지는데 유리하다.
    • 매핑정보가 명확하여, ERD를 보는 것에 대한 의존도를 낮출 수 있다.
  • DBMS에 대한 종속성이 줄어든다.
    • 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하기 때문에 RDBMS의 데이터 구조와 Java의 객체지향 모델 사이의 간격을 좁힐 수 있다.
    • 대부분 ORM 솔루션은 DB에 종속적이지 않다.
    • 종속적이지 않다는것은 구현 방법 뿐만아니라 많은 솔루션에서 자료형 타입까지 유효하다.
    • 프로그래머는 Object에 집중함으로 극단적으로 DBMS를 교체하는 거대한 작업에도 비교적 적은 리스크와 시간이 소요된다.
    • 또한 자바에서 가공할경우 equals, hashCode의 오버라이드 같은 자바의 기능을 이용할 수 있고, 간결하고 빠른 가공이 가능하다.

단점

  • 완벽한 ORM 으로만 서비스를 구현하기가 어렵다.
    • 사용하기는 편하지만 설계는 매우 신중하게 해야한다.
    • 프로젝트의 복잡성이 커질경우 난이도 또한 올라갈 수 있다.
    • 잘못 구현된 경우에 속도 저하 및 심각할 경우 일관성이 무너지는 문제점이 생길 수 있다.
    • 일부 자주 사용되는 대형 쿼리는 속도를 위해 SP를 쓰는등 별도의 튜닝이 필요한 경우가 있다.
    • DBMS의 고유 기능을 이용하기 어렵다. (하지만 이건 단점으로만 볼 수 없다 : 특정 DBMS의 고유기능을 이용하면 이식성이 저하된다.)
  • 프로시저가 많은 시스템에선 ORM의 객체 지향적인 장점을 활용하기 어렵다.
    • 이미 프로시저가 많은 시스템에선 다시 객체로 바꿔야하며, 그 과정에서 생산성 저하나 리스크가 많이 발생할 수 있다.

JPA Query Method

Query return types

Query subject keywords

Query predicate keyword

Query predicate modifier keywords

JPA 사용하기

JPA, H2-Database 사용

JPA 및 h2-database를 사용하기 위해서는 아래와 같은 dependency가 필요하다.

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true
    generate-ddl: false
    hibernate:
      ddl-auto: create
    defer-datasource-initialization: true
  • show-sql: sql 사용 로그 출력
  • format_sql: sql 사용을 보다 정리해줌
  • ddl-auto: spring 실행시 table을 create 한다.
  • defer-datasource-initialization: Spring 2.5 부터 resouce의 sql을 spring 생성시 초기화 하지 않기 때문에 초기화 하기 위해서 true로 설정해준다.

H2-Database 실행

spring:
  h2:
    console:
      enabled: true

JpaRepository 생성하기

public interface MemberRepository extends JpaRepository<Member, Long> {
}

JpaRepository를 상속하여 사용

JpaRepository - CRUD

1. 정렬, 검색, 삽입, 조회 하기

@Test
void crud(){ // create, read, update, delete

    System.out.println(">>> name 을 기준으로 내림차순 출력");
    List<Member> members1 = memberRepository.findAll(Sort.by(Sort.Direction.DESC, "name"));
    members1.forEach(System.out::println);

    System.out.println(">>> ID 가 1, 3, 5번인 Member 출력");
    List<Member> members2 = memberRepository.findAllById(Lists.newArrayList(1L, 3L, 5L));
    members2.forEach(System.out::println);

    System.out.println(">>> 멤버 삽입");
    Member member1 = new Member("duckbill", "duckbill@gmail.com");
    Member member2 = new Member("pypy", "pypy@gmail.com");
    memberRepository.saveAll(Lists.newArrayList(member1, member2));
    memberRepository.findAll().forEach(System.out::println);

    System.out.println("Member 중 ID 1 멤버 조회");
    Member member = memberRepository.findById(1L).orElse(null);
    System.out.println(member);
}

2. Count, Exists

@Test
void countAndExists(){
    long count = memberRepository.count();
    System.out.println(count);

    boolean exists = memberRepository.existsById(1L); // count 를 참조
    System.out.println(exists);
}

3. Delete, DeleteInBatch

  • Delete
    @Test
    void delete(){ // FEAT: n개의 delete 에 대해 n번의 delete 명령 발생
        memberRepository.delete(memberRepository.findById(1L).orElseThrow(RuntimeException::new));
        memberRepository.findAll().forEach(System.out::println);
    }
    • delete에서 null 이 들어오면 에러가 발생하므로 orElseThrow 를 활용
    • n개의 delete 에 대해 n번의 delete 명령 발생
  • DeleteInBatch
    @Test
    void deleteInBatch(){ // FEAT: n개의 delete 에 대해 1번의 delete 명령 발생 (or 명령)
        memberRepository.deleteAllByIdInBatch(Lists.newArrayList(1L, 3L));
        memberRepository.findAll().forEach(System.out::println);
    }
    • n개의 delete 에 대해 n번의 delete 명령 발생

4. Paging

데이터가 많을 때 Paging을 이용하여 데이터를 요구할 수 있다.

@Test
void paging(){ // 페이징 기법
    Page<Member> members = memberRepository.findAll(PageRequest.of(0, 3)); // 크기가 3인 페이지의 0번쨰 장
    System.out.println("page: " + members);
    System.out.println("totalElements: " + members.getTotalElements());
    System.out.println("totalPages: " + members.getTotalPages());
    System.out.println("numberOfElements: " + members.getNumberOfElements());
    System.out.println("sort: " + members.getSort());
    System.out.println("size: " + members.getSize());
    members.stream().forEach(System.out::println);
}
  • Page interface 활용

5. Example

Query by example : entity를 example로 만들고 mathcer을 추가하여 쿼리를 생성하고 검색하는 방식

@Test
void qbe(){ // MEMO: query by example : entity 를 example 로 만들고 matcher 를 추가하여 쿼리를 생성
    ExampleMatcher matcher = ExampleMatcher.matching()
            .withIgnorePaths("name") // name 은 무시
            .withMatcher("email", endsWith()); // email 은 뒷 부분 검색

    Example<Member> example = Example.of(new Member("ma", "gmail.com"), matcher);
    memberRepository.findAll(example).forEach(System.out::println);
}

@Entity

@Table

  • name: Table을 생성할 때 이름을 변경할 수 있다. 일반적으로는 잘 변경하지 않는다.
  • uniqueConstraints: DDL 생성시에 유니크 제약조건을 만든다. 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용된다. 복합키의 기능으로 사용된다. indexes 인덱스는 추가 쓰기 및 저장 공간을 희생하여 테이블에 대한 데이터 검색 작업의 속도를 향상시키는 데이터 구조입니다. JPA 인덱싱 참고 자료

AUTO 전략 / 기본키 매핑 정리

매핑 전략GenerationType.AUTO는 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다.

기본키 매핑 전략

  • SEQUENCE: 데이터베이스 시퀸스에서 식별자 값을 획득한 수 영속성 컨텍스트에 저장 (Oracle)
  • IDENTITY: 데이터베이스에 엔티티를 저장해서 식별자 값을 획득한 후 영속성 컨텍스트에 저장한다. (IDENTITY 전략은 테이블에 데이터를 저장해야 식별자 값을 획득할 수 있다.) (MySQL)
  • TABLE: 데이터베이스 시퀸스 생성용 테이블에서 식별자 값을 획득한 후 영속성 컨텍스트에 저장

@Column

  • name: 데이터베이스의 Column과 Object name을 별도로 매핑하기 위해서 사용
  • nullable: default는 null 이지만 false로 두면 not null로 테이블을 생성하게 된다.
  • unique: column 하나만 unique 키로 둘때 사용
  • length: default=255 Varchar 등의 크기를 결
  • insertable / updatable: DDL 뿐만 아니라 DML에도 영향을 끼친다. 값이 false이면 insert / update 시에 반영되지 않는다.
  • columnDefinition(DDL): 데이터베이스 컬럼 정보를 직접 줄 수 있다.

@Transient

해당 어노테이션을 붙인 Column은 영속성 처리에서 제외되기 때문에 DB에 반영되지 않는다. 객체에서만 사용된다.

@Transient
private String testData;

@Enumerate

자바의 enum 타입을 매핑할 때 사용한다.

Enum class의 생성

public enum Gender {
    MALE,
    FEMALE
}

Enum의 경우 일반적으로 DB에 저장될때 순서대로 숫자를 부여하여 저장된다.(위의 경우 MALE=0, FEMALE=1) 하지만 이런 경우 MALE의 앞에 BABY와 같이 새로이 생성된다면 전부 틀어지게 된다. 위와 같은 경우를 대비하여 @Enumerated(value=EnumType.STRING) 으로 String 형태로 저장될 수 있게 해주는 것이 안전하다.

@Temporal

날짜 타입 (java.util.Date, java.util.Calendar)을 매핑할 때 사용한다.

  • TemporalType.DATE: 날짜, 데이터베이스 date 타입과 매핑 (예: 2023-10-10)
  • TemporalType.TIME: 시간, 데이터베이스 time 타입과 매핑 (예: 15:02:11)
  • TemporalType.TIMESTAMP: 날짜와 시간, 데이터베이스 timestamp 타입과 매핑 (예: 2023-10-10 15:02:11)

@Transient

이 필드는 매핑하지 않는다. 따라서, 데이터베이스에 저장하지 않고 조회하지도 않는다. 임시로 어떤 값을 보관하고 싶을 때 사용한다.

@Transient
private Integer count;

Entity Listener

Entity listener 에는 다음과 같이 7가지가 있다.

  • @PrePersist: insert 메소드가 호출되기 전에 실행 @PrePersist를 이용한 createdAt, updatedAt 초기화
    @PrePersist
    public void prePersist() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }
  • @PreUpdate: Update Method 호출 전 실행 @PreUpdate를 이용한 updatedAt 업데이트
    @PreUpdate
    public void preUpdate(){
        this.updatedAt = LocalDateTime.now();
    }
  • @PreRemove: Remove Method 호출 전 실행
  • @PostPersist: insert Method 호출 후 실행
  • @PostUpdate: update Method 호출 후 실행
  • @PostRemove: remove Method 호출 후 실행
  • @PostLoad: select 조회가 일어난 이후 실

CreatedAt / UpdatedAt - Entity Listener

EntityListener 클래스 생성

public class MyEntityListener {
    @PrePersist
    public void prePersist(Object o){
        if (o instanceof Auditable){
            ((Auditable) o).setCreatedAt(LocalDateTime.now());
            ((Auditable) o).setUpdatedAt(LocalDateTime.now());
        }
    }

    @PreUpdate
    public void preUpdate(Object o){
        if (o instanceof Auditable){
            ((Auditable) o).setUpdatedAt(LocalDateTime.now());
        }
    }
}

Auditable Interface 생성

public interface Auditable {
    LocalDateTime getCreatedAt();
    LocalDateTime getUpdatedAt();

    void setCreatedAt(LocalDateTime createdAt);
    void setUpdatedAt(LocalDateTime updatedAt);
}

interface를 이용하여 Entity 클래스의 기능 정의

Entity에 Entity Listener 등록

@Entity
@EntityListeners(value = MyEntityListener.class)
public class Book implements Auditable
  • Entity 클래스에 EntityListener 클래스 등록
  • Entity 클래스에 interface implement

History 저장

  1. Member 클래스 저장을 위하여 Clone 클래스 생성

    MemberHistory class

    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Data
    @Builder
    public class MemberHistory implements Auditable{
        @Id
        @GeneratedValue
        private Long id;
        private Long memberId;
        private String name;
        private String email;
        private LocalDateTime createdAt;
        private LocalDateTime updatedAt;
    }
  2. MemberHistoryRepository 생성

  3. MemberEntityListener 클래스 생성

    PrePersist, PreUpdate 사용

    public class MemberEntityListener {
        // FIXME: EntityListener 은 spring bean 을 주입받지 못한다.
    //    @Autowired
    //    private MemberHistoryRepository memberHistoryRepository;
        @PrePersist
        @PreUpdate
        public void prePersistAndUpdate(Object o){
            MemberHistoryRepository memberHistoryRepository = BeanUtils.getBean(MemberHistoryRepository.class);
            Member member = (Member) o;
    
            MemberHistory memberHistory = MemberHistory.builder()
                    .memberId(member.getId())
                    .name(member.getName())
                    .email(member.getEmail())
                    .createdAt(member.getCreatedAt())
                    .updatedAt(member.getUpdatedAt())
                    .build();
            memberHistoryRepository.save(memberHistory);
        }
    }
    • EntityLister 클래스는 spring bean을 주입받지 못하기 때문에 별도의 방법으로 memberHistoryRepository를 주입하여야 한다.
    • ApplicationContextAware를 활용하여 주입할 수 있다.
  4. BeanUtils 클래스 생성 (ApplicationContextAware)‼️

    주입하고자하는 class 이름을 입력받으면 해당하는 클래스를 리턴

    @Component
    public class BeanUtils implements ApplicationContextAware {
        private static ApplicationContext applicationContext;
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            BeanUtils.applicationContext = applicationContext;
        }
    
        public static <T> T getBean(Class<T> clazz){
            return applicationContext.getBean(clazz);
        }
    }
  5. Member의 EntityListener에 MemberEntityListener.class 추가

Spring Listener의 사용

  1. @EnableJpaAuditing 어노테이션 부착

    @SpringBootApplication
    @EnableJpaAuditing
    public class JpaRepositoryApplication {
        public static void main(String[] args) {
            SpringApplication.run(JpaRepositoryApplication.class, args);
        }
    }
    • 단, 위와 같이 설정할 경우 Test파일에서는 JPA가 잘 동작하지 않으므로 JpaConfig와 같은 클래스를 생성하고 @EnableJpaAuditing 어노테이션을 부착하는 것이 좋다.
  2. EntityListener에 AuditingEntityLister.class 추가

    @EntityListeners(value = AuditingEntityListener.class)
  3. @CreatedDate, @LastModifiedDate를 사용하여 날짜 추가

    @CreatedDate
    private LocalDateTime createdAt;
    @LastModifiedDate
    private LocalDateTime updatedAt;

BaseEntity

지금까지의 경우 createdAt, updatedAt을 사용하는 클래스 모두에 해당 필드를 생성하여 초기화 해주었다. 지금부터는 BaseEntity 클래스를 생성하여 createdAt, updatedAt 필드를 생성하고 BaseEntity를 상속하는 방식으로 진행해 볼 수 있다.

  1. BaseEntity 클래스 생성

    @Data
    @MappedSuperclass // 해당 클래스의 필드를 상속받는 클래스의 필드로 추가
    @EntityListeners(value = AuditingEntityListener.class)
    public class BaseEntity {
        @CreatedDate
        private LocalDateTime createdAt;
        @LastModifiedDate
        private LocalDateTime updatedAt;
    }
    • @MappedSuperClass: 해당 클래스의 필드를 상속받는 클래스의 필드로 추가
  2. 하위 클래스에서 BaseEntity 클래스의 필드를 제대로 이용하기 위해서 super 클래스 호출에 대해 재정의 해주어야 한다.

    @ToString(callSuper = true)
    @EqualsAndHashCode(callSuper = true)
    public class Member extends BaseEntity implements Auditable {
    • @ToString 으로 toString 사용시에 상위 클래스 필드의 내용도 출력하게 해야한다.
    • @EqualsAndHashCode: 관련 필드를 기준으로 모든 개체에 의해 상속된 equals 및 hashCode 메서드에 대한 구현을 생성합니다. (callSuper=true)를 통해 하위 클래스에서 상위 클래스의 필드도 생성해준다.
  3. 추가적으로 BaseEntityAuditableimplements 해주면 하위 클래스는 자동적으로 상속 받게 되므로 삭제해도 무관하다.

    @Test
    void crud(){ // create, read, update, delete
    
        System.out.println(">>> name 을 기준으로 내림차순 출력");
        List<Member> members1 = memberRepository.findAll(Sort.by(Sort.Direction.DESC, "name"));
        members1.forEach(System.out::println);
    
        System.out.println(">>> ID 가 1, 3, 5번인 Member 출력");
        List<Member> members2 = memberRepository.findAllById(Lists.newArrayList(1L, 3L, 5L));
        members2.forEach(System.out::println);
    
        System.out.println(">>> 멤버 삽입");
        Member member1 = new Member("duckbill", "duckbill@gmail.com");
        Member member2 = new Member("pypy", "pypy@gmail.com");
        memberRepository.saveAll(Lists.newArrayList(member1, member2));
        memberRepository.findAll().forEach(System.out::println);
    
        System.out.println("Member 중 ID 1 멤버 조회");
        Member member = memberRepository.findById(1L).orElse(null);
        System.out.println(member);
    }
profile
같이 공부합시다~

0개의 댓글