JPA

์ด๋™์–ธยท2024๋…„ 10์›” 9์ผ

new world

๋ชฉ๋ก ๋ณด๊ธฐ
52/62
post-thumbnail

10.08 (ํ™”)

1. domain

๐Ÿ‘‰ ํ•˜๋‚˜์˜ ๋„๋ฉ”์ธ์— ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์—”ํ‹ฐํ‹ฐ๋“ค์ด ๋ชจ์—ฌ์žˆ๋‹ค (ex: ์ƒํ’ˆ, ์นดํ…Œ๊ณ ๋ฆฌ, ์ œ์กฐ์‚ฌ ๋“ฑ)
๐Ÿ‘‰ ํ…Œ์ด๋ธ” ์„ค๊ณ„ํ•˜๋Š”๊ฒƒ์ฒ˜๋Ÿผ ์„ค๊ณ„ํ•˜๋ฉด ๋จ.
๐Ÿ‘‰ ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ๋Š” ์‹๋ณ„์ž๊ฐ€ ์žˆ์–ด์•ผํ•œ๋‹ค. (pk๊ฐ€ ์žˆ์œผ๋ฉด ์—”ํ‹ฐํ‹ฐ)


๐Ÿ‘‰ ๊ฐ„๋‹จํ•œ ํŒŒ์ผ ๊ตฌ์กฐ

1-1. ProductEntity

package org.zerock.apidemo.domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "tbl_product_ex")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProductEntity extends BaseEntity {

    @Id // ํ•ด๋‹น jpa์—์„œ pk๊ฐ’์€ pno
    @GeneratedValue(strategy = GenerationType.IDENTITY) // autoIncrement๋กœ ์ž๋™ ์ƒ์„ฑ๋˜๋Š” pk
    private Long pno;

    @Column(nullable = false, length = 200) // not null, 200์ž
    private String pname;

    @Lob // ๊ธธ์ด๊ฐ€ ๊ธด ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•˜๊ณ ์‹ถ์„๋•Œ
    private String pdesc;

    private int price;

    @Builder.Default // Defalut ์˜ต์…˜
    private ProductStatus status =  ProductStatus.SALE;


    private boolean delFlag;


}

๐Ÿ‘‰ productEntity์—์„œ๋Š” db๋ฅผ ๋งŒ๋“œ๋Š” ๊ธฐ๋ณธ base๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด๋œ๋‹ค. table์ด pk๊ฐ€ ์žˆ๋“ฏ์ด pno๋ฅผ autoIncrement๋ฅผ ํ†ตํ•ด pk๋กœ ์ƒ์„ฑ ํ•ด์ฃผ๊ณ , status ๊ฐ™์€ ๊ฒฝ์šฐ์—๋Š” ProductStatus์—์„œ SALE, NOT_SALE๋“ฑ 3๊ฐ€์ง€ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋ณ„๋„์˜ ํด๋ž˜์Šค๋กœ ์ƒ์„ฑํ•ด๋‘์—ˆ๋‹ค.



1-2. ProductStatus

package org.zerock.apidemo.domain;

public enum ProductStatus { // status ๊ด€๋ จ ํƒ€์ž…

    SALE, NOT_SALE, RESERVED; // db์—์„œ ์ถœ๋ ฅ์‹œ์— SALE, NOT_SALE๋กœ ๋œจ๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ 0,1,2 ์˜ ์ˆœ๋ฒˆ๋Œ€๋กœ ๋œฌ๋‹ค.
}




1-3. BaseEntity

package org.zerock.apidemo.domain;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // ๋‚ ์งœ db์— ์ถœ๋ ฅ
public abstract class BaseEntity { // ์ž๋™์œผ๋กœ ๊ฐฑ์‹  ๋˜๋„๋ก ๋‚ ์งœ ํด๋ž˜์Šค

