JPA
Java Persistence API. 자바 ORM 기술 표준.
자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스로 구현체로는 Hibernate가 있다.
SQL 중심적 개발에서 객체를 중심으로 개발 할 수 있다.
객체 지향과 관계형 데이터베이스의 패러다임이 불일치하면서 발생하는 문제를 해결 하고 값을 DB에 저장하는 것을 컬렉션에 저장하듯이 객체를 저장할 수 있다.
최적화
1차 캐시와 동일성 보장
같은 트랜잭션 안에서는 같은 엔티티를 반환
DB Isolation Level이 Read Commit이어도 어플리케이션에서 Repeatable Read 보장
트랜잭션을 지원하는 쓰기 지연
트랜잭션이 커밋 될 때까지 INSERT SQL을 모아두고 JDBC BATCH SQL을 이용해 한번에 저장한다.
지연 로딩
지연로딩 : 객체가 실제 사용될 때 로딩
즉시로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회
주요 어노테이션
@Entity : JPA가 관리할 객체
@Id: DB PK와 매핑할 필드
@GeneratedValue
- IDENTITY : 데이터베이스에 위임 (MYSQL)
- SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용 (ORACLE)
- TABLE : 키 생성용 테이블 사용
- AUTO : Dialect에 따라 자동 지정
@Column
- name : 필드와 매핑할 테이블 컬럼명
- length : 길이
- insertable, updateable : 읽기전용
- nullable : null 허용 여부
- unique : 유니크 제약 조건
@Temporal
날짜 타입 매핑
@Enumerated
열거형 타입 매핑
Enum을 필드로 가질 때 사용하는데 Ordinal과 String으로 설정할 수 있는데 Ordinal의 경우 enum 값 중간에 새로운 값이 생길 경우 기존 데이터의 전부 꼬이게 된다. 무조건 STRING으로 해야한다.
@Lob
CLOB (String, char[]) , BLOB (byte[]) 매핑
@Trasient
이 필드는 매핑하지 않고 DB에 저장하지 않는다.
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@Temporal(TemporalType.TIMESTAMP)
private Date created;
@Enumerated(EnumType.STRING)
private MemberType memberType;
}
Dialect
SQL 표준을 지키지 않거나 특정 데이터베이스만의 고유한 기능
JPA에서 특정 데이터베이스에 종속되지 않기 위해 사용한다.
사용법
- 저장: jpa.persist(obj);
- 조회: T obj = jpa.find(id);
- 수정: obj.setName(“new name”);
- 삭제: jpa.remove(obj);
-
EntityManagerFactory 설정
-
EntityManager 설정
-
Transaction
-
BusinessLogic
EntityManagerFactory는 하나만 생성해서 어플리케이션 전체에서 공유
EntityManager는 스레드간 공유 하면 안됨. 한 트랜잭션에서 사용하고 제거
JPA의 모든 데이터 변경은 트랜잭션 안에서 실행해야 함
public void test() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("unit name");
EntityManager ef = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
Member member = getMember();
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
Spring data JPA
Spring에서 제공하는 JPA를 쓰기 쉽게 만들어 둔 모듈
EntityManager를 직접 다루지 않고 Repository인터페이스를 사용하면 미리 정해진 규칙대로 메서드를 입력하면, 해당 메서드 이름에 적합한 쿼리를 날리는 구현체를 만들어 Bean에 등록 됨
Spring Data JPA의 Repository 구현체인 SimpleJpaRepository 클래스는 내부적으로 EntityManager를 사용하고 있다.
사용법
환경
OpenJDK 11
Spring Boot 2.3.8
H2
-
의존성 추가
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2' }
-
application.properties에 데이터 소스 설정
# 기본 설정 (h2, hsql의 메모리 db는 아무 설정 안해도 됨) spring.datasource.url=jdbc:h2:tcp://localhost/~/test # spring.datasource.url=jdbc:[db]://[host]:[db-name] spring.datasource.username=zkdlu spring.datasource.password=1234 # 생성되는 sql 확인용 spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true # ? 로 출력되는거 로깅으로 확인하기 logging.level.org.hibernate.SQL=debug logging.level.org.hibernate.type.descriptor.sql=trace
데이터베이스 스키마 자동 생성 spring.jpa.hibernate.ddl-auto
- create : 기존 테이블 삭제 후 다시 생성
- create-drop : create와 동일. 종료 시점에 테이블 drop
- update : 변경된 것만 반영
- validate : 엔티티와 테이블이 정상 매핑되었는지만 확인
- none : 사용 안함
-
Repository 생성
public interface MemberRepository extends JpaRepository<Member, Long> { }
JpaRepository 구현체에 이미 @Repository어노테이션이 달려있음
-
테스트
public class JpaTestApplication implements ApplicationRunner { .... @Autowired private MemberRepository memberRepository; @Override public void run(ApplicationArguments args) throws Exception { Member member = getMember(); memberRepository.save(member); } }
연관관계
@JoinColumn : 외래 키를 매핑할 때 사용
- name : 매핑할 외래키 이름 (“필드명_참조테이블 기본키 컬럼명”)
@OneToOne : 1:1
@OneToMany : 1:M
@ManyToOne : N:1
@ManyToMany : N:M
단방향 매핑
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
}
양방향 매핑
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
양방향 매핑 규칙
-
객체의 두 관계중 하나를 연관관계의 주인으로 지정
mappedBy
-
연관관계의 주인만이 외래 키를 관리
위 경우 Member가 주인이며, Team 객체에서 Member를 변경해도 DB에 반영되지 않음
-
주인이 아닌쪽은 읽기만 가능
-
주인은 mappedBy 속성 사용 X
-
주인이 아니면 mappedBy 속성으로 주인 지정
외래 키가 있는 곳을 주인으로 정하자
public void test() throws Exception {
Member findMember = memberRepository.findById(2L).get();
Team findTeam = teamRepository.findById(4L).get();
findTeam.getMembers().add(findMember); //동작하지 않는다.
findMember.setTeam(findTeam); // 동작한다.
}
Trouble shooting
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role
양방향 매핑을 한 후 팀에 존재하는 멤버를 조회하면 해당 예외가 발생하였다.
-
원인
해당 예외는 member를 호출할 때, 영속성 컨텍스트가 종료되어 지연 로딩을 할 수 없어서 발생하는 예외로 일반적으로 트랜잭션 밖에서 조회할 경우 발생한다.
-
해결 방법
-
@Transactional 어노테이션 추가
-
또는 즉시 로딩으로 변경
@OneToMany(mappedBy = "team", fetch = FetchType.EAGER)
-