본문으로 바로가기

[JPA] Spring Data JPA

category Programming/JPA 2021. 5. 29. 16:45

객체 지향 패러다임

  • 시스템을 구성하는 객체들에게 적절한 책임을 할당하는 것
  • 상속
  • 연관 관계
    • 객체의 연관관계에는 방향성이 있다.
    • 테이블의 연관관계는 방향성이 없다.

( 연관 관계 -> A 객체에서 B 객체를 의존한다면 A -> B 의 연관관계를 가지고 있음. )

  • 객체는 자유롭게 객체 그래프를 탐색할 수 있어야 한다.

SQL을 직접 다룰 때 문제점

  • 새로운 필드 추가 시 관련된 SQL을 다 수정해야한다.
class Station {
    Long id;
    String name;
}
INSERT INTO statins('id', 'name') VALUES ...
SELECT 'id', 'name' FROM station
UPDATE station SET ...
  • 개발자들이 엔티티를 신뢰하고 사용할 수 없다.

ORM

JPA는 자바 진영의 ORM 프레임워크이다. JPA는 하나의 인터페이스 ( hibernate )

  • 데이터베이스 생성 schema.sql 를 자동으로 지원해준다.

    • 편한 만큼 리스크가 있다. ( 옵션을 잘 못 설정하면 지금까지의 데이터가 모두 날아갈 수도 있음. )
  • spring.jpa.hibernate.ddl-auto=create 속성을 추가해 실행 시점에 테이블 자동 생성 가능하다.

create: 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
create-drop: create와 같으나 종료시점에 테이블 DROP (embaded DB default)
update: 변경된 부분만 반영 (운영 DB에 사용하면 안됌)
validate: entity와 table이 정상 매핑되었는지만 확인
none: 사용하지 않음 (non embaded DB default)

JPA annotation

@Entity // (1)
@Table(name = "station") // (2)
public class Station {
    @Id // (3)
    @GeneratedValue(strategy = GenerationType.IDENTITY) // (4)
    private Long id;

    @Column(name = "name", nullable = false) // (5)
    private String name;

    protected Station() { // (6)
    }
}
  1. @Entity : 엔티티 클래스임을 지정하여 테이블과 매핑

  2. @Table : 매핑될 테이블을 지정하고 생략 시 엔티티 클래스 이름과 같은 테이블로 매핑

    ( 생략 시 대소문자 구분은 설정이나 sql 사에 따라 다를 수 있음 )

  3. @Id : 반드시 pk 가 있어야함. pk를 지정하는 annotation

  4. @GeneratedValue : pk의 생성 규칙

  5. @Column : 필드와 맵핑. 테이블의 컬럼이름 지정. notnull, unique, type, length 등을 지정

  6. 매게변수 없는 생성자 : JPA 에서 무조건 필요함. 가급적 protected로 만듬

예제

spring.jpa.properties.hibernate.format_sql=true

spring.jpa.show-sql=true

두 설정으로 콘솔 창에 테이블 생성 DDL 출력.

create table station (
    id bigint generated by default as identity,
    name varchar(255) not null,
    primary key (id)
)
  • generated by default as identity : auto increament
package subway.domin;

import javax.persistence.*;

@Table(name = "station")
@Entity
public class Station {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    protected Station() {
    }