    @CreatedDate
    private LocalDateTime createDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

๐Ÿ‘‰ ํ•ด๋‹น Entity๋Š” ๋‚ ์งœ๊ด€๋ จ Entity๋กœ ์—ฌ๋Ÿฌ ์—”ํ‹ฐํ‹ฐ์—์„œ ๋‚ ์งœ๊ด€๋ จ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ๋ณ„๋„์˜ ํด๋ž˜์Šค๋กœ ๋นผ์„œ ์‚ฌ์šฉํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค.




1-4. repository

package org.zerock.apidemo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.zerock.apidemo.domain.ProductEntity;

public interface ProductRepository extends JpaRepository<ProductEntity, Long> { // id์˜ ๋ฐ์ดํ„ฐํƒ€์ž…์„ ๋งž์ถฐ์„œ


}

๐Ÿ‘‰ ํ•ด๋‹น repository๋Š” JPA๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ DB์— ์‰ฝ๊ฒŒ ์ ‘๊ทผํ• ์ˆ˜์žˆ๋„๋ก ์—ฐ๊ฒฐ๊ณ ๋ฆฌ ๊ฐ™์€ ๊ฒƒ์ธ๋ฐ, ์šฐ์„  JpaRepository๋ฅผ extendsํ•˜์—ฌ ๋‚ด๋ถ€์— ์‚ฌ์šฉ๊ฐ€๋Šฅํ•œ CRUD, Paging๊ธฐ๋Šฅ๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ‘‰ ProductEntity์˜ pk๊ฐ’์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ž‘์„ฑ๋œ๋‹ค.




2. Dirty checking

๐Ÿ‘‰ JPA๋Š” Entity๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๊ด€๋ฆฌํ•˜๊ณ , ์ด ์ƒํƒœ๋ฅผ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ(Persistence Context)๋ผ๋Š” ์˜์—ญ์— ์ €์žฅํ•œ๋‹ค. ์ด๋•Œ ์—”ํ‹ฐํ‹ฐ์˜ ์†์„ฑ์ด ๋ณ€๊ฒฝ๋˜๋ฉด JPA๊ฐ€ ์–ด๋–ค ๋ถ€๋ถ„์ด ๋ณ€๊ฒฝ์ด ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์—ฌ DB์™€ ๊ฐ™๋„๋ก ๋™๊ธฐํ™”์‹œํ‚จ๋‹ค.




3. testCode

package org.zerock.apidemo.repository;

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import org.zerock.apidemo.domain.ProductEntity;
import org.zerock.apidemo.domain.ProductStatus;

import java.util.Optional;
import java.util.stream.IntStream;

@DataJpaTest
@Log4j2
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class ProductTests {

    @Autowired
    ProductRepository repo;

    @Test
    @Transactional
    @Commit
    public void insertDummies() {

        IntStream.rangeClosed(1,100).forEach(i -> { // 100๊ฐœ์˜ ๋”๋ฏธ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
            ProductEntity entity = ProductEntity.builder()
                    .pname("Product" + i)
                    .pdesc("Product desc " + i)
                    .price(i)
                    .build();
            repo.save(entity);
        });
    }

    @Test
    @Transactional
    public void testRead() {
        Long id = 50L;

        Optional<ProductEntity> result = repo.findById(id);

        ProductEntity productEntity = result.orElse(null);

        log.info(productEntity);

    }

    @Test
    @Transactional
    @Commit
    public void testUpdate() {
        Long id = 50L;

        Optional<ProductEntity> result = repo.findById(id);

        ProductEntity productEntity = result.orElse(null);

        productEntity.changeStatus(ProductStatus.NOT_SALE);

    }

    @Test
    @Transactional
    @Commit
    public void insertOne() {

            ProductEntity entity = ProductEntity.builder()
                    .pname("Test Product")
                    .pdesc("Test Product desc ")
                    .price(3000)
                    .build();
            repo.save(entity);

            log.info(entity.getPno());
    }

}




4. Dirty checking ํ™•์ธ

@Test
    @Transactional
    @Commit
    public void testUpdate() {
        Long id = 50L;

        Optional<ProductEntity> result = repo.findById(id);

        ProductEntity productEntity = result.orElse(null);

        productEntity.changeStatus(ProductStatus.NOT_SALE);

    }

๐Ÿ‘‰ ํ•ด๋‹น์ฝ”๋“œ๋ฅผ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ถ„์„ํ•ด๋ณด์ž๋ฉด, id๊ฐ€ 50๋ฒˆ์ธ ๋ฐ์ดํ„ฐ ๊ฐ’์˜ ์ƒํƒœ๋ฅผ Not_SALE๋กœ ๋ณ€๊ฒฝํ•˜๋Š”๊ฒƒ์ธ๋ฐ ์ด๊ณณ์—์„œ ์ค‘์š”ํ•œ์ ์ด ๋‘๊ฐ€์ง€๊ฐ€์žˆ๋‹ค. ์šฐ์„  Hibernate์—์„œ db๊ฐ’์ด ๊ฐ™์€์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด select๋ฌธ์„ ์‚ฌ์šฉํ•˜๊ณ , ์ดํ›„์— ๊ฐ’์ด ๋‹ค๋ฅด๋‹ค๋ฉด update๋ฅผ ํ†ตํ•ด์„œ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•ด์ฃผ๋Š” ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

๐Ÿ‘‰ ๋งŒ์•ฝ update๊ฐ€ ํ•„์š”๊ฐ€ ์—†๋‹ค๋ฉด ๊ธฐ์กด์˜ select๋ฌธ๋งŒ ์‹คํ–‰ํ•˜๊ฒŒ ๋˜๊ณ  ํ•„์š”ํ•˜๋‹ค๋ฉด ์ž๋™์œผ๋กœ update๋ฌธ๊นŒ์ง€ ์‹คํ–‰๋˜๋Š”๊ฒƒ์ด ์ค‘์š”ํ•œ์ ์ด๋‹ค. ๋‘๋ฒˆ์งธ๋กœ ํ•œ๋ฒˆ์— select์™€ update๊ฐ€ ์‹คํ–‰๋˜์–ด์•ผํ•˜๋ฏ€๋กœ @transactional์ด ๋ฌด์กฐ๊ฑด๋“ค์–ด๊ฐ€์ค˜์•ผํ•˜๊ณ , update๋œ ๋‚ด์šฉ์ด db์— ๋ฐ˜์˜๋˜์–ด์•ผํ•˜๋ฏ€๋กœ @Commit์ด ์ถ”๊ฐ€๋˜์–ด์•ผํ•œ๋‹ค.




5. paging

5-1. repository

package org.zerock.apidemo.repository;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.zerock.apidemo.domain.ProductEntity;

import java.util.List;

public interface ProductRepository extends JpaRepository<ProductEntity, Long> { // id์˜ ๋ฐ์ดํ„ฐํƒ€์ž…์„ ๋งž์ถฐ์„œ

