본문 바로가기
기초 및 언어/▶ Spring

15. Spring Boot JPA + 쿼리 어노테이션(jpql)

by 류딩이 2025. 10. 1.

🌈Spring Boot JPA + Lombok 엔티티 매핑 실습

(자동 테이블 생성, CRUD)


🌈1. DB 설정 + JPA 설정(application.properties)

 

 

 application.properties 에 작성

// DB연결 설정
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
spring.datasource.username=sqlid
spring.datasource.password=sqlpw
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

 

🔽 실행시 테이블 새로 자동생성

// JPA 테이블 생성
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

 

🔽 테이블을 시작할때마다 재생성을 안하고 업데이트 하기위함

➡️ 재생성을 안할시 create를 지우고 update로 수정

spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

 

JPA 설정

  • ddl-auto=create → 실행 시 테이블을 새로 자동 생성 (데이터 초기화됨)
  • show_sql=true → 실행되는 SQL 문장을 콘솔에서 확인 가능
  • format_sql=true → SQL 문장을 보기 좋게 줄바꿈/들여쓰기 출력

🌈2. 파일 경로 및 설정

 


 

🌈3. 클래스 설정 

Lombok 어노테이션

@NoArgsConstructor 매개변수가 없는 기본 생성자 자동 생성
@AllArgsConstructor 모든 필드를 매개변수로 받는 전체 생성자 자동 생성

 

 JPA 어노테이션

@Entity 클래스 이름을 보고 테이블 자동생성
@Id PK(Primary Key) 지정. @Entity가 붙은 클래스에는 반드시 하나 필요
@Table(name = 'e_test' ) 테이블 이름 새로 지정
@Column(name = "irum", nullable = false) 매핑될 컬럼 이름 및 제약조건 지정 (컬럼 이름: irum, not null)

 

 

 

🗂️entity

➡️📁EntityTest.java

package com.example.Ex02.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@NoArgsConstructor // 매개변수가 없는 생성자
@AllArgsConstructor // 매개변수가 모두 있는 생성자
@Table(name = "eTest") // 테이블 이름 새로 지정
@Entity // 클래스 이름을 보고 테이블 자동생성 
public class EntityTest {
    @Id // PK(Primary Key) 지정 @Entity가 붙은 클래스에는 반드시 하나 필요

    private int num;

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

    private String addr;
    private int age;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

🌈4. 인터페이스 설정

package com.example.Ex02.repository;

import com.example.Ex02.entity.EntityTest;
import org.springframework.data.jpa.repository.JpaRepository;

// JpaRepository<엔티티 클래스, 프라이머리키(PK) 타입>
// EntityTest 엔티티를 관리하고, PK 타입은 int 이므로 Integer 사용
public interface EntityRepository extends JpaRepository<EntityTest, Integer> {
}

 

✅ 특징

  1. JpaRepository<EntityTest, Integer>
    • 첫 번째 제네릭: 엔티티 클래스 (EntityTest)
    • 두 번째 제네릭: PK 타입 (int → Integer)
  2. JpaRepository 를 상속했기 때문에 CRUD 기능이 자동으로 제공
    • JpaRepository<T, ID>CrudRepository<T, ID>PagingAndSortingRepository<T, ID> 를 상속
    • 그래서 save, findById, findAll, deleteById 같은 CRUD 메서드가 기본 내장

🌈5. EntityTest 엔티티를 저장(insert) - Save() 

@SpringBootTest가 import가 안될때

더보기
더보기

spring-boot-starter-test 에서 scope주석 처리

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <!--<scope>test</scope>-->
</dependency>

 

@SpringBootTest
public class EntityRepositoryTest {

    @Autowired
    EntityRepository entityRepository;

    @Test
    @DisplayName("entity 테스트")
    public void entitySave(){
        EntityTest etest = new EntityTest();
        etest.setNum(100);
        etest.setName("ljr");
        etest.setAddr("경기");
        etest.setAge(20);

        entityRepository.save(etest);
    }
}

 

🔽실행방법 (메서드만 실행)

1. 왼쪽 메서드 실행 버튼

 

2. 마우스 우클릭 후 Run 'entitySave()'

 

 

 

➡️JpaRepository 의 save() 메서드는 반드시 엔티티(Entity) 객체만 받을 수 있습니다.

 


🌈6. EntityTest 엔티티를 조회(select) - findAll() / findByName()

 

6-1) 전체 조회 findAll()

- toString으로 출력시 dto에서 toString 오버라이딩

