85일차 - JPA

Yohan·2024년 6월 26일
0

코딩기록

목록 보기
127/156
post-custom-banner

JPA란?

  • ORM(Object-Relational Mapping) 표준 기술로서, 자바 클래스와 DB 테이블 간의 매핑 정보를 사용하여, SQL Query 없이 데이터를 조작할 수 있도록 도와줌

JPA 기능

  • Repository 인터페이스: 기본 CRUD 연산 메소드를 제공
  • 쿼리 메소드: 메소드 이름을 통해 쿼리를 생성하고 실행
  • 페이징과 정렬: 페이징과 정렬에 대한 복잡한 코드를 단순화
  • 동적 쿼리: 조건에 따라 동적으로 쿼리를 생성

Entity

  • entity 클래스를 먼저 생성하면, 톰캣을 실행할때 자동으로
    Database의 테이블을 엔터티클래스와 동일하게 만들어준다.
  • 카멜케이스로 작성하면, 알아서 db에는 스네이크케이스로 생성됨.
package com.spring.jpastudy.chap01.entity;

import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.*;
import java.time.LocalDateTime;

@Getter @ToString
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder

@Entity
@Table(name= "tbl_product") // 테이블 명 지정 가능
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
    // Oracle = SEQUENCE.       Mariadb, MySql = IDENTITY
    @Column(name= "prod_id")
    private Long id; // PK

    @Column(name= "prod_nm", length = 30, nullable = false) // VARCHAR(30) NOT NULL
    private String name; // 상품명

    @Column(name= "price")
    private int price; // 상품 가격

    @CreationTimestamp // INSERT시에 자동으로 서버시간 저장
    @Column(updatable = false) // 수정 불가
    private LocalDateTime createdAt; // 상품 등록시간

    @UpdateTimestamp // UPDATE문 실행시 자동으로 시간이 저장
    private LocalDateTime updatedAt; // 상품 수정시간

    // 데이터베이스에는 저장 안하고 클래스 내부에서만 사용할 필드
    @Transient
    private String nickName;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING) // EnumType.ORDINAL로 하면 int 타입으로 됨
    private Category category; // 상품 카테고리

    public enum Category {
        FOOD, FASHION, ELECTRONIC
    }

    // 컬럼 기본값 설정 (defualt value)
    @PrePersist
    public void prePersist() {
        if (this.price == 0) {
            this.price = 10000;
        }
        if (this.category == null) {
            this.category = Category.FOOD;
        }
    }
    
}

Repository

  • 메서드 생성 X
    -> 기본적인 CRUD기능은 JPA에서 제공하기 때문에 사용하면됨
    ex) save(S entity) , findById(ID id), findAll(),deleteById(ID id),delete(S entity), count()
package com.spring.jpastudy.chap01.repository;

import com.spring.jpastudy.chap01.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

// JpaRepository를 상속한 후 첫 번째 제너릭엔 Entity 클래스 타입
//                         두 번째 제너릭엔 PK 타입
// CRUD가 이미 구현                          // Entity, Pk의 타입을 명시
public interface ProductRepository extends JpaRepository<Product, Long> {

}

-> 추가적인 쿼리는 직접 작성가능, 예를들어 이름을 통해 user를 조회하고싶다면
List<User> findByName(String name); 사용가능


RepositoryTest

  • 메서드를 생성하지 않았지만 자동으로 SQL문 실행
package com.spring.jpastudy.chap01.repository;

import com.spring.jpastudy.chap01.entity.Product;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

import static com.spring.jpastudy.chap01.entity.Product.Category.*;
import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
@Rollback
class ProductRepositoryTest {

    @Autowired
    ProductRepository productRepository;

    @BeforeEach
    void insertBeforeTest() {

        Product p1 = Product.builder()
                .name("아이폰")
                .category(ELECTRONIC)
                .price(2000000)
                .build();
        Product p2 = Product.builder()
                .name("탕수육")
                .category(FOOD)
                .price(20000)
                .build();
        Product p3 = Product.builder()
                .name("구두")
                .category(FASHION)
                .price(300000)
                .build();
        Product p4 = Product.builder()
                .name("주먹밥")
                .category(FOOD)
                .price(1500)
                .build();

        productRepository.save(p1);
        productRepository.save(p2);
        productRepository.save(p3);
        productRepository.save(p4);

    }


    @Test
    @DisplayName("상품을 데이터베이스에 저장한다")
    void saveTest() {
        //given
        Product product = Product.builder()
                .name("떡볶이")
//                .price(90000)
//                .category(Product.Category.FASHION)
                .build();
        //when
        // insert후 저장된 데이터의 객체를 반환
        Product saved = productRepository.save(product);
        //then
        assertNotNull(saved);
    }


    @Test
    @DisplayName("1번 상품을 삭제한다")
    void deleteTest() {
        //given
        Long id = 1L;
        //when
        productRepository.deleteById(id);
        //then
        Product foundProduct = productRepository.findById(id)
                .orElse(null);

        assertNull(foundProduct);
    }

    @Test
    @DisplayName("3번 상품을 단일조회하면 그 상품명이 구두이다.")
    void findOneTest() {
        //given
        Long id = 3L;
        //when
        Product foundProduct = productRepository.findById(id).orElse(null);
        //then
        assertEquals("구두", foundProduct.getName());
        System.out.println("foundProduct = " + foundProduct);
    }

