스프링에서 JPA를 더 편리하게 사용할 수 있도록 도와주는 라이브러리
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {
Optional<T> findById(ID id);
<S extends T> S save(S entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}
인터페이스에 JpaRepository<> 인터페이스를 상속 받으면 Spring Data JPA가 프록시 기술을 사용해 구현 클래스를 대신 만들어 주고 구현 클래스의 인스턴스를 빈으로 등록한다.
Repository에 선언된 특정 메서드이름을 기반으로 Spring Data JPA가 JPQL 쿼리를 자동으로 만들어 실행해준다.
class User {
@Id Long pk;
Long id;
// …
}
interface UserRepository extends Repository<User, Long> {
Optional<User> findById(Long id);
Optional<User> findByPk(Long pk);
Optional<User> findUserById(Long id);
}
위와 같이 네이밍 룰에 따라 DB의 조회, 삽입, 수정, 삭제 등을 할 수 있다.
Optional<User> findById(Long id);
List<User> findByName(String name);
List<User> findByNameAndEmail(String name, String email);
List<User> findByNameOrEmail(String name, String email);
int save(User user);
Long countByAge(Integer Age);
boolean existsById(Long id);
void deleteById(Long id):
아래 표는 메서드에 포함되는 키워드가 어떤 의미인지, 어떻게 sql 쿼리로 변환되는지를 나타낸다.
| Keyword | Sample | JPQL snippet |
|---|---|---|
Distinct |
findDistinctByLastnameAndFirstname |
select distinct … where x.lastname = ?1 and x.firstname = ?2 |
And |
findByLastnameAndFirstname |
… where x.lastname = ?1 and x.firstname = ?2 |
Or |
findByLastnameOrFirstname |
… where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals |
findByFirstname, findByFirstnameIs, findByFirstnameEquals |
… where x.firstname = ?1 (or … where x.firstname IS NULL if the argument is null) |
Between |
findByStartDateBetween |
… where x.startDate between ?1 and ?2 |
LessThan |
findByAgeLessThan |
… where x.age < ?1 |
LessThanEqual |
findByAgeLessThanEqual |
… where x.age <= ?1 |
GreaterThan |
findByAgeGreaterThan |
… where x.age > ?1 |
GreaterThanEqual |
findByAgeGreaterThanEqual |
… where x.age >= ?1 |
After |
findByStartDateAfter |
… where x.startDate > ?1 |
Before |
findByStartDateBefore |
… where x.startDate < ?1 |
IsNull, Null |
findByAge(Is)Null |
… where x.age is null |
IsNotNull, NotNull |
findByAge(Is)NotNull |
… where x.age is not null |
Like |
findByFirstnameLike |
… where x.firstname like ?1 |
NotLike |
findByFirstnameNotLike |
… where x.firstname not like ?1 |
StartingWith |
findByFirstnameStartingWith |
… where x.firstname like ?1 (parameter bound with appended %) |
EndingWith |
findByFirstnameEndingWith |
… where x.firstname like ?1 (parameter bound with prepended %) |
Containing |
findByFirstnameContaining |
… where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy |
findByAgeOrderByLastnameDesc |
… where x.age = ?1 order by x.lastname desc |
Not |
findByLastnameNot |
… where x.lastname <> ?1 |
In |
findByAgeIn(Collection<Age> ages) |
… where x.age in ?1 |
NotIn |
findByAgeNotIn(Collection<Age> ages) |
… where x.age not in ?1 |
True |
findByActiveTrue() |
… where x.active = true |
False |
findByActiveFalse() |
… where x.active = false |
IgnoreCase |
findByFirstnameIgnoreCase |
… where UPPER(x.firstname) = UPPER(?1) |
Spring Data JPA의 findByXX 메서드는 기본적으로 Optional을 반환한다. 이로 인해 비즈니스 로직에서 Optional 처리를 위한 추가적인 작업이 필요하게 되는데, 이럴 때 default 메서드를 활용하면 이 문제를 우아하게 해결할 수 있다.
public interface UserRepository extends JpaRepository<User, Long> {
// Default 메소드를 사용하여 findById의 Optional을 내부적으로 처리
default User findUserById(Long id) {
return findById(id).orElseThrow(() -> new DataNotFoundException("User not found with id: " + id));
}
}
Spring Data JPA를 사용하다 보면 복잡한 쿼리 때문에 메서드명이 길어져 가독성을 해치는 경우가 있다. 이럴 때도 default 메서드를 활용하면 긴 메서드명을 간결하고 명확하게 표현할 수 있다.
public interface ProductRepository extends JpaRepository<Product, Long> {
// 기존의 긴 쿼리 메소드
List<Product> findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(String category, BigDecimal minPrice, BigDecimal maxPrice);
// Default 메소드를 사용하여 간결한 메소드명 제공
default List<Product> findProductsByCategoryAndPriceRange(String category, BigDecimal minPrice, BigDecimal maxPrice) {
return findAllByCategoryAndPriceGreaterThanEqualAndPriceLessThanEqualOrderByPriceAsc(category, minPrice, maxPrice);
}
}
여러 기본 제공 메서드를 하나의 고차 작업으로 결합할 수도 있다. 다만 Spring Data JPA의 Repository는 Data Access Layer의 일부로, 데이터베이스와의 상호작용만을 담당하는 것이 일반적이기 때문에 이 부분은 서비스 레이어에서 처리하는 것이 일반적이다.
public interface UserRepository extends JpaRepository<User, Long> {
// 사용자 ID로 사용자를 찾고, 존재할 경우 연락처 정보를 업데이트하는 메소드
default void updateUserContact(Long userId, String newContact) {
findById(userId).ifPresent(user -> {
user.setContact(newContact);
save(user);
});
}
}
Value 타입 종류
| 어노테이션 | 설명 |
|---|---|
| @Column | - String, Date, Boolean, 과 같은 타입들에 공통으로 사이즈 제한, 필드명 지정과 같이 옵션을 설정할 용도로 쓰인다. - Class 에 @Entity 가 붙어있으면 자동으로 필드들에 @Column 이 붙음 |
| @Enumerated | - Enum 매핑용도로 쓰이며 실무에서는 @Enumerated(EnumType.STRING) 으로 사용권장 - Default 타입인 ORDINAL 은 0,1,2.. 값으로 들어가기 때문에 추후 순서가 바뀔 가능성있다. |
| 어노테이션 | 설명 |
|---|---|
@Embeddable | 복합 값 객체로 사용할 클래스 지정 |
@Embedded | 복합 값 객체 적용할 필드 지정 |
@AttributeOverrides | 복합 값 객체 여러개 지정 |
@AttributeOverride | 복합 값 객체 필드명 선언 |
ex)
@Embeddable
public class Address {
private String city;
private String street;
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Setter
@Getter
public class Member{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "home_city")),
@AttributeOverride(name = "street", column = @Column(name = "home_street")),
})
private Address homeAddress;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "company_city")),
@AttributeOverride(name = "street", column = @Column(name = "company_street")),
})
private Address companyAddress;
}
위 Entity 로 인해 생성되는 테이블
CREATE TABLE MEMBER (
MEMBER_ID BIGINT NOT NULL AUTO_INCREMENT,
NAME VARCHAR(255) NOT NULL,
home_city VARCHAR(255) NOT NULL, # Embedded address.city
home_street VARCHAR(255) NOT NULL, # Embedded address.street
company_city VARCHAR(255) NOT NULL, # Embedded address.city
company_street VARCHAR(255) NOT NULL, # Embedded address.street
PRIMARY KEY (MEMBER_ID)
);
💡컬럼의 값 크기제한이 있기 때문에 현업에서는 Collection Value 를 사용하지 않고, 일대다 연관관계를 통한 Collection 타입의 변수를 주로 사용한다.
기본 타입의 콜렉션
@ElementCollection 어노테이션을 사용하여 매핑하면 된다.@ElementCollection 어노테이션을 사용하여 콜렉션 매핑이 가능하다.데이터 세트를 더 작고 관리하기 쉬운 Chunk로 나누는 기술
JpaRepository가 상속받는 상위 인터페이스로, 전체 문서들을 개별 페이지로 나누는 Pagination과 정렬하는 Sorting을 사용할 수 있게 해준다.
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
Window<User> findTop10ByLastname(String lastname, ScrollPosition position, Sort sort);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
limit(size)+1 된 값을 가져온다.Sort 클래스를 사용해 컬럼 값을 기준으로 정렬할 수 있다.
Sort sort1 = Sort.by("name").descending(); // 내림차순
Sort sort2 = Sort.by("password").ascending(); // 오름차순
Sort sortAll = sort1.and(sort2); // 2개이상 다중정렬도 가능하다
Pageable pageable = PageRequest.of(0, 10, sortAll); // pageable 생성시 추가
@Query 사용 시 Alias를 기준으로 정렬할 수 있다.
// Repository
// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u.user_name, u.password AS user_password FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
// Service
// 이렇게 해당 user_password 를 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", Sort.by("user_password"));
JpaSort 를 사용해서 쿼리 함수를 기준으로 정렬할 수 있다.
// Repository
// 아래와 같이 일반적인 쿼리에서
@Query("SELECT u FROM user u WHERE u.username = ?1") // 이건 없어도됨
List<User> findByUsername(String username, Sort sort);
// Service
// 이렇게 쿼리함수 LENGTH() 조건을 걸어서 password 문자길이 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", JpaSort.unsafe("LENGTH(password)"));
사용자 정의 JPQL 또는 SQL 쿼리를 정의하기 위한 어노테이션이다.
조건이 너무 많아져 메서드 명이 너무 길어지면 가독성이 떨어질 때 사용하면 유용할 수 있다.
@Query("select u from User u where u.lastname = ?0 or u.firstname = ?1")
Optional<User> findByLastnameOrFirstname(String lastname, firstname);
파라미터에 순서에 따라 ?에 0 ~ n 까지의 숫자를 붙인다.
@Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
User findByLastnameOrFirstname(@Param("lastname") String lastname,
@Param("firstname") String firstname);
@Param 어노테이션을 이용해 sql에서 파라미터의 자리를 명시해준다.
Compiler flag
-parameters를 사용하고, 파라미터 이름을 일치시키면@Param을 생략할 수 있다.
@NativeQuery(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1")
Page<User> findByLastname(String lastname, Pageable pageable);
쿼리를 JPQL이 아닌 SQL로 작성하는 옵션이다.
JPQL에서 지원하지 않는 영역이 필요한 경우와 성능이 더 좋은 경우에 사용한다.
사전적 의미와 같이 엔티티와 관련된 이벤트(Create, Update, Delete 등)를 추적하고 기록하여 감사하는 것이다.
@EntityListeners(AuditingEntityListeners.class)
@MappedSuperclass
@Getter
public class BaseEntity {
...
@CreatedBy
private String createdBy;
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedBy
private String modifiedBy;
@LastModifiedDate
private LocalDateTime modifiedDate;
}
@EnableJpaAuditing
@SpringBootApplication
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication, args);
}
}
BaseEntity에 생성일자, 수정일자를 Auditing 하는 것을 예시로 들었다.
일반 Entity에도 Auditing 설정을 할 수 있지만, 중복 코드가 발생하고 관리가 어려울 가능성이 높다.
그렇기 때문에 Auditing 관련 Entity를 생성하고 다른 Entity에서 이를 상속받게 하는 것이 일반적이다.
작성자 정보와 수정자 정보를 등록하기 위해서는 AuditoAware<T>를 Bean으로 등록해야 한다.
public AuditorAware<String> auditorProvider() {
return new AuditorAware<String>() {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("system");
}
};
}
람다를 사용해 원래의 익명 클래스를 축약하여 표현했으며 작성자 및 수정자를 Optional 타입으로 반환하여 설정해준다.
주석처리한 부분은 SpringSecurity를 사용했을 때 사용자의 정보를 생성자 및 수정자로 설정했을 경우이다.
| 어노테이션 | 설명 |
|---|---|
@CreatedBy |
엔티티를 생성하는 사용자 정보 |
@CreatedDate |
엔티티 생성 일자 |
@LastModifiedBy |
엔티티를 수정한 사용자 정보 |
@LastModifiedDate |
엔티티 수정 일자 |
📌
이 어노테이션을 엔티티에 적용하게 되면 Insert 쿼리를 날릴 때 null 인 값은 제외하고 쿼리문이 만들어집니다
📌
이 어노테이션을 엔티티에 적용하게 되면 Update 쿼리를 날릴 때 null인 값은 제외하고 쿼리문이 만들어집니다.