@Test
@DisplayName("10개 레코드 삽입")
public void e_testSave(){
    String[] irum = {"윤아", "아이유", "웬디", "민호"};
    String[] addr = {"서울", "제주", "부산"};
    int[] age = {10, 20, 30, 40};

    for(int i=1; i<=10; i++){
        EntityTest etest = new EntityTest();
        etest.setNum(i);
        etest.setName(irum[i % irum.length]); // 1 2 3 0 1 2 나눠서 나머지 결과값으로 반복작업
        etest.setAddr(addr[i % addr.length]);
        etest.setAge(age[i % age.length]);
        entityRepository.save(etest);
    }
}

@Test
@DisplayName("전체 레코드 조회")
public void findAllTest(){
    List<EntityTest> elists = entityRepository.findAll(); //전체 레코드 조회 : List 리턴
    for(EntityTest entity :elists){
/*            System.out.println(entity.getNum());
        System.out.println(entity.getName());
        System.out.println(entity.getAddr());
        System.out.println(entity.getAge());*/
        System.out.println(entity.toString()); // 메서드를 오버라이딩 하지않을시 => 주소출력, dto에서 오버라이딩
        System.out.println("================");
    }
}

 

 

6-2) 특정 레코드 조회 - findByName()

@Test
@DisplayName("이름이 '윤아'인 레코드 조회")
public void findName() {
    List<EntityTest> elists = entityRepository.findByName("윤아");
    for(EntityTest entity : elists){
        System.out.println(entity.toString());
    }
}

@Test
@DisplayName("이름에 '수'가 포함된 이름 조회")
public void findNameFind(){
    List<CompanyEntity> lists = companyRepository.findByNameContaining("수");

    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("이름에 '수'가 포함된 이름 조회 & 회사명 내림차순 조회")
public void findNameAndCompanyFind(){
    List<CompanyEntity> lists = companyRepository.findByNameContainingOrderByCompanyDesc("수");

    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("나이가 30인 레코드 조회")
public void findAge() {
    List<EntityTest> elists = entityRepository.findByAge(30);
    for(EntityTest entity : elists){
        System.out.println(entity.toString());
    }
}

@Test
@DisplayName("나이가 30 초과인 레코드 조회")
public void findAgeOver() {
    List<EntityTest> elists = entityRepository.findByAgeGreaterThan(30);
    for(EntityTest entity : elists){
        System.out.println(entity.toString());
    }
}

@Test
@DisplayName("나이가 30 이상인 레코드 조회")
public void findAgeEqual() {
    List<EntityTest> elists = entityRepository.findByAgeGreaterThanEqual(30);
    for(EntityTest entity : elists){
        System.out.println(entity.toString());
    }
}

 

인터페이스

 

// JpaRepository<EntityTest, Integer> 
// → 첫 번째 제네릭(EntityTest): 엔티티 클래스
// → 두 번째 제네릭(Integer): 프라이머리키(기본키) 타입
public interface EntityRepository extends JpaRepository<EntityTest, Integer> {

    // 1. name 컬럼이 특정 값과 일치하는 레코드 조회
    List<EntityTest> findByName(String name);

    // 2. age 컬럼이 특정 값과 일치하는 레코드 조회
    List<EntityTest> findByAge(int i);

    // 3. age 컬럼이 특정 값보다 큰 레코드 조회 (>)
    List<EntityTest> findByAgeGreaterThan(int i);

    // 4. age 컬럼이 특정 값 이상인 레코드 조회 (>=)
    List<EntityTest> findByAgeGreaterThanEqual(int i);
    
    
    
    List<CompanyEntity> findBySalaryGreaterThanEqual(int i);

    List<CompanyEntity> findByNameContaining(String 수);

    List<CompanyEntity> findByNameContainingOrderByCompanyDesc(String 수);
    
}

 

  1. 메서드 네이밍 규칙 (쿼리 메서드)
    • findBy필드명 → 해당 필드 조건으로 조회
    • findBy필드명And필드명 → 여러 조건 조회
    • findBy필드명GreaterThan → > 조건
    • findBy필드명GreaterThanEqual → >= 조건
    • findBy필드명LessThan, Between, Like, In 등도 가능
  2. 자동으로 JPQL 생성됨
    • 예) findByName("윤아")
      → 실행 SQL: select * from entity_test where name = '윤아';
    • 예) findByAgeGreaterThan(20)
      → 실행 SQL: select * from entity_test where age > 20;

 

6-3) id조회 (primarykey)

@Test
@DisplayName("id 조회")
// PK가 num인 경우 findById()와 findByNum()은 결과가 동일하다. 하지만 findById()는 PK 전용
public void getId(){
    Optional<ItemEntity> o = itemRepository.findById(50L);
    System.out.println("o" + o);
}

 

➡️findById()Optional을 리턴

➡️PK가 Long 타입 이므로 findById(50L) 메서드 안 숫자에 'L' 기입

 

6-4) 가격 조회(primarykey)

