Back to the JPA

[ JPA ] JPA <=> DB , Entity / Repository

Backcoder 2023. 1. 29. 14:10

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 form Member 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 문 처리가 필요할 때 사용