Spring JPA
JPA 소개
왜 JPA를 쓰느냐?
객체지향적 설계가 불가능, 계층형 설계가 불가능
패러다임이 안 맞다
Album extends Item 일 때
Album을 insert하려면?
Insert Into Item
Insert Into Album
객체의 연관관계
테이블의 연관관계
Member.getTeam()는 가능 Team→Member는 불가능
근데 테이블은 양방향임
객체는 객체 그래프를 탐색할 수 있어야 한다.
근데 sql은 처음 실행하는 sql에 따라 탐색할 수 있는 범위가 다르다.
ORM
•
Object-relational mapping
•
객체는 객체대로 설계
•
관계형 데이터베이스는 관계형 데이터베이스대로 설계
•
ORM 프레임워크가 중간에서 매핑
EJB → 하이버네이트 → JPA
EJB: 엔티티 빈
JPA → 인터페이스, 구현체는 다른게 따로 있다.
•
SQL 중심적인 개발에서 객체 중심으로 개발
•
생산성
저장: jpa.persist
조회: jpa.find
수정: member.setName
삭제: jpa.remove
•
유지보수
JPA 필드만 추가하면 됨
•
패러다임의 불일치를 해결
연관관계, 객체 그래프 탐색, 상속 관계를 모두 지원
신뢰할 수 있는 엔티티,계층
JPA로 같은 Id로 조회하면 같게 나온다.
JPA의 성능 최적화 기능
•
1차 캐시와 동일성 비교, JPA로 같은 id를 들고오면 cache로 저장해놓은걸 들고온다.
•
트랜잭션을 지원하는 쓰기 지연
◦
트랜잭션을 커밋할 때까지 Insert Sql을 모음
◦
커밋하는 순간 데이터베이스에 insert Sql을 모아서 보낸다.
•
지연 로딩과 즉시 로딩
◦
Team에 대한 객체를 사용할 때 쿼리를 날린다. (지연 로딩)
◦
지연로딩
즉시 로딩 간의 전환이 설정하나로 다 된다.
JPA 기초와 매핑
객체 매핑하기
@Entity
•
JPA가 관리할 객체, 엔티티라 한다.
@Id
•
DB PK 와 매핑할 필드
persistence.xml 이라는 설정파일이 필요하다. (데이터베이스 접속 정보, option들)
dialect : 데이터베이스 방언
•
각각의 데이터베이스가 제공하는 SQL 문법과 함수는 조금씩 다르다.
•
JPA는 특정 데이터베이스에 종속적이지 않은 기술
•
자바코드는 똑같이 짜는데, DB마다 다르게 짜야한다.
애플리케이션 개발
1.
엔티티 매니저 팩토리 설정
2.
엔티티 매니저 설정
3.
트랜잭션
4.
비즈니스 로직(CRUD)
Persistence : Class → persistence.xml(설정 정보)를 읽은 다음에
EntityManagerFactory(EntityManager를 생성해주는 것) 을 생성해준다.
EntityManagaerFactory는 웹앱 띄울때 딱 한 번만 실행되고, 트랜잭션이 실행될대마다 EntityManager가 생성되서 뭔가를 처리한다.
주의
•
엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유
•
엔티티 매니저는 쓰레드 간에 공유하면 안된다( 데이터베이스 커넥션 그 자체라서, 여러명의 트랜직션이 같은 데이터베이스 커넥션으로 묶여버린다)
필드와 컬럼 매핑
데이터베이스 스키마 자동 생성하기
DDL을 애플리케이션 실행 시점에 자동 생성
테이블 중심 → 객체 중심
데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성
이렇게 생성된 DDL은 개발장비에서만 사용
생성된 DDL은 운영서버에서는 사용하지 않거나, 적절히 다듬은 후 사용
hibernate.hbm2ddl.auto
•
create(Drop+Create)
•
create-drop(create와 같으나 종료시점에 테이블 DROP)
•
update(변경분만 반영, 운영 DB에는 사용하면 안됨)
•
validate(엔티티와 테이블이 정상 매핑되었는지만 확인)
•
none: 사용하지 않음
개발 초기 단게는 create 또는 update
테스트 서버는 update 또는 validate
스테이징과 운영서버는 validate 또는 none
매핑 어노테이션
•
Column(name = "USERNAME");
가장 많이 사용됨
name: 필드와 매핑할 테이블의 컬럼 이름
insertable,updatable = false → 읽기만 가능, insert할 때 안 들어간다.
nullabe = false → nul인지 아닌지 체킹한다.
•
Enumberated(EnumType.String)
열거형 매핑
가급적 String으로 할 것
•
Temporal : 날짜 타입 매핑
DATE → 날짜, TIME → 시간, TIMESTAMP → 날짜와 시간
•
Lob : String에 쓰면 CLOG, byte[]에 쓰면 BLOD
데이터가 너무 크면 그냥 바이트로 밀어넣어야한다.
•
Transient
이 필드는 매핑하지 않는다. 애플리케이션에서 DB에 저장하지 않는 필드
식별자 매핑 어노테이션
•
Id
•
GeneratedValue
IDENTITY: 데이터베이스에 위임, MYSQL
SEQUENCE: 데이터베이스 시퀀스, ORACLE
TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
AUTO: 방언에 따라 자동 선택
권장하는 식별자 전략 → 기본 키 제약 조건: null 아님, 유일, 변하면 안된다.
미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 현재 시점만 보면 유지가 되는데, 미래까지 이 조건을 만족하기는 어렵다. 대리키를 사용하자.
주민번호도 변한다.
비즈니스와 아무 관계가 없는, 대체키를 쓰는거다.
권장: Long + 대체키 + 키 생성전략 사용
int → 10억,20억에서 끝난다. 무조건 Long타입
연관관계 매핑
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.
객체를 테이블에 맞춰서 설계하다보면, 객체를 들고오는데 있어서 두 번 들고와야함
객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
테이블은 조인해서 들고오면 되는데, 객체는 참조가 없어서 db 스타일로 다 맞춰야한다.
연관관계 매핑 이론
1.
단방향 매핑
•
Member에서 Team을 조회할 수 있는데, Team에서는 Member를 조회하지 못한다.
•
@ManyToOne(fetch = FetchType.LAZY) → 지연로딩
•
JoinColumn(name = "TEAM_ID") : Foreign Key 걸기
2.
양방향 매핑
•
Member 에서도 Team, Team에서도 Member
•
Member{id, Team team, username} / Team(id,name,List members)
•
근데 DB는 방향이 없으니까 바뀌는게 없다.
양방향 매핑
반대 방향으로 객체 그래프 탐색
객체 연관관계
회원 → 팀 연관관계 1개(단방향)
팀 → 회원 연관관계 1개(단방향)
테이블 연관관계
회원
팀의 연관관계 1개(양방향)
객체의 양방향 관계
객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다
A→B : a.getB()
B→A: b.getA()
양방향으로 참조하려면, 단방향 2개를 만드는거다.
테이블의 양방향 관계
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐
select * from MEMBER M JOIN TEAM T on M.TEAM_ID = T.TEAM_ID
select * from TEAM T Join MEMBER M on T.TEAM_ID = M.TEAM_ID
members에 집어넣으면 MEMBER 테이블이 업데이트가 되야한다.
그럼 둘 중 누구를 믿어야하나? ( Member에 있는 Team 객체를 수정, Member.setTeam() vs Team.getMembers.append(Member) )
이래서 나온 개념이 Owner
Member의 Team과 Team의 members를 연관관계의 주인으로 정할 수 있다.
둘다 동시에 집어넣으면 연관관계의 주인꺼를 집어넣는다.
양방향 매핑 규칙
•
객체의 두 관계 중 하나를 연관관계의 주인을 정하고, 주인이 아닌거를 mappedBy로 정함.
•
연관관계의 주인만이 외래 키를 관리(등록,수정)
•
주인이 아닌쪽은 읽기만 가능
•
주인은 mappedBy 속성 사용 X
•
주인이 아니면 mappedBy 속성으로 주인 지정
그럼 누구를 주인으로 하는게 맞느냐?
외래키가 있는 곳으로 주인으로 정해라.
Why? Member.team이 연관관계의 주인, 개발자에게 알려줘야함.
그럼 일단 단방향으로 다 설계하고, 양방향이 필요할 때는 반대쪽에 자바코드를 추가하자.
양방향은 조회를 편하게 하기 위해서 하는거다.
양방향 매핑의 장점
단방향 매핑만으로도 이미 연관관계 매핑은 완료
양방향 매핑은 반대방향으로 조회 기능이 추가
JPQL에서 역방향으로 탐색할 일이 많음
단방향 매핑을 잘 하고 양방향은 추가하면 됨.
연관관계 매핑 어노테이션
다대다 (@ManyToMany)
일대일(@OneToOne)
상속 관계 매핑 어노테이션
•
inheritance
•
DiscriminatorColumn
•
DiscriminatorValue
•
MappedSuperclass
상속 관계를 DB테이블로 어떻게 설계할지, Join을 쓰도록 할건지, 아니면 통째로 할건지
복합키 어노테이션
•
IdClass
•
EmbeddedId
•
Embeddable
•
MapsId
JPA 내부구조
영속성 컨텍스트
가장 중요한거, 관계형 데이터베이스 매핑과 이거
엔티티 매니저 팩토리에서 요청 하나에 엔티티 매니저를 할당
엔티티 매니저가 커넥션 풀에서 DB랑 연결함
영속성 컨텍스트 - 엔티티를 영구 저장하는 환경
EntityManager.persist(entity)
영속성 컨텍스트 - 논리적인 개념, 눈에 보이지 않는다. 엔티티 매니저를 통해서 영속성 컨텍스트에 접근
엔티티 매니저와 영속성 컨텍스트가 N:1
엔티티의 생명주기
비영속 (new/transient)
•
객체를 생성한 상태
•
JPA랑 아무 관계가 없기 때문에
영속(managed)
•
생성을 한다
•
em.persist(member) → 이러면 영속상태가 된다.
•
영속 컨텍스트 안에서 관리되기 시작된다.
준영속(detached)
•
관리를 포기해버리는 상태
삭제(removed)
•
DB에서도 날려버리는 거
영속성 컨텍스트의 이점
•
1차 캐시
•
동일성 보장
•
트랜잭션을 지원하는 쓰기 지연
•
변경 감지
•
지연로딩
1.
엔티티 조회, 1차 캐시
•
멤버를 생성한 다음에 멤버를 집어넣으면, 내부에 키랑 밸류로 id와 객체가 저장
•
em.find하면 DB를 가는게 아니라 1차캐시를 먼저 뒤진다.
•
1차 캐시는 쓰레드 하나 생성될 때 잠깐 생기고, 트랜잭션을 시작해서 끝날때까지만 유지. global cache가 아니다.
•
find해서 없다? DB를 조회 → 1차 캐시에 저장 → member2 반환
2.
영속성 엔티티에서 동일성 보장
•
캐시에서 조회하기 때문에, 레퍼런스상 같은거다
3.
트랜잭션을 지원하는 쓰기 지연
•
insert 문이 한 번에 나간다.
•
persist하면 1차 캐시에 저장한다.
•
언제 DB에 넣냐? transaction.commit()을 하면 db에 보낸다.
•
쓰기 지연 SQL 저장소에 있는 SQL을 보낸다. (DB와의 sync를 맞춘다.)
•
commit() = flush(), db내 commit까지 다 함
4.
엔티티 수정 변경 감지
•
1차 캐시가 생성되는 시점에 스냅샷이란걸 떠놓는다.
•
commit이나 flush를 하면 스냅샷이랑 비교를 해서 바뀐게 있으면 바꿔서 넣는다.
•
1차 캐시에서 관리되는걸 영속상태라 그런다.
•
따라서 뭐가 바뀌었는지 안다.
•
뭐하러 하냐..? → 사상 때문에 그렇다. 자바 컬렉션에서 데이터를 들고왔다. 값 변경하고 값을 다시 담지 않는다. 값만 바꾸면 되기 때문
•
em.flush() → DB로 보낸다.
•
em.clear() → 캐시를 지운다.
5.
영속성 컨텍스트를 플러시하는 방법
•
em.flush()
◦
영속성 컨텍스트를 비우지 않음, 변경내용을 데이터베이스에 동기화
◦
트랜잭션이라는 작업 단위가 중요
•
트랜잭션 커밋
•
JPQL 쿼리 실행
6.
JPQL
•
SQL 결과를 내야하니까 캐시를 내보내고 들고온다.
•
JPQL의 경우에는 자동으로 나오는데, 다른 걸 같이 쓰면 자동으로 안나간다.
7.
준영속
•
영속 → 준영속
•
영속 상태의 엔티티가 영속성 컨텍스트에서 분리
•
영속성 컨텍스트가 제공하는 기능을 사용못함
em.detach(entity), em.clear(), em.close()
Member를 조회할 때 Team도 함께 조회해야할까?
나중에 쓸거라면 LAZY를 걸면 된다.
LAZY를 걸면 조회할 때, Proxy객체가 들어온다.
실제 사용하는 시점에 자기 내용을 채운다.
자주 함께 쓴다면 EAGER를 건다.
가급적 지연 로딩을 사용
즉시 로딩을 적용하면 예상하지 못한 SQL이 발생
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
지연로딩을 쓰려면 프록시 객체가 살아있어야하는데, 영속성이 보장되야 함.
트랜잭션이 끝나고 나서, 영속성이 보장이 안되는 경우가 있다.
JPA와 객체지향 쿼리
JPA는 다양한 쿼리 방법을 지원
JPQL
•
java persistence Query Language
•
객체를 대상으로 조회한다는 차이가 있다.
JPA Criteria
QueryDSL
네이티브 SQL
JPQL
검색을 하려면 엔티티 객체를 대상으로 검색
근데 모든 DB데이터를 객체로 변환해서 검색하는 것은 불가능
애플리케이션이 필요한 데이터만 DB에서 불러오려면 검색 조건이 있는 SQL이 있다.
JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
SQL과 문법이 유사 → 엔티티를 대상으로 쿼리를 날림
멤버 엔티티를 검색한거다.
em.createQuery(jpql,Member.class).getResultList();
테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리
SQL을 추상화해서 특정 데이터베이스 SQL에 의존 X
Ex)
select m from Member m where m.age > 18
select 절 from _ 절 [where 절] [groupby _절] [having _절] [orderby _절]
엔티티와 속성은 대소문자 구분, 별칭을 무조건 사용
query.getResultList() → 리스트 반환
query.getSingleResult() → 결과가 정확히 하나
파라미터 바인딩, 이름이나 위치 기준이 모두 가능, 이름이 낫다.
프로젝션
→ select username,age from Member m → 단순 값 프로젝션
new 명령어: 단순한 값을 DTO로 바로 조회
select new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
페이징 API → 다음 두 API로 추상화
setFirstResult : 조회 시작 위치, 0부터 시작
setMaxResult : 조회할 데이터 수
조인에서 약간 다르다.
페치 조인 - 엔티티 객체 그래프를 한번에 조회하는 방법
별칭을 사용할 수 없다.
JPQL: select m from Member m join fetch m.team
멤버를 조회할 때 팀까지 같이 들고온다.
현업에서 list를 뿌릴 때 쿼리가 n번 나간다.
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql,Member.class).getResultList();
for(Member member: members){
System.out.println("username = " + member.getUsername());
}
서브쿼리 지원, 기본적인 함수들 다 됨.
Named쿼리 - 어노테이션
NamedQuery(name = "", query = "") : 이런 식으로 가져다 줄 수 있다.
애플리케이션 로딩 시점에 에러를 잡을 수 있다.
JPA 기반 프로젝트
Spring Data JPA
지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결
개발자는 인터페이스만 작성
스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입
JpaRespository에서 공통적으로 사용되는 JPA가 구현되어있다.
인터페이스가 인터페이스를 받을 때는 extends
save(), findOne(), findAll() , 스프링 프로젝트 생성 시점에 구현 클래스를 만들어서 주입
스프링 데이터 , 스프링 데이터 JPA가 있는데 스프링 데이터 JPA는 스프링 데이터를 확장
와! 메서드 이름으로 쿼리를 생성함
findByName → 멤버 안에 name이라는게 있기 때문에 findByName하면 날라간다.
이름으로 검색 + 정렬 + 페이징 까지 가능함
Pageable page = new PageRequest(1,20,new Sort...);
Page<Member> result = memberRepository.findByName("hello",page);
int total = result.getTotalElements(); 전체 수
List<Members> members = result.getContent(); 데이터
@Query라는 어노테이션으로 쿼리를 짤 수 있다.
Web 페이징과 정렬 기능 → 컨트롤러에서 페이징 처리 객체를 바로 받을 수 있다.
QueryDSL
SQL,JPQL을 코드로 작성할 수 있도록 도와주는 빌더
SQL,JPQL은 문자, Type-check이 불가능 → 컴파일 시점에 알 수 있는 방법이 없다.
해당 로직 실행 전까지는 작동여부 확인 불가
Member.java → QMember.java라는 query전용 객체를 만들 수 있다.
IDE의 도움을 받을 수 있는 것 & 쿼리를 잘못 짤 일이 없다.
제약조건을 조립가능 - 가독성,재사용