    @Test
    @DisplayName("상품을 전체조회하면 상품의 총 개수가 4개이다.")
    void findAllTest() {
        //given

        //when
        List<Product> productList = productRepository.findAll();

        //then
        System.out.println("\n\n\n");

        productList.forEach(System.out::println);

        System.out.println("\n\n\n");

        assertEquals(4, productList.size());

    }

    @Test
    @DisplayName("2번 상품의 이름과 카테고리를 수정한다")
    void modifyTest() {
        //given
        Long id = 2L;
        String newName = "청소기";
        Product.Category newCategory = ELECTRONIC;
        //when

        /*
            jpa에서는 수정메서드를 따로 제공하지 않습니다.
            단일 조회를 수행한 후 setter를 통해 값을 변경하고
            다시 save를하면 INSERT대신에 UPDATE문이 나갑니다.
         */
        Product product = productRepository.findById(id).orElse(null);
        product.setName(newName);
        product.setCategory(newCategory);

        Product saved = productRepository.save(product);

        //then
        assertEquals(newName, saved.getName());
    }



}

PK전략 (랜덤문자)

  • @GenericGenerator로 만들고 @GeneratedValue로 사용
  • id의 전략을 uuid로 함으로써 절대 겹치지않는 고유 값으로 설정
    ->ex) 3b160fb6-e4d9-4987-b737-3f853aff7300
  • @GenericGenerator(strategy = "uuid", name = "uid")
    -> 이름은 내 마음대로 설정가능, uid라는 이름으로 uuid 전략 사용
  • @GeneratedValue(generator = "uid")
    -> 기본 키를 자동 생성할 때 uid라는 이름을 사용
package com.spring.jpastudy.chap02.entity;

import lombok.*;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;

@Setter
@Getter
@ToString
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@AllArgsConstructor
@Builder

@Entity
@Table(name = "tbl_student")
public class Student {

    // 랜덤문자로 PK지정
    @Id
    @Column(name = "stu_id")
    @GeneratedValue(generator = "uid")
    @GenericGenerator(strategy = "uuid", name = "uid") // universal unique id
    private String id;

    @Column(name = "stu_name", nullable = false)
    private String name;

    private String city;

    private String major;

쿼리문 커스텀

https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html4

package com.spring.jpastudy.chap02.repository;


public interface StudentRepository extends JpaRepository<Student, String> {

    // 쿼리메서드: 메서드의 이름에 특별한 규칙을 적용하면
    // SQL이 규칙에 맞게 생성됨.

    //  findBy + 필드명, findBy는 규칙(고정)
    List<Student> findByName(String name);

    // 규칙에 맞게 WHERE 조건 추가한 메서드 
    List<Student> findByCityAndMajor(String city, String Major);
    
    // Containing은 '포함'의 의미
    // WHERE major like '%major%'
    List<Student> findByMajorContaining(String Major);

    // WHERE major like 'major%'
    List<Student> findByMajorStartingWith(String Major);

    // WHERE major like '%major'
    List<Student> findByMajorEndingWith(String Major);
    
    // WHERE age <= ?
    List<Student> findByAgeLessThanEqual(int age);
}

test

   @Test
    @DisplayName("이름이 춘식이인 학생의 모든 정보를 조회한다")
    void findByNameTest() {
        //given
        String name = "춘식이";
        //when
        List<Student> students = studentRepository.findByName(name);
        //then
        assertEquals(1, students.size());

        System.out.println("students = " + students);
    }
    
        @Test
    @DisplayName("도시 이름과 전공으로 학생을 조회")
    void findByCityAndMajorTest() {
        //given
        String city = "제주도";
        String major = "화학공학";
        //when
        List<Student> foundStu = studentRepository.findByCityAndMajor(city, major);
        //then
        System.out.println("foundStu = " + foundStu);
    }


    @Test
    @DisplayName("전공이 공학으로 끝나는 학생들 조회")
    void findByMajorContainingTest() {
        //given
        String majorContaining = "공학";
        //when
        List<Student> students = studentRepository.findByMajorContaining(majorContaining);
        //then
        students.forEach(System.out::println);
    }

native Sql (순수 SQL 작문)

  • repository
 // 순수한 SQL
    // native sql 사용하기 (메서드 명은 막 지어도 됨. 규칙에 해당되지 않음)
    //                              파라미터 작명 내맘대로. @Param과 통일만 시켜준다  nativeQuery값은 필수
    @Query(value = "SELECT * FROM tbl_student WHERE stu_name = :snm OR city = :city", nativeQuery = true)
    List<Student> getStudentByNameOrCity(@Param("snm") String name, @Param("city") String city);

    
    // 다른 방법. 파라미터가 ?1 부터 자동으로 바인딩 된다.
    @Query(value = "SELECT * FROM tbl_student WHERE stu_name = ?1 OR city = ?2", nativeQuery = true)
    List<Student> getStudentByNameOrCity2(String name, String city);
  • test
    @Test
    @DisplayName("도시 또는 이름으로 학생을 조회")
    void nativeSQLTest() {
        //given
        String name = "춘식이";
        String city = "제주도";
        //when
        List<Student> student = studentRepository.getStudentByNameOrCity(name, city);
        //then
        student.forEach(System.out::println);
    }
profile
백엔드 개발자
post-custom-banner

0개의 댓글