RDBMS와 객체지향 간의 중간에서 2개를 매핑하는 역할을
ORM
이라고 한다.
JPA
는 이런 ORM의 표준 명세로 자바에서 제공하는API
이다.
JPA
는 인터페이스이고, 이를 구현한 구현체가 대표적으로Hibernate
등이 있다.
1. 특정 DB에 종속되지 않음
- 설정 파일에서 사용할 DB 변경 가능
2. 객체지향 프로그래밍
3. 생산성 향상
- DB에 새로운 칼럼 추가 시, DTO 클래스의 필드도 모두 변경해야 함.
- JPA는 매핑 클래스에 필드만 추가하면 끝!
( DAO : DB의 data에 접근하기 위한 객체 )
( DTO : 계층 간 데이터 교환을 하기 위한 객체 )
1. 복잡한 쿼리 처리
- 복잡하면 SQL 사용 권장
- 이를 보완한 JPQL이 존재
2. 성능 저하 위험
3. 학습 시간
메소드 | 내용 |
---|---|
find() | 영속성 컨텍스트에 Entity 검색 후, 없으면 DB에서 해당 데이터 찾아 영속성 컨텍스트에 저장 |
persist() | Entity를 영속성 컨텍스트에 저장 |
remove() | Entity 클래스를 영속성 컨텍스트에서 삭제 |
flush() | 영속성 컨텍스트에 저장된 내용을 DB에 반영 |
생명주기 | 내용 |
---|---|
new | PC와 관련 없는 상태 |
managed | Entity가 PC에 저장되어 관리되는 상태 / 커밋 시점에 DB 반영 |
detached | PC에 저장되었다가 분리된 상태 |
removed | PC와 DB에서 삭제된 상태 |
Item item = new Item(); // PC와 관련없음
item.setItemNm("테스트 상품");
EntityManager em = entityManagerFactory.createEntityManager();
// EntityManager 생성
EntityTransaction transaction = em.getTransaction();
// 데이터 변경 시, 무결성 위해 트랜잭션 필수!
em.persist(item);
// PC에 저장된 상태. 아직 SQL X
transaction.commit(); // 트랜잭션을 DB에 반영
em.close(); // 자원 반환
emf.close();
스냅샷
을 가짐.UPDATE
를 쓰기 지연 저장소
에 보관함.다음 챕터에선 본격적으로 프로젝트를 만들어보자
server.port = 80
#MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shop?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1254mlm
#실행되는 쿼리 콘솔 출력
spring.jpa.properties.hibernate.shop_sql=true
#콘솔창에 출력되는 쿼리를 가독성이 좋게 포맷팅
spring.jpa.properties.hibernate.format_sql=true
#쿼리에 물음표로 출력되는 바인드 파라미터 출력
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.hibernate.ddl-auto=create // DB 초기화 전략
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect // SQL 방언 설정
SQL 방언
SQL은 표존 SQL과 DBMS 벤더에서 제공하는 SQL이 존재.
각 공급업체에서 만든 SQL을 방언(Dialect)이라고 함.
옵션 | 내용 |
---|---|
none | 사용하지 않음 |
create | 기존 테이블 삭제 후 테이블 생성 |
create-drop | 기존 테이블 삭제 후 테이블 생성. 종료 시점에 테이블 삭제 |
update | 변경된 스키마 적요 |
validate | 엔티티와 테이블 정상 매핑 확인 |
스테이징 환경이란?
운영 환경
은 실제 서비스를 운영하는 환경.
운영환경과 거의 동일한 환경 구성하여, 배포하기 전 여러 가지 기능을 검증하는 환경
엔티티를 만들기 위해 테이블에 어떤 데이터가 저장될지 설계해야함
그전에 엔티티 매핑 관련 어노테이션을 알아보자
@Lob에 대해
DDL이란?
테이블, 스키마, 인덱스, 뷰, 도메인을 정의, 변경, 제거할 때 사용하는 용어.
( CREATE, DROP 등이 이에 해당 )
https://cbw1030.tistory.com/71
생성 전략 | 내용 |
---|---|
GenerationType.AUTO (default) | JPA 구현체가 자동으로 생성 전략 결정 |
GenerationType.IDENTITY | 기본키 생성을 DB에 위임 (MySQL은 AUTO_INCREMENT로 기본키 생성) |
GenerationType.SEQUENCE | DB 시퀀스 오브젝트를 이용해 기본키 생성. @SqeuenceGenerator로 시퀀스 등록 필요 |
GenerationType.TABLE | 키 생성요 테이블 사용. @TableGenerator 필요 |
기본키와 DB 시퀀스 오브젝트의 의미
기본키 : DB에서 조회시 구분할 수 있는 유일한 기준
시퀀스 : 순차적으로 증가하는 값을 반환해주는 DB 객체
@Entity
@Table(name="item")
@Getter
@Setter
@ToString
public class Item {
@Id
@Column(name="item_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id; // 상품 코드
@Column(nullable = false, length = 50)
private String itemNm; // 상품명
@Column(name="price", nullable = false)
private int price; // 가격
@Column(nullable = false)
private int stockNumber; // 재고 수량
@Lob // BLOB, CLOB 타입 매핑
@Column(nullable = false)
private String itemDetail; // 상품 상세 설명
@Enumerated(EnumType.STRING) // enum 타입 매핑
private ItemSellStatus itemSellStatus; // 상품 판매 상태 (enum 타입 클래스)
private LocalDateTime regTime; // 등록 시간
private LocalDateTime updateTime; // 수정 시간
}
EnumType.ORDINAL : enum 순서 값을 DB에 저장 EnumType.STRING : enum 이름을 DB에 저장 |
---|
enum ItemSellStatus {
SELL, SOLDOUT
}
public interface ItemRepository extends JpaRepository<Item, Long> { }
메소드 | 기능 |
---|---|
save(S entity) | 엔티티 저장 및 수정 |
void delete(T entity) | 엔티티 삭제 |
count() | 엔티티 총 개수 반환 |
Iterable | 모든 엔티티 조회 |
h2DBMS를 이용해서 테스트 코드를 작성해보자.
테스트 코드용 프로퍼티 파일을 만들어 준다.
# Datasource 설정
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.username=sa
spring.datasource.password=
# H2 DB 방언 설정
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
직접적인 테스트 코드를 만든다.
@SpringBootTest // 실제 애플리케이션 구동 할 때처럼 모든 빈을 IoC 컨테이너에 등록
@TestPropertySource(locations="classpath:application-test.properties") // 기본 프로퍼티보다 우선적으로 설정하게 함
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
@Test
@DisplayName("상품 저장 테스트")
public void createItemTest(){
Item item = new Item();
item.setItemNm("테스트 상품");
item.setPrice(10000);
item.setItemDetail("테스트 상품 상세 설명");
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100);
item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
Item savedItem = itemRepository.save(item);
System.out.println(savedItem.toString());
}
- Spring Data JPA는 인터페이스만 작성하면 런타임 시점에 자바의 Dynamic Proxy를 이용해서 객체를 동적으로 생성해줌.
Repository 인터페이스에 간단한 네이밍 룰을 이용해 메서드를 작성해서 원하는 쿼리를 실행할 수 있다
[ 규칙 ]
find + (엔티티 이름) + By + 변수이름
public interface ItemRepository extends JpaRepository<Item, Long> {
List<Item> findByItemNm(String itemNm);
// 아이템 리스트 만드는 메서드
public void createItemList(){
for(int i=1;i<=10;i++){
Item item = new Item();
item.setItemNm("테스트 상품" + i);
item.setPrice(10000 + i);
item.setItemDetail("테스트 상품 상세 설명" + i);
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100); item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
Item savedItem = itemRepository.save(item);
}
}
@Test
@DisplayName("상품명 조회 테스트")
public void findByItemNmTest(){
this.createItemList(); // 1. 아이템들 생성
List<Item> itemList = itemRepository.findByItemNm("테스트 상품1"); // ItemNm이 테스트 상품1인 아이템을 받기
for(Item item : itemList){
System.out.println(item.toString()); // 해당 리스트 출력
}
}
Hibernate:
select
item0_.item_id as item_id1_0_,
item0_.item_detail as item_det2_0_,
item0_.item_nm as item_nm3_0_,
item0_.item_sell_status as item_sel4_0_,
item0_.price as price5_0_,
item0_.rec_time as reg_time6_0_,
item0_.stock_number as stock_nu7_0_,
item0_.update_time as update_t8_0_,
from
item item0_
where
item0_.item_nm=?
binding parameter [1] as [VARCHAR] - [테스트 상품1]
// 인터페이스
List<Item> findByItemNmOrItemDetail(String itemNm, String itemDetail);
// 테스트 코드
@Test
@DisplayName("상품명, 상품상세설명 or 테스트")
public void findByItemNmOrItemDetailTest(){
this.createItemList();
List<Item> itemList = itemRepository.findByItemNmOrItemDetail("테스트 상품1", "테스트 상품 상세 설명5");
for(Item item : itemList){
System.out.println(item.toString());
}
}
// 인터페이스
List<Item> findByPriceLessThan(Integer price);
// 테스트 코드
@Test
@DisplayName("가격 LessThan 테스트")
public void findByPriceLessThanTest(){
this.createItemList();
List<Item> itemList = itemRepository.findByPriceLessThan(10005);
for(Item item : itemList){
System.out.println(item.toString());
}
}
오름차순 정렬
: OrderBy + 속성명 + Asc` 를 이용하면 된다!
내림차순 정렬
: OrderBy + 속성명 + Desc` 를 이용하면 된다!// 인터페이스
List<Item> findByPriceLessThanOrderByPriceDesc(Integer price); // 내림차순
// 테스트 코드
@Test
@DisplayName("가격 내림차순 조회 테스트")
public void findByPriceLessThanOrderByPriceDesc(){
this.createItemList();
List<Item> itemList = itemRepository.findByPriceLessThanOrderByPriceDesc(10005);
for(Item item : itemList){
System.out.println(item.toString());
}
}
@Query
를 지원한다.JPQL
을 이용해서 작성해야한다.JPQL
@Query("select i from Item i where i.itemDetail like " + // like 다음에 띄어쓰기 해야됨
"%:itemDetail% order by i.price desc")
List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);
@Test
@DisplayName("@Query를 이용한 상품 조회 테스트")
public void findByItemDetailTest(){
this.createItemList();
List<Item> itemList = itemRepository.findByItemDetail("테스트 상품 상세 설명");
for(Item item : itemList){
System.out.println(item.toString());
}
}
@Query
의 nativeQuery
속성을 사용.
@Query("select i from Item i where i.itemDetail like " +
"%:itemDetail% order by i.price desc", nativeQuery = true)
List<Item> findByItemDetail(@Param("itemDetail") String itemDetail);
@Test
@DisplayName("nativeQuery를 이용한 상품 조회 테스트")
public void findByItemDetailTest(){
this.createItemList();
List<Item> itemList = itemRepository.findByItemDetail("테스트 상품 상세 설명");
for(Item item : itemList){
System.out.println(item.toString());
}
}
@Query
는 JPQL 문법을 문자열로 입력하기 때문에, 컴파일 시점에 에러가 발견되지 않는다.Querydsl
를 알아보자.@Query("select i from Item wheere ...)
위 코드의 JPQL의 문법 중 where를 wheere로 잘못 입력되었다.
이때 Querydsl이 오타 발생시 바로 알려주고, 동적으로 쿼리를 생성해준다!
@PersistenceContext
EntityManager em;
@Test
@DisplayName("Querydsl 조회 테스트1")
public void queryDslTest(){
this.createItemList();
JPAQueryFactory queryFactory = new JPAQueryFactory(em); // 쿼리를 동적으로 생성
QItem qItem = QItem.item; // 쿼리 생성위해 자동생성된 QItem 객체 사용
JPAQuery<Item> query = queryFactory.selectFrom(qItem)
.where(qItem.itemSellStatus.eq(ItemSellStatus.SELL))
.where(qItem.itemDetail.like("%" + "테스트 상품 상세 설명" + "%"))
.orderBy(qItem.price.desc());
List<Item> itemList = query.fetch(); // 쿼리 결과를 리스트로 반환, fetch(0 시점에 쿼리문 실행
for(Item item : itemList){
System.out.println(item.toString());
}
}
@PersistenceContext
: 영속컨텍스트 사용하기 위해 EntityManager 빈을 주입QItem
: QueryDSL전용 엔티티 (간단한 쿼리는 (Spring Data)JPA를 사용하고, 동적쿼리는 QueryDSL을 사용하여 개발하곤 합니다.)메소드 | 내용 |
---|---|
List fetch() | 조회 결과 리스트 반환 |
T fetchOne | 조회 대상이 1건인 경우 제네릭으로 지정한 타입 반환 |
T fetchFirst() | 조회 대상 중 1건만 반환 |
Long fatchCount() | 조회 대상 개수 반환 |
QueryResult fetchResults() | 조회한 리스트와 전체 개수를 포함한 QueryResults 반환 |
Hibernate:
select
item0_.item_id as item_id1_0_,
item0_.item_detail as item_det2_0_,
item0_.item_nm as item_nm3_0_,
item0_.item_sell_status as item_sel4_0_,
item0_.price as price5_0_,
item0_.rec_time as reg_time6_0_,
item0_.stock_number as stock_nu7_0_,
item0_.update_time as update_t8_0_,
from
item item0_
where
item0_.item_sell_status=?
and(
item-_.item_detail like ? escape '!'
)
order by
item0_.price desc
QuerydslPredicateExecutor를 이용한 상품 조회를 해보자
Predicate란?
'이 조건이 맞다'고 판단하는 근거를 함수로 제공하는 것
public interface ItemRepository extends JpaRepository<Item, Long>,
QuerydslPredicateExecutor<Item> {
메소드 | 내용 |
---|---|
long count(Predicate) | 조건에 맞는 데이터 총 개수 반환 |
boolean exists(Predicate) | 조건에 맞는 데이터 존재 여부 반환 |
Iterator findAll(Predicate) | 조건에 맞는 모든 데이터 반환 |
Page findAll(Predicate, Pageable) | 조건에 맞는 페이지 데이터 반환 |
Iterator findAll(Predicate, Sort) | 조건에 맞는 정렬된 데이터 반환 |
T findOne(Predicate) | 조건에 맞는 데이터 1개 반환 |
public void createItemList2(){
for(int i=1;i<=5;i++){
Item item = new Item();
item.setItemNm("테스트 상품"+i);
item.setPrice(10000 + i);
item.setItemDetail("테스트 상품 상세 설명"+i);
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100);
item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
itemRepository.save(item);
}
for(int i=6;i<=10;i++){
Item item = new Item();
item.setItemNm("테스트 상품"+i);
item.setPrice(10000 + i);
item.setItemDetail("테스트 상품 상세 설명"+i);
item.setItemSellStatus(ItemSellStatus.SOLD_OUT);
item.setStockNumber(0);
item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
itemRepository.save(item);
}
}
@Test
@DisplayName("상품 Querydsl 조회 테스트2")
public void queryDslTest2(){
this.createItemList2();
BooleanBuilder booleanBuilder = new BooleanBuilder(); // 2: 쿼리에 들어갈 조건을 만들어주는 빌더
QItem item = QItem.item;
String itemDetail = "테스트 상품 상세 설명";
int price = 10003;
String itemSellStat = "SELL";
booleanBuilder.and(item.itemDetail.like("%" + itemDetail + "%")); // 3: 조회 시 조건
booleanBuilder.and(item.price.gt(price));
if(StringUtils.equals(itemSellStat, ItemSellStatus.SELL)){
booleanBuilder.and(item.itemSellStatus.eq(ItemSellStatus.SELL));
}
Pageable pageable = PageRequest.of(0, 5); // 4: 데이터를 페이징해 조회하도록 Pageble 객체 생성
Page<Item> itemPagingResult =
itemRepository.findAll(booleanBuilder, pageable); // 5: 조건에 맞는 데이터를 Page 객체로 받아옴.
System.out.println("total elements : " +
itemPagingResult. getTotalElements());
List<Item> resultItemList = itemPagingResult.getContent();
for(Item resultItem: resultItemList){
System.out.println(resultItem.toString());
}
}
createItemList2
: 1~10까진 SELL, 11~20까진 SOLDOUTBooleanBuilder
: 동적으로 쿼리를 생성해주면서 조건까지 넣을 꺼임.and
: 조회 시 사용할 조건들을 설정Pageable
: 페이지를 어떻게 보여줄지 정보를 전달하는 APIPageRequest
: 몇 페이지, 한 페이지의 사이즈, Sorting 방법(Option)을 가지고, Repository에 Paging을 요청할 때 사용하는 것Hibernate:
select
item0_.item_id as item_id1_0_,
item0_.item_detail as item_det2_0_,
item0_.item_nm as item_nm3_0_,
item0_.item_sell_status as item_sel4_0_,
item0_.price as price5_0_,
item0_.rec_time as reg_time6_0_,
item0_.stock_number as stock_nu7_0_,
item0_.update_time as update_t8_0_,
from
item item0_
where
(
item0_.item_detail like ? escape '!'
)
and item0_.price>?
and item0_.item_sell_status=? limit ?