๐ ํ๋์ ๋๋ฉ์ธ์ ์ฌ๋ฌ๊ฐ์ง ์ํฐํฐ๋ค์ด ๋ชจ์ฌ์๋ค (ex: ์ํ, ์นดํ
๊ณ ๋ฆฌ, ์ ์กฐ์ฌ ๋ฑ)
๐ ํ
์ด๋ธ ์ค๊ณํ๋๊ฒ์ฒ๋ผ ์ค๊ณํ๋ฉด ๋จ.
๐ ๋ชจ๋ ์ํฐํฐ๋ ์๋ณ์๊ฐ ์์ด์ผํ๋ค. (pk๊ฐ ์์ผ๋ฉด ์ํฐํฐ)

๐ ๊ฐ๋จํ ํ์ผ ๊ตฌ์กฐ
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๊ฐ์ง ํ์
์ ์ฌ์ฉํ๋ฏ๋ก ๋ณ๋์ ํด๋์ค๋ก ์์ฑํด๋์๋ค.
package org.zerock.apidemo.domain;
public enum ProductStatus { // status ๊ด๋ จ ํ์
SALE, NOT_SALE, RESERVED; // db์์ ์ถ๋ ฅ์์ SALE, NOT_SALE๋ก ๋จ๋๊ฒ ์๋๋ผ 0,1,2 ์ ์๋ฒ๋๋ก ๋ฌ๋ค.
}
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๋ก ์ฌ๋ฌ ์ํฐํฐ์์ ๋ ์ง๊ด๋ จ ์ฝ๋๋ฅผ ์ฌ์ฉํ๋ฏ๋ก ๋ณ๋์ ํด๋์ค๋ก ๋นผ์ ์ฌ์ฉํ๋๋ก ๋ง๋ค์๋ค.
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๊ฐ์ ์ฐธ๊ณ ํ์ฌ ์์ฑ๋๋ค.
๐ JPA๋ Entity๋ฅผ ๋ฉ๋ชจ๋ฆฌ์์ ๊ด๋ฆฌํ๊ณ , ์ด ์ํ๋ฅผ ์์์ฑ ์ปจํ ์คํธ(Persistence Context)๋ผ๋ ์์ญ์ ์ ์ฅํ๋ค. ์ด๋ ์ํฐํฐ์ ์์ฑ์ด ๋ณ๊ฒฝ๋๋ฉด JPA๊ฐ ์ด๋ค ๋ถ๋ถ์ด ๋ณ๊ฒฝ์ด ๋์๋์ง ํ์ธํ์ฌ DB์ ๊ฐ๋๋ก ๋๊ธฐํ์ํจ๋ค.
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());
}
}
@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์ด ์ถ๊ฐ๋์ด์ผํ๋ค.
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๋ค์ ์ถ๋ ฅํ๋๋ก ๋ง๋ค๊ธฐ ์ํด์ ์ด ์ฒ๋ผ ์์ฑํ๊ณ
@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์ ๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์ถ๋ ฅํ๋ค.
๐ 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์๋ ์ธ๋ฑ์ค๋ฅผ ์ด๋ป๊ฒ ํ๊ณ ๊ฐ์ง ์ ํด์ค๋ค.
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");
}
}