기존에 memory db로 구현했던 프로젝트를 JPA를 사용해 MySQL을 사용하도록 변경해보았다.
book_store라는 스키마를 새로 생성하고, 안에 user, book 테이블을 만들어 사용한다.
resources 폴더 안의 application.properties 파일을 .yaml로 바꿔 작성해주었다.
spring:
jpa:
show-sql: true
properties:
format_sql: true
dialect: package org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: validate
datasource:
url: jdbc:mysql://localhost:3306/book_store?useSSL=false&useUnicode=true&allowPublicKeyRetrieval=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root1234!!
지난 번에 JPA를 테스트해볼 때 썼던 파일과 db이름만 빼고 동일하다.
그 다음에는, 이 프로젝트를 생성할 때 포함하지 않았던 JPA, MySQL의 dependency를 build.gradle에서 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
user 패키지로 가서, 직접 만들었던 SimpleDataRepository를 상속받았던 UserRepository를 본다. 구현부를 없애고, 상속을 해제하고, 인터페이스로 바꿔준다.
// UserRepository
public interface UserRepository extends JpaRepository<UserEntity,Long> {}
UserEntity로 넘어가서 Entity 상속을 해제하고, @Entity(name="테이블명")을 추가한다. 또, 상속을 해제했기 때문에 따로 id 변수를 추가하고, 기본키 애너테이션과 DB 값 위임 애너테이션을 달아준다.
// UserEntity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity(name="user")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int score;
}
BookRepository, BookEntity도 마찬가지로 수정해준다.
// BookRepository
public interface BookRepository extends JpaRepository<BookEntity, Long> {
}
// BookEntity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity(name="book")
public class BookEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String category;
private BigDecimal amount;
}
UserService에서 약간 충돌이 발생하긴 하지만, 일단은 주석처리하고 서버가 켜지는지 확인한다. 잘 되는 것을 확인했으면 충돌이 났던 UserService를 수정한다.
UserRepository가 인터페이스화되면서 따로 만들었던 findAllByScoreGreaterThan의 구현부를 삭제해서 문제가 발생한다.
이 메서드가 받아오는 것은 쿼리문으로 보면 SELECT * FROM book_store.user WHERE score > (입력값); 처럼 쓸 수 있다.
JPA 쿼리메서드는 따로 메서드를 생성해줄 필요 없이 메서드 이름으로 쿼리를 작성하는 것과 같다. CamelCase 기준으로 메서드 명이 파싱되며, findAllBy는 SELECT, 그 다음에 오는 것이 변수명, 그 뒤에 조건이 붙는다.
// UserRepository
publicByScoreGreaterThan(int score);
위의 findAllByScoreGreaterThan은 user 입력받은 숫자보다 높은 Score를 찾아 반환한다. 만약 이상의 Score를 받아오고 싶다면 findAllByScoreGreaterThanEqual이라고 붙여준다.
입력받는 값의 변수명은 조건이 되는 칼럼명과 같을 필요는 없다.
바뀐 내용을 UserService에서도 수정해준다.
// UserService
public List<UserEntity> filterScore(int score){
return userRepository.findAllByScoreGreaterThan(score);
}
만약 min값보다 크고 max 값보다 작은 score를 가진 데이터를 찾고 싶다면, 다음과 같이 쿼리 메서드를 작성할 수 있다.
public List<UserEntity> findAllByScoreGreaterThanAndScoreLessThan(int min, int max);
공식 문서를 참고해서 쿼리 메서드를 사용하면 된다.
JPA를 사용하면 쿼리 메서드를 추천하지만, JPA에서도 테이블을 조인해야 하거나 쿼리문이 복잡해질 경우에는 직접 쿼리를 작성할 수도 있다. 먼저 JPQL을 사용하는 방법이 있다. 편의상 위의 쿼리메서드와 같은 내용으로 작성하였다.
@Query(
"select u from user u where u.score >= ?1 AND u.score <= ?2"
)
List<UserEntity> score(int min, int max);
쿼리 애너테이션이 받는 쿼리의 select 뒤에 오는 u는 Entity를 말한다. 따라서 이는 select * 라고 보면 되고, 테이블명 뒤에 나오는 u는 테이블 약칭이다. ?1과 ?2는 입력받은 파라미터를 차례로 가리킨다.
다음으로는 네이티브 쿼리문을 작성하는 방법이다. 똑같이 @Query 애너테이션을 붙이고 nativeQuery값을 true로 주면 sql문을 직접 작성할 수 있다.
@Query(
value = "select * from user as u where u.score >= :min AND u.score <= :max",
nativeQuery = true
)
List<UserEntity> score(
@Param(value="min")int min,
@Param(value="max")int max);
파라미터가 많아지면, @Param을 사용하여 바인딩시켜줄 수도 있다.