객체 지향 패러다임
- 시스템을 구성하는 객체들에게 적절한 책임을 할당하는 것
- 상속
- 연관 관계
- 객체의 연관관계에는 방향성이 있다.
- 테이블의 연관관계는 방향성이 없다.
( 연관 관계 -> 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)
}
}
@Entity
: 엔티티 클래스임을 지정하여 테이블과 매핑@Table
: 매핑될 테이블을 지정하고 생략 시 엔티티 클래스 이름과 같은 테이블로 매핑( 생략 시 대소문자 구분은 설정이나 sql 사에 따라 다를 수 있음 )
@Id
: 반드시 pk 가 있어야함. pk를 지정하는 annotation@GeneratedValue
: pk의 생성 규칙@Column
: 필드와 맵핑. 테이블의 컬럼이름 지정. notnull, unique, type, length 등을 지정매게변수 없는 생성자 : 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);
}
- save() 실행시 INSERT SQL을 생성 후 쓰기 지연 SQL 저장소에 저장
- 1차 캐시에 id, entity 를 저장
- 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 |
---|