@Test
@DisplayName("6. price조회")
// price 300보다 작은 레코드를 가격기준 내림차순 정렬 조회 출력
// Query method
public void getPrice(){
    List<ItemEntity>lists = itemRepository.findByPriceLessThanOrderByPriceDesc(300);
    for (ItemEntity item:lists){
        System.out.println(item);
    }
}

@Test
@DisplayName("7. price조회")
// price 300큰 레코드를 가격기준 오름차순 정렬 조회 출력
// Query method
public void getPrice2(){
    List<ItemEntity>lists = itemRepository.findByPriceGreaterThanOrderByPriceAsc(300);
    for (ItemEntity item:lists){
        System.out.println(item);
    }
}

 

 

인터페이스

public interface ItemRepository extends JpaRepository<ItemEntity, Long> {

    List<ItemEntity> findByItemNm(String ss);

    List<ItemEntity> findByPriceLessThanOrderByPriceDesc(int i);

    List<ItemEntity> findByPriceGreaterThanOrderByPriceAsc(int i);
}

 

🌈7. 쿼리 어노테이션 (JPQL)

//쿼리 어노테이션
@Test
@DisplayName("Query annotation으로 전체레코드 조회")
public void allSelect(){
    List<CompanyEntity> lists = companyRepository.findAllQuery();
    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("전체 레코드 개수조회")
public void countCompany(){
    int count = companyRepository.countCompany();
    System.out.println("➡️count :" + count);
}

@Test
@DisplayName("이름이 '수지'인 레코드 조회")
public void findByNameSuji(){
    List<CompanyEntity> lists = companyRepository.findByNameSuji("수지");
    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("이름에 '수'가 포함된 레코드 조회")
public void findByNameSujiContainQuery(){
    List<CompanyEntity> lists = companyRepository.findNameContainSu("수");
    for(CompanyEntity cEntity : lists) {
        System.out.println(cEntity.toString());
    }
}

@Test
@DisplayName("상세설명에 '어' 포함된 레코드 조회")
public void ItemDetailFind(){
    List<ItemEntity>lists = itemRepository.findByItemDetail("어");
    for (ItemEntity item:lists){
        System.out.println(item);
    }
}

@Test
@DisplayName("상세설명에 '어' 포함 & 가격이 300이상 레코드 조회")
public void ItemDetailPriceFind(){
    List<ItemEntity>lists = itemRepository.findByItemDetailPrice("어", 300);
    for (ItemEntity item:lists){
        System.out.println(item);
    }
}

@Test
@DisplayName("7. 레코드 삭제")
public void deleteId(){
    itemRepository.deleteById(1L);
    }

 

인터페이스

nativeQuery = true : 테이블명 작성
nativeQuery = false : 클래스명 작성
package com.example.Ex02.repository;


import com.example.Ex02.entity.CompanyEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

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

public interface CompanyRepository extends JpaRepository<CompanyEntity, Integer> {

    // 1. 전체 조회
    /*@Query(value = "select * from company_entity", nativeQuery = true)*/
    @Query(value = "select i from CompanyEntity i", nativeQuery = false)
    List<CompanyEntity> findAllQuery();

    // 2. 레코드 개수 조회
    @Query("select count(company) from CompanyEntity company")
    int countCompany();
    
    // 3. 이름이 '수지'인 사람 조회
    @Query("SELECT i FROM CompanyEntity i WHERE i.name = :name")
    List<CompanyEntity> findByNameSuji(@Param("name") String name);
    
    // 4. 이름에 '수'가 포함된 사람조회 & 회사이름 내림차순
    @Query("SELECT i FROM CompanyEntity i WHERE i.name LIKE %:name% order by company desc")
    List<CompanyEntity> findNameContainSu(@Param("name") String name);
    
    // 5. 상세설명에 '어'들어간 레코드 찾기
    @Query(value = "select i from ItemEntity i where i.itemDetail like %:itemDetail%")
    List<ItemEntity> findByItemDetail(@Param("itemDetail")String itemDetail);

    // 6. 상세설명에 '어'포함 & price가 300이상인 레코드 조회
    @Query(value = "select i from ItemEntity i where i.itemDetail like %:itemDetail% and i.price >= :price")
    List<ItemEntity> findByItemDetailPrice(@Param("itemDetail, price") String itemDetail, int price);
    
    //8. 특정 레코드 삭제
    @Modifying
    @Transactional
    @Query(value = "delete from item_entity where item_name = :itemname", nativeQuery = true)
    void deleteItemName(@Param("itemname") String itemName);
}

 

delete를 할때 두개의 어노테이션이 필요한 이유

1️⃣ @Modifying — “SELECT 아님!” 표시

2️⃣ @Transactional — : “commit / rollback” 보장

: 삭제가 되면 커밋, 안되면 롤백

데이터를 바꾸는(delete/update/insert) 모든 쿼리는 트랜잭션 안에서만 안전하게 실행