    public Station(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}
  • @GeneratedValue(strategy = GenerationType.IDENTITY)
    • 기본 키 생성을 데이터 베이스에서 처리
    • null 값으로 row 생성 시 auto increament ( sql 벤더사마다 다름 )
  • @GeneratedValue(strategy = GenerationType.SEQUENCE) : 데이터베이스 Sequence Object 사용
  • @GeneratedValue(strategy = GenerationType.TABLE) : 키 생성 전용 테이블을 생성
    • ```@TableGenerate``와 함께 사용
    • (strategy = GenerationType.TABLE, generator = "MEMBER_SEQ_GENERATOR")
  • @GeneratedValue(strategy = GenerationType.AUTO) : sql 벤더사별 키 생성 규칙에 자동 맞춤
  • 가급적 GeneratedValue를 지정하는 것이 좋음
package subway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

실행결과

Hibernate: 

    drop table if exists station CASCADE 
Hibernate: 

    create table station (
       id bigint generated by default as identity,
        name varchar(255) not null,
        primary key (id)
    )

Spring Data JPA

기존

class StationRepository {
    void save(Station station) {...}
    Station findById(Long id) {...}
    List<Station> findAll() {...}
    Station findByName(String name) {...}
}

Spring Data JPA

interface StationRepository extends JpaRepository<Station, Long> {
    Station findByName(String name);
}
  • 기본 반복 작업의 CRUD 메서드를 기본적으로 제공
  • JpaRepository<T, ID> : spring 프레임워크의 jpa 모듈 T : 엔터티 타입, ID : ID
  • 메서드 네임으로 sql query 자동생성 Appendix C: Repository query keywords

예제

save() 자동생성

findByName()

테스트

package subway.domin;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class StationRepositoryTest {
    @Autowired // JpaRepository를 상송받은 interface 는 자동으로 bean으로 인식
    private StationRepository station;

    @Test
    void save() {
        Station expected = new Station("잠실역");
        Station actual = station.save(expected);
        assertThat(actual.getId()).isNotNull();
        assertThat(actual.getName()).isEqualTo("잠실역");
    }

    @Test
    void findByName() {
        Station expected = new Station("잠실역");
        station.save(expected);

        Station actual = station.findByName("잠실역");
        assertThat(actual.getId()).isNotNull();
        assertThat(actual.getName()).isEqualTo("잠실역");
        assertThat(actual).isSameAs(expected);
    }
}

실행결과

Hibernate: 
    insert 
    into
        station
        (id, name) 
    values
        (null, ?)
Hibernate: 
    insert 
    into
        station
        (id, name) 
    values
        (null, ?)
Hibernate: 
    select
        station0_.id as id1_0_,
        station0_.name as name2_0_ 
    from
        station station0_ 
    where
        station0_.name=?
  • assertThat(actual).isSameAs(expected); 실행 결과 값 뿐만아니라 주소값도 같다는 결과를 도출

영속성 컨텍스트

  • 인메모리 컬렉션
  • 엔티티 영구 저장하는 환경
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
  • 애플리케이션과 데이터베이스 사이의 버퍼

  • 1차 캐시의 id 값에 Entity를 가지고 있기 때문에 id 값이 있으면 db조회를 하지않고 entity를 바로 가져옴

  • id값이 없으면 db 조회 후 entity를 1차 캐시에 저장 후 조회

@Transactional

  • 트랜잭션을 커밋하는 순간 영속성 컨텍스트를 데이터베이스에 저장
    • 영속성 컨텍스트에 대해선 공부
엔티티의 생명주기
* 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
* 영속(managed): 영속성 컨텍스트에 저장된 상태
* 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
* 삭제(removed): 삭제된 상태
@Test
    void findByName() {
        Station expected = new Station("잠실역"); // 비영속 상태
        station.save(expected); // 영속 상태

        Station actual = station.findByName("잠실역");
        assertThat(actual.getId()).isNotNull();
        assertThat(actual.getName()).isEqualTo("잠실역");
        assertThat(actual).isSameAs(expected);
    }
  1. save() 실행시 INSERT SQL을 생성 후 쓰기 지연 SQL 저장소에 저장
  2. 1차 캐시에 id, entity 를 저장
  3. flush() 후 DB에 저장

save() 이후에 findByName("잠실역") 이 조회가 가능한 이유는

ID 기반이 아닌 name 등의 컬럼으로 조회 시, 조회 이전까지 했던 행위들을 flush() 하기 때문이다.

flush() 된 데이터도 트랜젝션이 끝나지 않았다면 Rollback이 가능하다.

변경 감지

  • dirty checking
  • 처음 DB에서 조회 하면 1차 캐시에 저장하면서 스냅샷에 저장을 해둔다
    • 스냅샷은 처음 조회된 상태를 저장
  • 후에 flush() 시에 entity와 스냅샷을 비교하여 변경내역이 있다면 update 쿼리를 실행

예제 1

@Test
    void update() {
        Station station1 = stations.save(new Station("잠실역"));
        station1.changeName("몽촌토성역"); 
        Station station2 = stations.findByName("몽촌토성역");
        assertThat(station2).isNotNull(); // true
    }

실행결과

Hibernate: 
    insert 
    into
        station
        (id, name) 
    values
        (null, ?)
Hibernate: 
    update
        station 
    set
        name=? 
    where
        id=?
Hibernate: 
    select
        station0_.id as id1_0_,
        station0_.name as name2_0_ 
    from
        station station0_ 
    where
        station0_.name=?

예제 2

@Test
    void update() {
        Station station1 = stations.save(new Station("잠실역"));
        station1.changeName("몽촌토성역");
        station1.changeName("잠실역");
        Station station2 = stations.findByName("몽촌토성역");
        assertThat(station2).isNotNull();
    }

실행결과

Hibernate: 
    insert 
    into
        station
        (id, name) 
    values
        (null, ?)
Hibernate: 
    select
        station0_.id as id1_0_,
        station0_.name as name2_0_ 
    from
        station station0_ 
    where
        station0_.name=?
  • 두번째 잠실역으로 변경 후 entity와 스냅샷의 비교 결과 동일하기 때문에 update 쿼리가 발생하지 않음

merge : 준영속 -> 영속

persistance : 비영속 -> 영속

'Programming > JPA' 카테고리의 다른 글

[JPA] 연관 관계  (0) 2021.05.29