[ JPA ] JPA <=> DB , Entity / Repository
DB를 조작, 정의하고자 할 때 SQL 문, 쿼리문을 사용해야 합니다.
SQL문을 사용해 DB 와 연결하는 과정에서, 매 쿼리문 마다 반복되는 코드가 많았고,
JDBC => Spring JDBC => SQL Mapper ( Mybatis )
이를 간결하게 만들기 위한 방법들이 발전해오며, Mybatis도 여전히 많이 사용되고 있습니다.
하지만 SQL문에 의존적인 면이 있어서,
이를 자동화시키고, 객체지향적 쿼리를 만들고자
=> Hibernate ( open source ) 가 등장하게 되었고,
=> Spring JPA 로 발전 해 오게 되었습니다.
여전히 DB 에는 SQL 문을 통해 전달하지만,
개발자 입장에서는 JPA에서 미리 만들어 놓은
findByEmail();
등과 같은 메소드만 사용하면
select * from BOARD t where t.email =:email;
=> 자동으로 SQL문으로 변환을 해주는 장치가 추가된 것이라 보면 될 것 같습니다.
이런 방식으로, JPA는 'SQL 문에서 벗어나자' 라는 방향성으로
DML 뿐만 아니라, DDL 문 또한 자동화 시키는 방법을 만들어 두었습니다.
create table BOARD( );
=> 이렇게 sql문으로 직접 테이블을 생성하던 걸
@ 어노테이션 방식을 통해, 자동으로 테이블을 생성할 수 있게 해줍니다. ( jpa ddl auto - 개발용 only )
** 하지만 실무에서는 테이블 설계 부터해서 DB 설계는 복잡하고, 매우 중시되는 단계이기 때문에 DDL 에 있어서는
1. 먼저 SQL 문으로 직접 데이터베이스를 구성하고
2. JPA 에서는 만들어진 DB 테이블들에 '맞춰서' @어노테이션 등으로 Entity 를 구성하는
순서로 진행되고 있습니다.
[ @Entity ]
: DB 의 Table 과 매칭됩니다.
@Entity
@Table(name = "todo")
public class TodoEntity {
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String todoId;
@Column(nullable = false, length = 30)
private String title;
private boolean done; //기본값 false 니까 insert 할 때 안넣어줘도 자동 false
@CreationTimestamp
private LocalDateTime createdDate;
//회원과 관계 설정
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private UserEntity user;
//우회용 외래키 => 애가 joinColumn 에서 객체 대신 String 으로 차지
@Column(name = "user_id")
private String userId;
}
@Table(name="테이블명")
@Column(name="컬럼명", nullable=false, unique=true)
@Id
=> PK 의미, notnull unique
- Entity 에 @Id 즉, PK 설정 필수
@GeneratedValue(strategy= GenerationType. ~
.IDENTITY : MySQL DB 에서 auto_increment 역할
.SEQUENCE : 오라클DB에서 auto_increment 역할
.AUTO : 위의 기능을 자동으로 해주지만 명확하지 않음
.TABLE : 기본키 설정 방법이 따로 있을 때 사용
[ UUID PK ]
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
private String todoId;
@CreationTimestamp
: insert 시점 서버시간 stamp
@UpdateTimestamp
: update 시점 서버시간 stamp
@Enumerated(EnumType.ORDINAL / STRING )
=> Enum 클래스에 붙여서 0,1 index로 가져올지, String 자체로 가져올지 정함
@ManyToOne / @OneToOne / @OneToMany
: 컬럼 Join 의미
- 어노테이션을 붙이는 Entity 를 기준으로,
게시물 Entity 에서
Many(게시물) => To => One(사용자) 와 같은 방식으로 사용합니다.
- 컬럼을 Join 시키는 방식이 기존의 방식과 JPA 방식 사이에는 차이가 있습니다.
- 기존 SQL 방식
=> Foreign Key 잡아서 join 해서 가져다 쓰는 방식 ( 정규화 )
dept_id foreign key ...
( 하나의 공유 컬럼으로 두 테이블을 Join 시키는 방식 )
- JPA 방식
=> 가져다 쓸 객체를 통째로 넣어버리는 방식 ( 객체지향적 )
public class Employee {
@ManyToOne // 내가 기준. employee 가 many => to => one ( 부서 )
@JoinColumn(name="dept_id")
private Department department;
}
[ 정규화 - 정규형 위반 ]
- DB 에서는 하나의 컬럼에 하나의 data 만 들어가야 한다.
ex ) 취미 컬럼 : 농구, 수영 ( X ) 하나씩 만
=> 회원의 여러개 취미를 저장하고 싶으면
새로 테이블 만들어서 join 시켜서 가져다 쓰는 식으로 사용합니다.
- JPA 에서는 여러 데이터를 그냥 List 등 객체로 보내버리면 된다는 생각
=> 객체지향적으로 짜기는 짜고
=> 그럼 hibernate 가 자동으로 SQL 식으로 바꿔서 만들어 주는 방식입니다.
[ fetch = FetchType.LAZY ]
// Employee Entity
@ManyToOne
@JoinColumn(name="dept_id")
private Department department;
위와 같이 join 을 걸면
- Employee테이블 left outer join Department테이블
=> Department 가 없는 Employee 까지 전부 조회
=> findById 로 Employee 의 Data 만 조회하더라도
Department 객체 자체가 Join 으로(@ManyToOne) 잡혀있어서
자동으로 department 데이터까지 다 가지고 오게 됩니다. ( 자원낭비! )
=> fetch 설정 필수!
@ManyToOne(fetch = FetchType.LAZY)
Eager : 항상 join하도록 설정 ( Default값 으로 지정되있음 )
LAZY : 부서정보가 필요할 때만 join ( ManyToOne 사용 시 fetch LAZY 필수! )
- Transaction 을 고려하여 사용해야 합니다.
[ Transaction ]
: All or Nothing
=> 전체 성공이 아니면 의미가 없다
즉, 트랜잭션 단위로 묶어서 실행,
=> 성공 시 Commit / 실패 시 RollBack
ex ) 두개의 upate를 트랜잭션으로 묶어서 실행
=> 하나만 성공하면 실패 - 롤백해야 한다.
=> fetch-LAZY 로 잡아두면, Transaction 에서 순서가 제대로 실행되지 않을 수 있습니다.
( insert 와 select 순서. insert 가 되기도 전에 select 시도해버림 )
=> Transaction 을 따로 빼서 순서대로 실행시키는 방식으로
@Test
@Transactional ( spring.org.annotation import / not javax )
=> DML 쿼리가 여러개 동시에 나가는 상황에 트랜잭션 처리 ( 순서 상의 안전 확보 )
[ optimizing ]
- JPA 에서 fetch-LAZY 설정 시,
필요할 때 알아서 join 해서 가져와주고
그냥 각 테이블 각자 조회 하는게 좋다고 판단하면 두 번 쿼리를 날리는 등 최적화한 query 문을 사용합니다.
[ 양방향 Mapping ]
- DB 페러다임에서는 단방향이 기본 개념
사원 => department
- JPA 객체지향 mapping 에서는
사원 <=> deparment
양방향 mapping 을 지원해준다.
public class Employee {
@ManyToOne(fetch=fetchType.LAZY)
private Department department;
}
public class Department {
@OneToMany(mappedBy= "department")
// 양방향 맵핑에서는 상대방 엔티티의 정보를 수정할 수는 없고 단순 조회 기능만 지원
// mappedBy = "상대방 엔티티에서 등록된 객체이름"
private Employee employee;
}
[ ManyToMany 관계 ]
- 상품 <=> 주문 과 같은 관계는 서로 Many To Many 관계가 성립됩니다.
이런 관계에서는 @ManyToMany 는 문제점이 있어 잘 쓰이지 않고,
=> 수동으로 OneToMany 두 개로 풀어서 사용합니다.
상품 === 주문상품(중간테이블) === 주문
상품/주문 각 테이블에서 중간테이블과 OneToMany 걸어서 연결
[ JpaRepository ]
@Repository
public interface MemberRepository extends JpaRepository<MemberEntity, Long> {}
- <엔티티, 엔티티 PK 타입 >
- JpaRepository extends CrudRepository
- CrudRepository 는 부모라 좀 더 추상적, findAll 하면 iterator 로 받음
- JpaRepository는 List로 받음
[ Null 처리 ]
: orElse(null) / orElseThrow()
Optional<P> post = postRepository.findById(postNo);
==
PostEntity postEntity = postRepository.findById(postNo).orElseThrow();
[ Entity <=> DTO ]
public PostResponseDTO insertPost(final RequestPostDTO postDTO) {
final PostEntity postEntity = RequestPostDTO .toEntity(); // DTO로 받아서 Entity 로 만들어서 DB 저장
PostEntity savedEntity = postRepository.save(postEntity);
return new PostResponseDTO(savedEntity); //응답줄 때는 다시 응답용 DTO 로 만들어서 보내주고
}
- @validated 는 RequestDTO 에서 클라이언트 입력값에 진행
- @Entity 는 DB와 매칭하는 용도. 요청/응답 에는 필요한 정보만 담은 DTO를 사용
- Entity <=> DTO 변환하는 메소드, 생성자 만들어서 사용
[ Exception 처리 ]
JpaRepository의 메소드들을 사용할 때
Exception을 Throws 하는 메소드일 경우 ( docs 확인 )
=> 해당 exception을 던질 가능성이 있는 메소드 라는 의미 ( RuntimeException )
=> 해당 메소드가 throws 하는 exception들을 다 던지던가
크게 RuntimeException => try - catch 처리 ( Controller )
[ Jpa Hibernate 키워드 방식 ]
- findBy~ 이후의 변수명, keyword를 인식하고 => 자동으로 쿼리 생성해줌 ( 은행원의 노가다 )
- 간단한 처리할 때 사용
- 좀 복잡해지면 query dsl / 혹은 JPQL / nativeQuery
- 더 복잡해지면 Mybatis 등 사용 ( 동적 쿼리 )
[ JPQL ]
select m form Member m where m.userId=~ ;
- select 별칭 from 엔티티 별칭 where 별칭.필드명
- 별칭을 사용
- 테이블 column 명이 아닌, Java 엔티티 클래스의 필드명을 사용
[ nativeQuery ]
: DB 컬럼명을 사용
@Query(value = "SELECT * FROM jpamember where nickname=:nickname
", nativeQuery = true)
MemberEntity getMember(@Param("nickname")String nickname);
- 객체를 받아 왔을 때
nickname=:#{#oneboard.nickname} 이런식으로 뽑아서 사용
[ @Param(" " ) ]
: SQL 에 들어가는 변수명과 파라미터이름 매칭
select m from MemberEntity as m where m.email=:em;
Member Entity getmember(@Param("em")String email);
[ @Modifying ]
: 수정 / 삭제 쿼리 사용 시 => @Query 메소드에 붙여줘야 함
@Modifying
@Query ( )
=> JPQL 이건 NativeQuery 건 다 필요
[ Query DSL ]
QMember.select().from( ).
이런식의 Java 코드로 쿼리문을 작성하는 것
=> 컴파일에서 오류를 잡아준다는 장점
- Jpa keyword 방식 / JPQL 로는 어려운 동적 쿼리 등 더 복잡한 SQL 문 처리가 필요할 때 사용