JPQL이란 JPA Query Language의 줄임말, JPA에서 사용할 수 있는 쿼리.
문법은 SQL과 매우 비슷하다. SQL과의 차이점은 SQL에서는 테이블이나 컬럼의 이름을 사용하는 것과 달리 JPQL은 다음과 같이 엔티티 객체를 대상으로 수행하는 쿼리이기에 매핑된 엔티티의 이름과 필드의 이름을 사용한다는 것이다.
SELECT p FROM Product(엔티티 타입) p WHERE p.number(엔티티 속성) = ?1;
리포지토리는 JpaRepository를 상속받는 것만으로도 다양한 CRUD 메서드 제공.
하지만 이러한 기본 메서드들은 식별자 기반으로 생성되기에 별도의 메서드를 정의하는 경우가 많음. 이때 간단한 쿼리문 작성을 위해 사용되는 것이 쿼리 메서드.
'find...By', 'exist...By'와 같은 키워드로 쿼리의 주제를 정함.
'By' - 서술어의 시작을 나타내는 구분자 역할.
서술어 부분은 검색 및 정렬 조건을 지정하는 영역. 엔티티 속성 정의, AND나 OR 사용해 조건 확장 가능
예) (리턴타입) + {주제+서술어(속성)} 구조의 메서드
List<Person> findByLastnameAndEmail(String lastName, String email);
서술어에 들어가는 엔티티 속성 식(Expression)은 위와 같이 엔티티에서 관리하고 있는 속성(필드)만 참조 가능
조회하는 기능을 수행하는 키워드. '...'으로 표시한 영역에는 도메인(엔티티) 표현. 그러나 리포지토리에서 이미 도메인을 설정한 후에 메서드를 사용하기 때문에 중복으로 판단해 생략하기도 함. 리턴 타입으로 Collection이나 Stream에 속한 하위 타입 설정 가능.
// 리포지토리에 쿼리 메서드 작성
Optional<Product> findByNumber(Long number);
List<Product> findAllByName(String name);
Product queryByNumber(Long number);
특정 데이터가 존재하는지 확인하는 키워드. 리턴 타입으로는 boolean 사용.
boolean existsByNumber(Long number);
조회 쿼리를 수행한 후 쿼리 결과로 나온 레코드 개수 리턴.
long countByName(String name);
삭제 쿼리 수행. 리턴 타입 없거나 삭제한 횟수 리턴.
void deleteByNumber(Long number);
long removeByName(String name);
쿼리를 통해 조회된 결과값의 개수를 제한하는 키워드. 두 키워드는 동일한 동작 수행. 주제와 By 사이에 위치함. 일반적으로 한 번의 동작으로 여러 건을 조회할 때 사용, 단 건 조회는 <number.> 생략.
List<Product> findFirst5ByName(String name);
List<Product> findTop100ByName(String name);
JPQL의 서술어 부분에서 사용
값의 일치를 조건으로 사용하는 조건자 키워드. 생략되는 경우가 많으며 Equals와 동일한 기능 수행.
// findByNumber 메서드와 동일하게 동작
Product findByNumberIs(Long number);
Product findByNumberEquals(Long number);
값의 불일치를 조건으로 사용하는 조건자 키워드. Is는 생략하고 Not 키워드만 사용 가능.
Product findByNumberIsNot(Long number);
Product findByNumberNot(Long number);
값이 null인지 검사하는 조건자 키워드.
List<Product> findByUpdatedAtNull();
List<Product> findByUpdatedAtIsNull();
List<Product> findByUpdatedAtNotNull();
List<Product> findByUpdatedAtIsNotNull();
boolean 타입으로 지정된 칼럼값을 확인하는 키워드.
Product findByisActiveTrue();
Product findByisActiveIsTrue();
Product findByisActiveFalse();
Product findByisActiveIsFalse();
여러 조건을 묶을 때 사용.
Product findByNumberAndName(Long number, String name);
Product findByNumberOrName(Long number, String name);
숫자나 datetime 컬럼을 대상으로 한 비교 연산에 사용할 수 있는 조건자 키워드.
GreaterThan, LessThan 키워드는 비교 대상에 대한 초과/미만의 개념으로 비교 연산 수행, 경계값을 포함하려면 Equal 키워드 추가.
List<Product> findByPriceIsGreaterThan(Long price);
List<Product> findByPriceGreaterThan(Long price);
List<Product> findByPriceGreaterThanEqual(Long price);
List<Product> findByPriceIsLessThan(Long price);
List<Product> findByPriceLessThan(Long price);
List<Product> findByPriceLessThanEqual(Long price);
List<Product> findByPriceIsBetween(Long lowPrice, Long highPrice);
List<Product> findByPriceBetween(Long lowPrice, Long highPrice);
컬럼값에서 일부 일치 여부를 확인하는 조건자 키워드.
SQL 쿼리문에서 값의 일부를 포함하는 값을 추출할 때 사용하는 '%' 키워드와 동일한 역할을 하는 키워드.
자동으로 생성되는 SQL문을 보면 Containing 키워드는 문자열의 양 끝, StartingWith 키워드는 문자열의 앞, EndingWith 키워드는 문자열의 끝에 '%'가 배치됨. 여기서 별도로 고려해야 하는 키워드는 Like 키워드인데, 이 키워드는 코드 수준에서 메소드를 호출하면서 전달하는 값에 %를 명시적으로 입력해야 함.
List<Product> findByNameLike(String name);
List<Product> findByNameIsLike(String name);
List<Product> findByNameContains(String name);
List<Product> findByNameContaining(String name);
List<Product> findByNameIsContaining(String name);
List<Product> findByNameStartsWith(String name);
List<Product> findByNameStartingWith(String name);
List<Product> findByNameIsStartingWith(String name);
List<Product> findByNameEndsWith(String name);
List<Product> findByNameEndingWith(String name);
List<Product> findByNameIsEndingWith(String name);
기본 쿼리 메서드 작성 후 OrderBy 키워드 삽입, 정렬하고자 하는 칼럼과 오름차순/내림차순 설정
List<Product> findByNameOrderByNumberAsc(String name); // Asc : 오름차순 // 상품정보를 이름으로 검색한 후 상품 번호로 오름차순 정렬
List<Product> findByNameOrderByNumberDesc(String name); // Desc : 내림차순
조건 여러 개 사용시 And나 Or 키워드 사용X
List<Product> findByNameOrderByPriceAscStockDesc(String name); // 먼저 Price를 기준으로 오름차순 정렬 후 후순위로 재고수량을 기준으로 내림차순 정렬 수행
매개변수를 활용한 쿼리 정렬
List<Product> findByName(String name, Sort sort);
이름에 키워드를 넣지 않고 Sort 객체를 활용해 매개변수로 받아들인 정렬 기준을 가지고 쿼리문 작성. 쿼리 메서드를 정의하는 단계에서 코드가 줄어드는 장점이 있음.
@SpringBootTest
public class ProductRepositoryTest0 {
@Autowired
ProductRepository productRepository;
@Test
void sortingAndPagingTest() {
Product product1 = new Product();
product1.setName("펜");
product1.setPrice(1000);
product1.setStock(100);
product1.setCreatedAt(LocalDateTime.now());
product1.setUpdatedAt(LocalDateTime.now());
Product product2 = new Product();
product2.setName("펜");
product2.setPrice(5000);
product2.setStock(300);
product2.setCreatedAt(LocalDateTime.now());
product2.setUpdatedAt(LocalDateTime.now());
Product product3 = new Product();
product3.setName("펜");
product3.setPrice(500);
product3.setStock(50);
product3.setCreatedAt(LocalDateTime.now());
product3.setUpdatedAt(LocalDateTime.now());
Product savedProduct1 = productRepository.save(product1);
Product savedProduct2 = productRepository.save(product2);
Product savedProduct3 = productRepository.save(product3);
productRepository.findByName("펜", Sort.by(Sort.Order.asc("price")));
productRepository.findByName("펜", Sort.by(Sort.Order.asc("price"), Sort.Order.desc("stock")));
}
}
Sort 부분을 하나의 메서드로 분리해 쿼리 메서드를 호출하는 방법
...상단 코드 동일, 생략
productRepository.findByName("펜", getSort());
}
private Sort getSort() {
return Sort.by(
Sort.Order.asc("price"),
Sort.Order.desc("stock")
);
}
데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것.
ex) 25개 레코드 -> 레코드를 7개씩, 총 4개의 페이지로 구분 / 특정 페이지를 가져옴.
JPA에서는 페이징 처리를 위해 Page와 Pageable 사용.
// 페이징 처리를 위한 쿼리 메서드
Page<Product> findByName(String name, Pageable pageable);
리턴 타입으로 Page를 설정하고 매개변수에는 Pageable 타입의 객체 정의.
// 페이징 쿼리 메서드 호출
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));
리턴 타입으로 Page 객체를 받아야 하기에 Page<Product>로 타입을 선언해 객체를 리턴 받음, Pageable 파라미터를 전달하기 위해 PageRequest 클래스 사용. PageRequest는 Pageable의 구현체.
일반적으로 PageRequest는 of 메서드를 통해 PageRequest 객체 생성. of 메서드는 매개변수에 따라 다양한 형태로 오버로딩.
Page 객체를 그대로 출력하면 해당 객체의 값을 보여주지 않고 몇 번째 페이지에 해당하는지만 확인 가능.
Page<Product> productPage = productRepository.findByName("펜", PageRequest.of(0, 2));
System.out.println(productPage.getContent()); // 배열 형태로 값 출력
@Query 어노테이션을 사용해 직접 JPQL을 작성할 수 있음.
@Query("SELECT p FROM Product AS p WHERE p.name = ?1")
List<Product> findByName(String name);
FROM 뒤에서 엔티티 타입 지정, 별칭 생성(AS 생략 가능).
WHERE문에서는 SQL과 마찬가지로 조건 지정.
조건문에서 사용한 '?1'은 파라미터를 전달받기 위한 인자에 해당, 1은 첫 번째 파라미터 의미. 파라미터의 순서가 바뀌면 오류 발생 가능성이 있어 다음과 같이 @Param 어노테이션을 사용하는 것이 좋음.
@Query("SELECT p FROM Product AS p WHERE p.name = :name")
List<Product> findByNameParam(@Param("name") String name);
파라미터를 바인딩하는 방식으로 메서드를 구현하면 코드의 가독성이 높아지고 유지보수가 수월해짐.
@Query를 사용하면 엔티티 타입이 아니라 원하는 컬럼의 값만 추출 가능.
@Query("SELECT p.name, p.price, p.stock FROM Product p WHERE p.name = :name")
List<Object[]> findByNameParam2(@Param("name") String name);
SELECT에 가져오고자 하는 컬럼 지정. 이때 메서드에서는 Object 배열의 리스트 형태로 리턴 타입을 지정해야 함.
메서드의 이름을 기반으로 생성하는 JPQL의 한계는 @Query 어노테이션을 통해 대부분 해소할 수 있지만 직접 문자열을 입력하기 때문에 컴파일 시점에 에러를 잡지 못하고 런타임 에러가 발생할 수 있음.
쿼리의 문자열이 잘못된 경우에는 애플리케이션이 실행된 후 로직이 실행되고 나서야 오류 발견.
이러한 이유로 개발 환경에서는 문제가 없어 보이다가 실제 운영 환경에 애플리케이션을 배포하고 나서 오류가 발견되는 리스크 유발.
이 같은 문제 해결을 위해 QueryDSL 사용.
QueryDSL은 문자열이 아닌 코드로 쿼리 작성을 도와줌.
정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크. 문자열이나 XML 파일을 통해 쿼리를 작성하는 대신 QueryDSL이 제공하는 Fluent API를 활용해 쿼리 생성.
1. 의존성 추가
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
2. APT 플러그인 추가
<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>
<options>
<querydsl.entityAccessors>true</querydsl.entityAccessors>
</options>
</configuration>
</execution>
</executions>
</plugin>
APT란?
Annotation Processing Tool
어노테이션으로 정의된 코드를 기반으로 새로운 코드를 생성하는 기능.
클래스를 컴파일하는 기능도 제공.
3. 메이븐의 compile 단계를 클릭해 빌드 작업 수행
빌드가 완료되면 위에서 작성했던 outputDirectory에 설정한 generated-source 경로에 다음과 같이 Q도메인 클래스가 생성된 것을 볼 수 있음.
4. IntelliJ에서 [File] -> [Project Structure]
[Mark as]의 [Sources] 눌러 IDE에서 소스파일로 인식할 수 있게 설정
QueryDSL 적용 완료❗
QueryDSL은 지금까지 작성했던 엔티티 클래스와 Q도메인이라는 쿼리 타입의 클래스를 자체적으로 생성해서 메타데이터로 사용. 이를 통해 SQL과 같은 쿼리를 생성해 제공
@PersistenceContext
EntityManager entityManager;
@Test
void queryDslTest() {
JPAQuery<Product> query = new JPAQuery(entityManager);
QProduct qProduct = QProduct.product;
List<Product> productList = query
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Product product : productList) {
System.out.println("----------------");
System.out.println();
System.out.println("Product Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("----------------");
}
}
List 타입으로 값을 리턴받기 위해서는 fetch() 메서드 사용
@Test
void queryDslTest2() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
List<Product> productList = jpaQueryFactory.selectFrom(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Product product : productList) {
System.out.println("----------------");
System.out.println();
System.out.println("Product Number : " + product.getNumber());
System.out.println("Product Name : " + product.getName());
System.out.println("Product Price : " + product.getPrice());
System.out.println("Product Stock : " + product.getStock());
System.out.println();
System.out.println("----------------");
}
}
만약 전체 컬럼을 조회하지 않고 일부만 조회하고 싶다면 다음과 같이 selectFrom()이 아닌 select()와 from() 메서드를 구분해서 사용하면 됨.
@Test
void queryDslTest3() {
JPAQueryFactory jpaQueryFactory = new JPAQueryFactory(entityManager);
QProduct qProduct = QProduct.product;
// select 대상이 하나인 경우
List<String> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (String product : productList) {
System.out.println("----------------");
System.out.println("Product Name : " + product);
System.out.println("----------------");
}
// select 대상이 여러 개일 경우 (,)로 구분해서 작성
List<Tuple> tupleList = jpaQueryFactory
.select(qProduct.name, qProduct.price)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (Tuple product : tupleList) {
System.out.println("----------------");
System.out.println("Product Name : " + product.get(qProduct.name));
System.out.println("Product Name : " + product.get(qProduct.price));
System.out.println("----------------");
}
}
@Configuration
public class QueryDSLConfiguration {
@PersistenceContext
EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
JPAQueryFactory 객체를 @Bean 객체로 등록해두면 매번 JPAQueryFactory를 초기화하지 않고 스프링 컨테이너에서 가져다 쓸 수 있음.
@Autowired
JPAQueryFactory jpaQueryFactory;
@Test
void queryDSLTest4() {
QProduct qProduct = QProduct.product;
List<String> productList = jpaQueryFactory
.select(qProduct.name)
.from(qProduct)
.where(qProduct.name.eq("펜"))
.orderBy(qProduct.price.asc())
.fetch();
for (String product : productList) {
System.out.println("----------------");
System.out.println("Product Name : " + product);
System.out.println("----------------");
}
}
스프링 데이터 JPA에서는 QueryDSL을 더욱 편하게 사용할 수 있게 QuerydslPredicateExecutor 인터페이스와 QuerydslRepositorySupport 클래스 제공
JpaRepository와 함께 리포지토리에서 QueryDSL을 사용할 수 있게 인터페이스 제공
QuerydslPredicateExecutor를 사용하는 리포지토리 생성
public interface QProductRepository extends JpaRepository<Product, Long>, QuerydslPredicateExecutor<Product> {
}
QuerydslPredicateExecutor에서 제공하는 메서드
대부분 Predicate 타입을 매개변수로 받음.
Predicate는 표현식을 작성할 수 있게 QueryDSL에서 제공하는 인터페이스.
@SpringBootTest
public class QProductRepositoryTest {
@Autowired
QProductRepository qProductRepository;
@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());
}
}
}
Predicate는 간단하게 표현식으로 정의하는 쿼리.
위는 Predicate를 명시적으로 정의하고 사용했지만 다음과 같이 서술부만 가져다 사용할 수도 있음.
@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를 활용하면 더욱 편하게 QueryDSL을 사용할 수 있지만 join이나 fetch 기능은 사용할 수 없는 단점 O
가장 보편적으로 사용하는 방식은 CustomRepository를 활용해 리포지토리를 구현하는 방식
ProductRepositoryCustom
인터페이스를 생성하고 쿼리로 구현하고자 하는 메서드 정의
public interface ProductRepositoryCustom {
List<Product> findByName(String name);
}
ProductRepositoryCustom 인터페이스의 구현체인 ProductRepositoryCustomImpl 클래스 생성
QueryDSL 사용을 위해 QuerydslRepositorySupport를 상속받고 ProductRepositoryCustom 인터페이스 구현. QuerydslRepositorySupport를 상속받으면 생성자를 통해 도메인 클래스를 부모 클래스에 전달해야 함.
인터페이스에 정의한 메서드 구현.
@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)
.where(product.name.eq(name))
.select(product)
.fetch();
return productList;
}
}
ProductRepository 인터페이스
기존에 리포지토리를 생성하는 것과 동일하게 JpaRepository 상속받음.
기본적으로 JpaRepository에서 제공하는 메서드도 사용할 수 있고, 별도로 ProductRepositoryCustom 인터페이스에서 정의한 메서드도 구현체를 통해 사용 가능.
@Repository("productRepositorySupport")
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
}
@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());
}
}
}
리포지토리를 구성하면서 모든 로직을 구현했기 때문에 findByName() 메서드를 사용할 때는 위와 같이 간단히 구현 가능
JPA에서 'Audit'이란 '감시하다'라는 뜻
각 데이터마다 '누가', '언제' 데이터를 생성했고 변경했는지 감시한다는 의미
대표적으로 많이 사용되는 필드
이러한 필드들은 매번 엔티티 생성 또는 변경 때마다 값을 주입해야 하는 번거로움. 이를 해소하기 위해 Spring Data JPA에서 이러한 값을 자동으로 넣어주는 기능 제공.
@SpringBootApplication
@EnableJpaAuditing
public class JpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
}
또는
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}
코드의 중복을 없애기 위해 각 엔티티에 공통으로 들어가게 되는 컬럼(필드)을 하나의 클래스로 빼는 작업 수행
@Getter
@Setter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt; // 생성 일자
@LastModifiedDate
private LocalDateTime updatedAt; // 변경 일자
}
위와 같이 BaseEntity를 생성하고 Product 엔티티 클래스에서 공통 컬럼을 제거해서 다음과 같이 코드 수정
@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Table(name = "product")
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long number;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer price;
@Column(nullable = false)
private Integer stock;
}
@ToString, @EqualAndHashCode 어노테이션에 적용한 callSuper 속성은 부모 클래스의 필드를 포함하는 역할 수행
이렇게 설정하면 매번 LocalDateTime.now() 메서드를 사용해 시간을 주입하지 않아도 자동으로 값이 생성됨
@Test
public void auditingTest() {
Product product = new Product();
product.setName("펜");
product.setPrice(1000);
product.setStock(100);
Product savedProduct = productRepository.save(product);
System.out.println("productName : " + savedProduct.getName());
System.out.println("createdAt : " + savedProduct.getCreatedAt());
}
// 출력 결과
productName : 펜
createdAt : 2023-11-20T16:13:13.546600200
Product 엔티티에 생성일자를 입력하지 않았지만 데이터베이스에는 생성일자가 저장되며, 엔티티의 필드를 출력해보면 해당 시간이 출력됨.
JPA Auditing 기능에는 @CreatedBy, @ModifiedBy 어노테이션도 존재.
누가 엔티티를 생성했고 수정했는지 자동으로 값을 주입하는 기능.
이 기능을 사용하려면 AuditorAware를 스프링 빈으로 등록해야 함.
Q&A
1. JPA에서 사용할 수 있는 쿼리는?
답: JPQL
2. 조회하는 기능을 수행하는 쿼리 메서드의 주제 키워드는?
답: find...By
3. 값이 null인지 검사하는 쿼리 메서드의 조건자 키워드는?
답: (Is)Null
4. 데이터베이스의 레코드를 개수로 나눠 페이지를 구분하는 것은?
답: 페이징
5. 이 어노테이션을 사용하면 직접 JPQL을 작성할 수 있다. 이 어노테이션 이름은?
답: @Query
6. 정적 타입을 이용해 SQL과 같은 쿼리를 생성할 수 있도록 지원하는 프레임워크는?
답: QueryDSL
7. 데이터 생성 날짜를 자동으로 주입해주는 어노테이션 이름은?
답: @CreatedDate
8. 리턴 타입이 Person 엔티티 리스트이고, Lastname 속성과 Email 속성으로 엔티티를 조회하는 쿼리 메서드 코드를 작성하여라.
List<Person> findByLastnameAndEmail(String lastName, String email);
9. 리턴 타입이 Product 엔티티 리스트이고, 상품정보를 이름(NameOrder)으로 검색한 후 상품번호(Number)로 오름차순 정렬하는 쿼리 메서드를 작성하여라.
List<Product> findByNameOrderByNumberAsc(String name);
출처 - (책) 스프링 부트 핵심 가이드 / 장정우, 위키북스
ⓒ 머핀
[스프링 3팀] 10장. 유효성 검사와 예외 처리 (2) | 2023.12.22 |
---|---|
[스프링3] 9장 연관관계 매핑 (1) | 2023.12.01 |
[스프링 3] 07 테스트 코드 작성하기 (0) | 2023.11.17 |
[스프링3] 6장. 데이터베이스 연동 - 2 (0) | 2023.11.10 |
[스프링3] 6장. 데이터베이스 연동 - 1 (0) | 2023.11.03 |