    @Query("select p from ProductEntity p where p.pname like %:keyword% ")
    Page<ProductEntity> getByPname(@Param("keyword")String keyword, Pageable pageable);
}

๐Ÿ‘‰ reposiotry์—์„œ @Query๋ฅผ ์ด์šฉํ•˜์—ฌ ํ•ด๋‹นํ‚ค์›Œ๋“œ๊ฐ€ ๋“ค์–ด๊ฐ„ pname๋“ค์„ ์ถœ๋ ฅํ•˜๋„๋ก ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ ์ด ์ฒ˜๋Ÿผ ์ž‘์„ฑํ•˜๊ณ 

5-2. testCode

    @Test
    @Transactional
    public void testLike() {

        int page = 0;
        int size = 20;

        Pageable pageable = PageRequest.of(page,size, Sort.by("pno").descending());

        String keyword = "1";

       Page<ProductEntity> result = repo.getByPname(keyword, pageable);

       result.get().forEach( productEntity -> log.info(productEntity.getPno())) ;



    }

๐Ÿ‘‰ page,size๋ฅผ ์ •ํ•ด๋†“๊ณ , pageable์„ of๋ฅผ ํ†ตํ•ด ๋งŒ๋“ค์–ด๋†“๋Š”๋‹ค. ์ดํ›„์— ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋“ค์–ด๊ฐˆ keyword๋ฅผ ์ž‘์„ฑํ•˜์—ฌ repository์˜ ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ถœ๋ ฅํ•œ๋‹ค.




6. index์ƒ์„ฑ

๐Ÿ‘‰ ProductEntity table์ƒ์„ฑ์‹œ์— index๋ฅผ ์ •ํ•ด์ฃผ๊ณ ์‹ถ๋‹ค๋ฉด

@Table(name = "tbl_product_ex", indexes = {
        @Index(name = "idx_product_sale", columnList = "status, pno") // ์ƒํƒœ์™€ pno๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ•˜๋Š” ์ธ๋ฑ์Šค ์ƒ์„ฑ, ํ•˜์ง€๋งŒ order by ๋ฅผ ์‚ฌ์šฉ๋ถˆ๊ฐ€
})

๐Ÿ‘‰ ์ด ์ฒ˜๋Ÿผ index๋ฅผ ์ •ํ•ด์ค€๋‹ค.


    @Query("select p from ProductEntity p where p.pname like %:keyword% and p.status = 0 ")
    Page<ProductEntity> getByPname(@Param("keyword")String keyword, Pageable pageable);

๐Ÿ‘‰ repository์—๋„ ์ธ๋ฑ์Šค๋ฅผ ์–ด๋–ป๊ฒŒ ํƒ€๊ณ ๊ฐˆ์ง€ ์ •ํ•ด์ค€๋‹ค.




7. CORS

package org.zerock.apidemo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebAPIConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("HEAD","GET","POST","PUT","DELETE","OPTIONS")
                .maxAge(300)
                .allowedHeaders("Authorization", "Cache-Control","Content-Type");
    }
}

0๊ฐœ์˜ ๋Œ“๊ธ€