Spring Data JPA :: Spring Data JPA
Oliver Gierke, Thomas Darimont, Christoph Strobl, Mark Paluch, Jay Bryant, Greg Turnquist Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that
docs.spring.io
JPQL(JPA Query Language)
JPQL은 자바 영속성 쿼리 언어(Java Persistence Query Language)의 약자로, 객체 지향 데이터베이스 시스템인 JPA(Java Persistence API)에서 사용되는 쿼리 언어다. JPQL은 객체 지향적인 방식으로 데이터베이스를 쿼리할 수 있게 해주며, 엔터티와 그들의 속성을 기반으로 쿼리를 작성할 수 있다.
JPQL은 SQL과 유사하지만, 데이터베이스의 테이블이 아닌 엔터티와 엔터티의 관계를 기반(매핑된 엔티티의 이름과 필드의 이름을 사용)으로 쿼리를 작성한다. 따라서 JPQL은 객체 지향 모델에 더 가깝고, 특정 데이터베이스 제품에 종속되지 않는다. 이는 애플리케이션을 다른 데이터베이스로 전환할 때도 쿼리를 변경하지 않아도 되는 이점을 제공한다.
JPQL은 주로 JPA에서 엔터티를 조회, 수정, 삭제하는 등의 작업을 수행할 때 사용된다.
JPQL 쿼리는 다음과 같은 형식을 가질 수 있다.
SELECT e FROM EntityName e WHERE e.property = :value
여기서 EntityName은 조회할 엔터티 타입의 이름이며, property는 해당 엔터티 속성이고, value는 쿼리 파라미터다.
JPQL은 복잡한 쿼리를 작성할 수 있으며, 다양한 기능과 연산자를 지원한다. 또한 네이티브 SQL 쿼리를 사용할 수도 있다.
쿼리 메서드
쿼리 메서드(Query Methods)는 Spring Data JPA에서 제공하는 기능 중 하나로, 메서드 이름을 통해 간단하고 직관적으로 데이터베이스 쿼리를 작성할 수 있는 방법입니다.
Spring Data JPA는 JpaRepository를 상속받는 것만으로도 엔터티(repository)의 기본 CRUD(Create, Read, Update, Delete) 기능을 제공한다. 기본 CRUD메서드들은 식별자 기반으로 생성되기 때문에 별도로 사용자가 정의한 메서드 이름을 분석하여 해당하는 쿼리를 자동으로 생성하고 실행할 수 있는 기능을 지원한다.
쿼리 메서드는 주제(Subject)와 서술어(Predicate)로 구분한다.
주제(Subject)
조회메서드: find...By, read...By, get...By, query...By, serach...By, stream...By
List<User> findByFirstName(String firstName);
List<User> readByLastName(String lastName);
User getByUsername(String username);
List<Product> queryByCategory(String category);
List<Article> searchByTitleContaining(String keyword);
Stream<Order> streamByCustomerId(Long customerId);
exists...By
boolean existsByEmail(String email);
existsBy 메서드는 주어진 조건을 만족하는 엔터티의 존재 여부를 반환한다.
count...By
long countByStatus(String status);
countBy 메서드는 주어진 조건을 만족하는 엔터티의 개수를 반환한다.
delete...By, remove...By
void deleteByExpiredDateBefore(LocalDate date);
deleteBy 또는 removeBy 메서드는 주어진 조건을 만족하는 엔터티를 삭제할 때 사용된다.
...First<number>..., ...Top<number>...
List<Product> findTop5ByOrderByPriceDesc();
First나 Top 키워드는 결과 집합에서 상위 N개의 엔터티를 가져오는 데 사용된다. 이를 통해 가장 높은 우선 순위의 엔터티를 검색할 수 있다.
조건자 키워드
Is
findByFirstNameIs(String firstName)
주어진 값과 일치하는 엔터티를 조회한다.
IsNot
findByLastNameIsNot(String lastName)
주어진 값과 일치하지 않는 엔터티를 조회한다.
IsNull / IsNotNull
findByEmailIsNull()
findByPhoneNumberIsNotNull()
속성이 NULL인 엔터티를 조회하거나, 특정 속성이 NULL이 아닌 엔터티를 조회한다.
IsTrue / IsFalse
findByActiveIsTrue()
findByEnabledIsFalse()
논리 값이 참(true) 또는 거짓(false)인 엔터티를 조회한다.
And / Or
findByFirstNameAndLastName(String firstName, String lastName)
findByAgeOrGender(int age, String gender)
여러 조건을 조합하여 엔터티를 조회한다. AND 연산자는 모든 조건이 충족되어야 하고, OR 연산자는 하나 이상의 조건이 충족되어야 한다.
IsGreaterThan / IsLessThan / IsBetween
findByAgeIsGreaterThan(int age)
findByPriceIsBetween(double minPrice, double maxPrice)
주어진 값보다 큰 엔터티를 조회하거나, 주어진 값보다 작은 엔터티를 조회하거나, 두 값 사이에 있는 엔터티를 조회한다.
경계값을 포함하려면 Equal을 추가해주어야한다.
IsStartingWith(=startsWith) / IsEndingWith(=EndsWith) / IsContaining(=Contains) / IsLike
findByFirstNameStartingWith(String prefix)
findByLastNameEndingWith(String suffix)
findByAddressContaining(String keyword)
findByEmailLike(String pattern)
특정 문자열로 시작하거나, 특정 문자열로 끝나거나, 특정 문자열을 포함하거나, 패턴에 일치하는 엔터티를 조회한다.
정렬과 페이징 처리
정렬처리: OrderBy 사용
List<Product> findByNameOrderByPriceAscStockDesc(String name);
정렬 구문에서는 And와 Or이 필요없이 우선순위 기준으로 차례대로 작성하면 된다.
가독성을 위해 매개변수를 이용해 정렬할 수 있다.
List<Product> findByName(String name, Sort sort);
호출부는 아래와 같다.
repo.findByName("이름", Sort.by(Order.asc("price"), Order.desc("stock")));
페이징 처리
페이징이란 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것을 의미한다.
JPA에서는 페이징 처리를 위해 Page와 Pageable을 제공한다.
// 쿼리메서드
Page<Product> findByName(String name, Pageable pageable);
// 호출
Page<Priduct> productPage = repo.findByName("이름", PageRequest.of(0,2));
// 배열 형태로 값 출력
System.out.println(productPage.getContent());
- PageRequest: Pageable의 구현체
- 일반적으로 of 메서드를 통해 객체를 생성한다.
@Query 어노테이션
@Query 어노테이션으로 JPQL을 직접 작성할 수 있다.
// 1 파라미터 순서로 ? 사용
@Query("SELECT p FORM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);
// 2 @Param 사용
@Query("SELECT p FORM Product AS p WHERE p.name = :name")
List<Product> findByName(@Param("name") String name);
QueryDSL
QueryDSL은 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크다. 문자열이 아닌 자바 코드로 쿼리를 생성하기 위한 도구다. QueryDSL을 사용하면 쿼리를 문자열이 아닌 자바 코드로 작성하여 컴파일 시점에 오류를 잡을 수 있으며, 코드 자동 완성 및 타입 안전성을 제공하여 개발 생산성을 향상시킨다.
장점
- 유형 안전한 쿼리: QueryDSL은 자바의 타입 시스템을 활용하여 쿼리 작성 시 타입 안전성을 보장한다. 이는 컴파일 시점에서 오류를 잡을 수 있고, 런타임 에러를 방지한다.
- 코드 자동 완성: IDE에서 QueryDSL을 사용할 경우, 엔터티 및 속성의 이름을 자동 완성할 수 있으므로 쿼리 작성이 더욱 편리하다.
- 동적 쿼리 생성: QueryDSL을 사용하면 동적으로 쿼리를 생성할 수 있다. 복잡한 조건을 프로그래밍적으로 구성하여 다양한 상황에 대응할 수 있다.
- JPA와의 통합: QueryDSL은 JPA와 함께 사용되기 때문에 JPA의 모든 기능을 활용할 수 있다. 엔터티와 관계를 기반으로 쿼리를 작성할 수 있다.
- SQL 및 JPQL 지원: QueryDSL은 SQL과 JPQL을 모두 지원한다. 따라서 데이터베이스 벤더에 독립적인 코드를 작성할 수 있다.
- 코드 가독성 향상: 문자열로 된 쿼리보다 자바 코드로 쿼리를 작성할 수 있으므로 가독성이 향상되고 유지보수가 쉬워진다.
- 다양한 기능 제공: 조건자, 정렬, 페이징, 서브쿼리 등 다양한 쿼리 작성 기능을 제공한다.
데이터베이스 쿼리를 더욱 효율적으로 작성하고 관리할 수 있도록 도와준다.
설정
의존성 추가
먼저 프로젝트의 빌드 도구(예: Maven, Gradle)에 QueryDSL 의존성을 추가해야 한다.
- Maven
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version> <!-- 최신 버전으로 변경 가능 -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version> <!-- 최신 버전으로 변경 가능 -->
</dependency>
- Gradle
implementation 'com.querydsl:querydsl-jpa:5.0.0' // 최신 버전으로 변경 가능
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0' // 최신 버전으로 변경 가능
APT 플러그인 추가
APT(Annotation Processing Tool)란 어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능이다.(JDK1.6부터 도입)
QueryDSL을 사용하여 엔터티에 대한 쿼리를 생성하려면 querydsl-apt 플러그인을 설정해야 한다.
- Maven - pom.xml
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- Gradle - build.gradle
plugins {
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
querydsl {
jpa = true
querydslSourcesDir = projectDir.absolutePath + "/src/main/generated"
}
dependencies {
compile "com.querydsl:querydsl-jpa"
}
IDE 설정
때에따라 IDE설정이 필요할 수 있다.
File -> Project Structure -> Modules 탭에서 generated-sources를 눌러 Mark as 항목에 있는 Sources를 눌러서 IDC에서 소스파일로 인식할 수 있게 설정한다.
Query DSL 사용하기
반환 메서드
- fetch(): 쿼리 결과를 리스트 형태로 반환한다. 쿼리 결과가 없으면 빈 리스트를 반환한다.
List<User> users = queryFactory
.selectFrom(qUser)
.fetch();
- fetchOne(): 쿼리 결과에서 첫 번째 엔터티를 반환한다. 쿼리 결과가 없으면 null을 반환한다.
User user = queryFactory
.selectFrom(qUser)
.where(qUser.id.eq(1))
.fetchOne();
- fetchFirst(): 쿼리 결과에서 첫 번째 엔터티를 반환한다. fetchOne()과 동일한 역할을 하지만, 명시적으로 첫 번째 엔터티임을 나타내어 가독성을 향상시킨다.
User user = queryFactory
.selectFrom(qUser)
.where(qUser.id.eq(1))
.fetchFirst();
- fetchCount(): 쿼리 결과의 엔터티 수를 반환한다.
long count = queryFactory
.selectFrom(qUser)
.fetchCount();
- fetchResults(): 쿼리 결과를 페이징하여 반환한다. 조회 결과 리스트와 개수를 포함한 Query Results를 반환한다.
QueryResults<User> results = queryFactory
.selectFrom(qUser)
.fetchResults();
List<User> users = results.getResults(); // 쿼리 결과
long total = results.getTotal(); // 전체 결과 수
이러한 반환 메서드들은 QueryDSL을 사용하여 작성된 쿼리를 실행하고 결과를 처리하는 데 사용된다. 필요에 따라 적절한 반환 메서드를 선택하여 사용하면 된다.
JPAQuery
JPAQuery query = new JPAQuery(entityManager);
QUser qUser = QUser.user; // QueryDSL의 엔터티 클래스
List<User> users = query
.from(qUser)
.where(qUser.age.gt(18)) // gt: greater than
.fetch();
JPAQueryFactory
select절부터 작성이 가능하다.
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
QUser qUser = QUser.user; // QueryDSL의 엔터티 클래스
List<User> users = queryFactory
.select(qUser.name, qUser.age)
.from(qUser)
.where(qUser.age.gt(18)) // gt: greater than
.fetch();
- Bean 등록
import javax.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.querydsl.jpa.impl.JPAQueryFactory;
@Configuration
public class QueryDSLConfig {
private final EntityManager entityManager;
public QueryDSLConfig(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
QuerydslPredicateExecutor 인터페이스
QuerydslPredicateExecutor 는 Spring Data JPA에서 제공하는 인터페이스로, 리포지토리에서 QueryDSL을 사용하여 동적인 쿼리를 실행할 수 있게 해준다. 이 인터페이스를 사용하면 개발자는 QueryDSL을 사용하여 복잡한 검색 조건을 지정할 수 있으며, Spring Data JPA가 이를 처리하여 데이터를 쉽게 조회할 수 있다.
- 리포지토리 생성
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
// 추가적인 메서드 정의
}
- 사용하기
@SpringBootTest
public class QProductRepositoryTest {
@Autowired
QProductRepository qProductRepository;
// 예제 8.34
@Test
public void queryDSLTest1() {
Predicate predicate = QProduct.product.name.containsIgnoreCase("펜")
.and(QProduct.product.price.between(1000, 2500));
Optional<Product> foundProduct = qProductRepository.findOne(predicate);
if(foundProduct.isPresent()){
Product product = foundProduct.get();
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
// 예제 8.35
@Test
public void queryDSLTest2() {
QProduct qProduct = QProduct.product;
Iterable<Product> productList = qProductRepository.findAll(
qProduct.name.contains("펜")
.and(qProduct.price.between(550, 1500))
);
for (Product product : productList) {
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
}
[QuerydslPredicateExecutor가 제공하는 주요 메서드]
- Optional<T> findOne(Predicate predicate)
: 주어진 Predicate에 따라 엔터티를 조회하고, 첫 번째 결과를 반환한다. - long count(Predicate predicate)
: 주어진 Predicate에 따라 엔터티의 수를 세어 반환한다. - boolean exists(Predicate predicate)
: 주어진 Predicate에 해당하는 엔터티가 존재하는지 여부를 확인한다. - Iterable<T> findAll(Predicate predicate)
: 주어진 Predicate에 따라 엔터티를 조회한다.
오버로딩 메서드들
Iterable<T> findAll(Predicate predicate):
주어진 Predicate에 따라 엔터티를 조회하여 Iterable로 반환합니다.
List<T> findAll(Predicate predicate, Sort sort):
주어진 Predicate에 따라 엔터티를 조회하고, 정렬 기준에 따라 정렬된 List로 반환합니다.
Page<T> findAll(Predicate predicate, Pageable pageable):
주어진 Predicate에 따라 엔터티를 조회하고, 페이징 처리하여 Page 객체로 반환합니다.
List<T> findAll(Predicate predicate, OrderSpecifier<?>... orders):
주어진 Predicate에 따라 엔터티를 조회하고, 주어진 OrderSpecifier에 따라 정렬된 List로 반환합니다.
List<T> findAll(Predicate predicate, QueryModifiers queryModifiers):
주어진 Predicate에 따라 엔터티를 조회하고, 주어진 QueryModifiers에 따라 변경된 결과를 List로 반환합니다.
<S extends T> Iterable<S> findAll(Predicate predicate, Class<S> type):
주어진 Predicate에 따라 엔터티를 조회하고, 주어진 타입으로 캐스팅하여 Iterable로 반환합니다.
join 이나 fetch기능은 사용할 수 없는 단점이 있다.
QuerydslRepositorySupport 추상 클래스
QuerydslRepositorySupport는 Spring Data JPA에서 제공하는 클래스로, QueryDSL을 사용하여 동적인 쿼리를 실행할 수 있게 해주는 유틸리티 클래스다.
보통 Repository 인터페이스를 확장하는 방식으로 사용됩니다. 이 클래스를 사용하면 Repository 메서드에 QueryDSL을 적용하여 동적인 검색을 수행할 수 있다.
package com.springboot.advanced_jpa.data.repository.support;
import com.springboot.advanced_jpa.data.entity.Product;
import java.util.List;
/**
* 필요한 쿼리를 작성할 메소드를 정의하는 인터페이스
* 예제 8.36
*/
public interface ProductRepositoryCustom {
List<Product> findByName(String name);
}
package com.springboot.advanced_jpa.data.repository.support;
import com.springboot.advanced_jpa.data.entity.Product;
import com.springboot.advanced_jpa.data.entity.QProduct;
import java.util.List;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import org.springframework.stereotype.Component;
// 예제 8.37
@Component
public class ProductRepositoryCustomImpl extends QuerydslRepositorySupport implements
ProductRepositoryCustom {
public ProductRepositoryCustomImpl() {
super(Product.class);
}
@Override
public List<Product> findByName(String name) {
QProduct product = QProduct.product;
List<Product> productList = from(product) // JPAQuery 리턴
.where(product.name.eq(name))
.select(product)
.fetch();
return productList;
}
}
package com.springboot.advanced_jpa.data.repository.support;
import com.springboot.advanced_jpa.data.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
// 예제 8.38
// repository 패키지 안에 있는 ProductRepository와 이름이 동일하여 Bean 생성 충돌이 발생하므로 Bean 이름을 지정해줘야함
@Repository("productRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
}
package com.springboot.advanced_jpa.data.repository.support;
import com.springboot.advanced_jpa.data.entity.Product;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
// 예제 8.39
@SpringBootTest
public class ProductRepositoryTest {
@Autowired
ProductRepository productRepository;
@Test
void findByNameTest(){
List<Product> productList = productRepository.findByName("펜");
for(Product product : productList){
System.out.println(product.getNumber());
System.out.println(product.getName());
System.out.println(product.getPrice());
System.out.println(product.getStock());
}
}
}
JPA Auditing
JPA(Auditing)은 자동으로 생성일자(created date) 및 최근 수정일자(last modified date)를 추적하고 엔티티에 대한 변경을 모니터링하는 기능을 제공한다. 이를 통해 엔티티의 변경 이력을 추적하고 관리할 수 있다. 주로 생성일자와 최근 수정일자를 기록하는 데 사용한다.
Spring Data JPA에서는 @CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate 어노테이션을 통해 JPA Auditing을 지원한다. 이러한 어노테이션을 사용하면 자동으로 생성일자와 최근 수정일자를 추적할 수 있다.
BaseEntity만들기
import javax.persistence.*;
import java.util.Date;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_date", nullable = false, updatable = false)
@CreatedDate
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "last_modified_date")
@LastModifiedDate
private Date lastModifiedDate;
// Getters and setters
}
BaseEntity 생성: 먼저 BaseEntity 클래스를 만든다. 이 클래스는 모든 엔티티의 기본 클래스가 될 것이다. 이 클래스에는 생성일시(createdDate)와 수정일시(lastModifiedDate)를 저장할 필드가 포함된다.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
Auditing 설정: Spring Boot 애플리케이션에서는 @EnableJpaAuditing 어노테이션을 사용하여 JPA auditing을 활성화할 수 있다. 이를 애플리케이션의 메인 설정 클래스에 추가한다.
또는 따로 설정해준다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.auditing.DateTimeProviderFactoryBean;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBeanSupport;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import java.util.Optional;
@Configuration
@EnableJpaAuditing(dateTimeProviderRef = "auditingDateTimeProvider")
public class JpaAuditingConfiguration {
@Bean
public AuditingDateTimeProvider auditingDateTimeProvider() {
return new AuditingDateTimeProvider();
}
@Bean
public AuditingHandler auditingHandler(DateTimeProvider dateTimeProvider) {
return new AuditingHandler(dateTimeProvider);
}
@Bean
public RepositoryFactorySupport auditingJpaRepositoryFactory(EntityManager entityManager,
AuditingHandler auditingHandler) {
JpaRepositoryFactoryBeanSupport factory = new JpaRepositoryFactoryBeanSupport();
factory.setEntityManager(entityManager);
factory.setAuditingHandler(auditingHandler);
return factory.getObject();
}
@Bean
public DateTimeProviderFactoryBean dateTimeProviderFactoryBean() {
DateTimeProviderFactoryBean factoryBean = new DateTimeProviderFactoryBean();
factoryBean.setDateTimeProvider(Optional.ofNullable(auditingDateTimeProvider()));
return factoryBean;
}
}
- EnableJpaAuditing 어노테이션을 사용하여 JPA auditing을 활성화. 이때 dateTimeProviderRef 속성을 통해 날짜 및 시간 공급자를 지정할 수 있다.
- AuditingDateTimeProvider를 빈으로 등록하여 날짜 및 시간 정보를 제공
- AuditingHandler를 빈으로 등록하여 JPA auditing을 처리
- JpaRepositoryFactoryBean을 사용하여 auditing을 적용한 리포지토리 팩토리를 생성
- DateTimeProviderFactoryBean을 사용하여 날짜 및 시간 공급자를 생성
이렇게하면 JPA auditing을 보다 세밀하게 제어할 수 있으며, 필요에 따라 auditing 관련 빈을 직접 구성할 수 있다.
'spring > spring jpa' 카테고리의 다른 글
[JPA] @MapsId로 FK를 PK로 설정하기 (0) | 2024.04.01 |
---|---|
[JPA][스프링 부트 핵심 가이드] 연관관계 매핑과 영속성 전이 (0) | 2024.02.19 |
[JPA][스프링 부트 핵심 가이드] 리포지토리 메서드 생성 규칙 (0) | 2024.02.08 |
[JPA][스프링 부트 핵심 가이드] JPA & 영속성 컨텍스트 & 엔티티 매니저 (0) | 2024.02.08 |
[JPA][스프링 부트 핵심 가이드] ORM의 개념과 장단점 (0) | 2024.02.08 |