Spring Boot - Spring Data JPA (with MySQL)
Spring Boot에서 Spring Data JPA를 사용하여 MySQL을 다루는 법을 알아보겠습니다.
1. Dependency 추가
1 | dependencies { |
2. 설정
1 | spring.jpa.hibernate.ddl-auto=create |
spring.jpa.hibernate.ddl-auto
의 값의 의미는 다음과 같다.
- none: 아무 것도 하지 않는다. (MySQL에서의 기본값)
- validate: DB와 엔티티의 스키마가 맞지 않으면 다른 부분을 출력하고 종료한다.
- update: 변경된 엔티티의 스키마를 적용한다.
- create: SessionFactory가 시작될 때, 기존의 테이블을 drop하고 새 테이블을 생성한다.
- create-drop: create의 동작 + SessionFactory가 종료될 때, 테이블을 drop 한다.
3. Entity 클래스 정의
1 |
|
- 컬럼 타입에 Integer, String 뿐만 아니라 Double, Char, Boolean등 다양한 Java Primitive Type을 사용할 수 있습니다.
- String은 컬럽 타입이
VARCHAR(255)
로 자동으로 지정됩니다. @Table
이나@Column
에서name
을 명시헤주지 않으면 해당 클래스나 멤버 변수의 이름을 snale_case로 변환한 것을 사용합니다.nullable
을 false로 지정해주지 않으면 true가 기본값입니다.columnDefinition
을 통해 컬럼 타입을 직접 지정해줄 수 있습니다. LocalDateTime, OffsetDateTime, ZonedDateTime은 별다른 지정이 없어도 DATETIME으로 타입이 지정됩니다. 하지만 기본으로 소수점 6자리를 사용하도록 설정되기 때문에 저는 저렇게 길이를 0으로 명시하는 편입니다.columnDefinition
뒤에default [값]
을 붙여서 기본값을 지정해줄 수 있습니다. 컬럼이 시간, 날짜와 관련된 타입일 경우CURRENT_TIMESTAMP
를 붙여서 레코드가 생성될 때의 시간을 기본값으로 할 수 있습니다.
3.1. Default가 정의된 컬럼 다루기
1 |
|
위와 같이 Default값이 설정되어 있는 컬럼에 아무 것도 넣지 않고 save를 하면 DB에는 기본값인 100이 아니라 NULL값이 저장되어 있습니다. 이는 Hibernate가 INSERT 구문을 생성할 때 아래와 같이 모든 값을 명시하기 때문입니다.
1 | INSERT INT user (name, score) VALUE ('John', NULL) -- Hibernate가 생성한 Query |
이 문제를 해결하기 위해 2가지 방법이 있습니다.
1) @DynamicInsert 사용하기
단순히 엔티티 클래스에 @DynamicInsert
어노테이션을 사용하면 됩니다.
@DynamicInsert
어노테이션은 값이 NULL인 컬럼을 INSERT 구문에 넣지 않는 기능을 합니다.
추가로 @DynamicUpdate
어노테이션도 존재하는데 이름에서 유추할 수 있듯이 값이 NULL인 컬럼을 UPDATE 구문에 넣지 않는 기능을 합니다.
1 |
|
아래와 같이 엔티티를 save한 후에도 DB에는 기본값이 저장되어 있으나, 엔티티 객체에서는 해당 컬럼의 값이 그대로 null로 남아있다는 단점이 있습니다.
1 | repository.save(entity); |
2) 엔티티 클래스 멤버 변수에 기본값 주기
@ColumnDefault
어노테이션을 사용하지 않고 단순히 Java 코드 단계에서 기본값을 정의합니다.
DB 스키마에서 기본값을 확인할 수 없는 단점이 있습니다.
1 |
|
3.2. NOT NULL과 Default를 동시에 정의하기
어떤 컬럼을 NOT NULL
로 선언하고 해당 컬럼에 기본값을 주고 싶은 경우가 종종있습니다.
하지만 아래와 같이 컬럼을 정의하고 해당 컬럼의 값을 넣지 않고 save를 하면 org.hibernate.PropertyValueException
이 발생합니다.
1 |
|
이 컬럼의 nullable 여부는 INSERT 구문을 생성하기 전에 확인하는데 default값은 INSERT 구문이 실행해야 집어넣어지기 때문입니다. 이 문제를 해결하기 위해서는 아래와 같이 2가지 방법이 있습니다.
1 | // 방법 1 |
columnDefinition
에서 컬럼의 타입과 기본값과 NOT NULL을 동시에 정의하는 방법- Java 코드에 직접 기본값을 정의
4. Repository 인터페이스 정의
1 | public interface UserRepository extends JpaRepository<UserEntity, Integer> { |
실수로 내용을 빼먹은 것이 아닙니다. 단순히 JpaRepository만 상속하면 준비는 끝났습니다. JpaRepository를 상속할 때 두번째 제너릭은 Entity의 @Id 컬럼의 타입을 넣어주면 됩니다.
5. 간단한 CRUD
5.1. SELECT
1 |
|
5.2. INSERT
1 |
|
repository에 save한 후 생성된 기본키를 가져올 수 있습니다.
5.3. UPDATE
1 | public void update() { |
5.4. DELETE
1 | public void delete() { |
6. 조건절이 있는 CRUD
6.1. 메서드명으로부터 쿼리 생성
1 | public interface UserRepository extends JpaRepository<UserEntity, Integer> { |
이렇게 단순히 UserRepository
인터페이스에 메서드를 추가해주는 것으로 검색 쿼리를 생성할 수 있습니다.
And, Or, Between, LessThan, Like 등 다양한 조건을 추가해줄 수 있습니다.
자세한 정보는
문서
를 참고해 주세요
6.2. @Query 어노테이션
하지만 위와 같은 방법으로는 SELECT 쿼리만 사용할 수 있을뿐만 아니라 소괄호를 통한 연산 우선순위를 적용하는 등 복잡한 쿼리를 할 수 없습니다. 이제부터 직접 쿼리문을 작성하는 방법을 소개하겠습니다.
1 | public interface UserRepository extends JpaRepository<UserEntity, Integer> { |
여기서 @Query
어노테이션 안에 있는 쿼리문은 일반적인 SQL이 아니라 JPQL이라는 Java 독자적인 쿼리 언어입니다.
여기서 테이블명이나 컬럼명 대신 엔티티 클래스 이름과 앤티티 클래스의 멤버 번수 이름을 사용합니다.
UPDATE나 DELETE같이 DB를 변경해야하는 경우에는 반드시 @Transactional
, @Modifying
어노테이션을 사용해야합니다.
자세한 사용방법은 여기를 참고해 주세요.
6.3. Example
하지만 위와같은 방법으로는 검색 조건의 수가 유동적인 경우에는 사용할 수 없습니다. 예시 엔티티를 생성해서 비교하는 벙법을 소개하겠습니다.
1 | UserEntity toCompare = new UserEntity(1, "John", null); |
예시 엔티티를 생성해서 Example.of
메서드로 예시를 생성하고 findAll
이나 findOne
메서드에 넘겨주면 됩니다.
예시 엔티티에서 값이 NULL아닌 모든 프로퍼티와 비교합니다.
ExampleMatcher
를 통해 문자열의 대소문자 구분 없음, 정규표현식, 포함관계의 비교를 할 수 있습니다.
자세한 사용방법은 여기를 참고해 주세요.
6.4. JpaSpecificationExecutor
하지만 위와같은 방법으로는 검색 조건의 수가 유동적이며 범위 검색이 있는 경우에는 사용할 수 없습니다. Repository에 JpaSpecificationExecutor를 상속하는 방법을 소개하겠습니다.
1 | public interface UserRepository extends JpaRepository<UserEntity, Integer>, JpaSpecificationExecutor<UserEntity> { |
먼저 Repository 인터페이스에 추가로 JpaSpecificationExecutor
를 상속합니다.
1 | public class UserSpecs { |
먼저 각각의 조건 항을 정의하는 메서드를 만듭니다.
여기서 builder
는 CriteriaBuilder
라는 클래스인데 equal, between 뿐만 아니라 여러분이 생각할 수 있는 모든 것이 다 있습니다.
자세한 정보는 문서를 확인해주세요.
1 | Specification<UserEntity> spec = Specification.where(null); |
위와같이 파라미터로 넘어온 map에 키가 있는지 없는지 체크하면서 조건을 추가해줄 수 있습니다. 여기서 and 메서드말고 or, not 메서드도 사용할 수 있습니다.
7. 정렬과 페이징처리
7.1. Repository의 기본 메서드
1) 정렬
1 | List<UserEntity> list = repository.findAll(Sort.by(Sort.Direction.ASC, "score")); |
Sort
클래스를 통해 정렬을 할 수 있습니다.
2) 페이징
1 | List<UserEntity> list = repository.findAll(PageRequest.of(0, 10)); |
PageRequest
클래스를 통해 페이징 처리를 할 수 있습니다.
페이지는 0부터 시작합니다.
세번째 파라미터로 Sort 객체를 넘겨줌으로써 페이징 처리와 정렬을 동시에 할 수 있습니다.
자세한 정보는
PageRequest 공식 문서,
Sort 공식 문서
를 참고해 주세요.
7.2. 메서드명으로부터 쿼리 생성
1) 정렬
1 | public interface UserRepository extends JpaRepository<UserEntity, Integer> { |
- 메서드 명뒤에
OrderBy[프로퍼티이름][Desc|Asc]
를 붙이면 됩니다. - 아니면
Sort
객체를 맨 끝에 파라미터로 추가하면 됩니다.
2) 페이징
1 | public interface UserRepository extends JpaRepository<UserEntity, Integer> { |
Pageable 객체를 맨 끝에 파라미터로 추가하면 됩니다. 반환 타입도 위의 3개중에 아무거나 하나 고르면 JPA가 다 알아서 합니다.
Slice
hasNext
, hasPrevious
, isFirst
, isLast
의 메서드를 통해 다음, 이전 페이지가 존재하는지, 현재 페이지가 처음이나 마지막인지 알 수 있습니다.
이외에도 다양한 기능을 제공하니 자세한 정보는
문서
를 확인해 주세요.
Page
위의 Slice를 상속받은 것이기 때문에 Slice의 모든 기능을 쓸 수 있습니다.
내부적으로 COUNT 함수를 실행시켜서 검색 결과의 총 갯수를 구합니다.
COUNT 함수의 오버헤드가 큰 경우 별로 추천하지 않습니다.
getTotalElements
, getTotalPages
메서드를 통해 전체 결과 갯수, 전체 페이지 수를 구할 수 있습니다.
자세한 정보는
문서
를 확인해 주세요.
7.3. @Query 어노테이션
1 | public interface UserRepository extends JpaRepository<UserEntity, Integer> { |
위에서 한 것 처럼 Sort
나 Pageable
객체를 파라미터에 추가하고 반환 타입을 List
, Slice
, Page
로 바꾸면 JPA가 다 알아서 합니다.
7.4. Example
1 | List<UserEntity> users = repository.findAll(ex, PageRequest.of(0, 10)); |
메서드 파라미터 뒤에 Sort
나 Pageable
객체를 넘겨주면 됩니다.
7.5. JpaSpecificationExecutor
1 | List<UserEntity> users = repository.findAll(spec, PageRequest.of(0, 10)); |
마찬가지로 메서드 파라미터 뒤에 Sort
나 Pageable
객체를 넘겨주면 됩니다.
8. 기본 실행 SQL 파일
resources 폴더 루트에 import.sql
이라는 파일이 있으면 Hibernate는 Entity를 통한 Table 자동 생성이 끝난 후 이 파일을 실행합니다.
보통 실행때마다 데이터를 Drop하는 설정에서 요긴하게 쓰입니다.
Appendix
dependency에 JPA를 포함했지만 JPA 설정을 건너뛰고 싶은 경우
1 | spring.autoconfigure.exclude= \ |
Spring Boot - Spring Data JPA (with MySQL)