[ 살펴보기 ] Spring Data JPA - Basics
![[ 살펴보기 ] Spring Data JPA - Basics](https://cdn.hashnode.com/res/hashnode/image/upload/v1732980754625/51deff64-760f-4090-bf9d-aaa05b941bd5.jpeg)
Spring Data JPA는 hibernate와 같은 JPA provider에 abstraction layer를 추가해 entity 관리와 database table에 대한 query 작업을 보다 쉽게 처리할 수 있게 해준다.
Spring Data JPA에서 제공하는 JpaRepository interface를 통해를 database table을 조작하는 방법을 살펴보자. 테스트를 위해 database는 postgresql를 사용하며 build tool은 gradle을 사용한다. build.gradle에 추가한 dependencies는 다음과 같다.
build.gradle
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
그리고 application의 설정 파일을 통해 database connection 정보를 설정해준다. 아래 예제에선 database이름이 testdb, db username이 postgres, password가 00000이라고 가정한다.
resources/application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/testdb
username: postgres
password: 00000
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
Entity는 database table와 mapping되는 객체를 말한다. 다음 구조를 갖는 table 있다고 가정해보자.
CREATE TABLE users (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY
name VARCHAR(30)
email VARCHAR NOT NULL
)
위의 Users table과 mapping되는 entity class는 다음과 같이 생성할 수 있다.
User.class
package com.jd.testapp.users;
import jakarta.persistence.*;
import lombok.*;
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Setter
@Entity
@Table(name="users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", length=30)
private String name;
@Column(name = "email", nullable = false, columnDefinition="VARCHAR")
private String email;
}
위의 예제는 id, name, email column으로 구성된 users table과 mapping될 entity class를 구현하고 있다. @Entity annotation을 통해 해당 객체를 JPA entity로 지정하고 @Table annotation을 통해 entity가 mapping되는 table 이름을 설정한다.
@Id annotation을 통해 객체의 id가 될 field를 지정하고 @GeneratedValue annotation을 통해 기본 키 생성 방식을 지정한다. 위의 예제처럼 IDENTITY로 설정하면 새로운 data가 추가될 때 id field는 database에서 자동으로 생성한다.
Entity class의 field를 선언할 때 위의 예제와 같이 @Column annotation을 통해 tabel에 생성될 column에 대한 상세 정보를 설정할 수 있다. name option을 위의 예제와 같이 명시적으로 지정하지 않으면 class에 선언된 field의 동일한 이름이 사용된다.
String type의 field은 VARCHAR type으로 변환되며 length를 지정해주지 않으면 default length로 VARCHAR(255)가 적용된다. 만약 length limit이 없는 VARCHAR type으로 data type을 설정하고 싶다면 email field와 같이 columnDefinition option을 사용한다.
다음 예제와 같이 columnDefinition option을 통해 hibernate가 table을 생성할 때 사용할 column type을 직접 설정할 수 있다.
@Column(name = "email", columnDefinition = "TEXT")
private String email;
@Column(name = "city", columnDefinition = "TEXT DEFAULT 'seoul'")
private String city;
@Column(name = "created_at", columnDefinition = "TIMESTAMP WITH TIME ZONE")
private OffsetDateTime createdAt;
@Getter, @Setter, @AllArgsConstructor, @NoArgsConstructor annotation은 class의 constructor와 class에 선언된 field를 기준으로 getter, setter method을 생성하기 위해 lombok에서 제공하는 annotation이다.
위의 entity class를 추가하고 spring application을 실행해보면 application.yml 파일에서 jpa.hibernate.ddl-auto property를 update로 설정했기에 database에 users라는 table이 새롭게 생성되는 것을 확인할 수 있다. update 상태로 두면 추후 entity class 변경으로 인해 자칫 데이터가 유실될 수도 있으므로 table이 생성된 걸 확인 했으면 jpa.hibernate.ddl-auto property는 다시 none으로 변경해주자.
이제 User entity를 통해 users table과 interaction할 수 있도록 repository를 구성해보자.
UserRepository.java
package com.jd.testapp.users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
위의 예제에서 users table과 interaction 하기위해 UserRepository interface를 구현하고 있다. 구현하는 interface는 CrudRepository, ListCrudRepository, JpaRepository 등을 extends 함으로써 crud 작업에 필요한 기본적인 method를 상속받는다.
위의 예제에선 JpaRepository interface를 extends하고 있으며 첫 번째 generic으로 Entity class 그리고 두 번째 generic으로 Entity class의 id로 사용된 field의 data type을 전달 한다.
그리고 예제와 같이 특정 repository에 대한 interface를 구성하면 해당 interface를 실제로 구현하는 class를 별로도 작성하지 않아도 spring data jpa가 application을 실행할 때 구현 class를 자동으로 생성해준다.
이제 위의 UserRepository를 통해 user table에 대한 crud 작업을 할 수 있다. 테스트를 위해 user 관련 business logic을 담당하는 UserService class를 생성해 다음과 같이 UserRepository를 주입하면 해당 repository가 제공하는 method를 통해 users table에 대한 sql query를 수행할 수 있다.
Find
Repository를 통해 table의 데이터를 조회하고 싶다면 find 관련 method를 사용할 수 있다. 아래 예제는 findAll method를 통해 user table의 모든 데이터를 조회하는 예제다.
UserService.java
package com.jd.testapp.users;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
UserRepository userRepository;
public List<User> getUserAll() {
List<User> user = userRepository.findAll();
return user;
}
...
}
UserRepository의 findAll method는 다음 sql statement와 같다.
SELECT * FROM users;
만약 특정 id value를 기준으로 user data를 조회하고 싶다면 다음과 같이 findById method를 사용한다.
...
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
...
}
UserRepository의 findById method는 다음 sql statement와 같은 역할을 한다. WHERE condition에 전달되는 값은 findById method가 전달 받는 id parameter 값이다.
SELECT * FROM users WEHRE id = 1;
만약 id가 아니라 entity class에 선언된 다른 field를 기준으로 search를 수행하고 싶다면 UserRepository interface에 custom method를 추가해야 한다.
...
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String name);
}
특정 entity를 기준으로 search를 수행하는 custom method를 추가할 때는 위와 같이 findBy + entity field name 형식으로 추가해준다. Repository에 추가한 custom method는 이제 아래와 같이 repository 객체를 통해 사용할 수 있다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserByName(String name) {
return userRepository.findByName(name);
}
...
}
Save
Repository를 통해 table의 데이터를 추가하고 싶다면 save method를 사용할 수 있다.
...
public User createUser(CreateUserDto user){
User newUser = new User();
newUser.setName(user.getName());
newUser.setEmail(user.getEmail());
newUser.setAddress(user.getAddress());
return userRepository.save(newUser);
}
...
UserRepository의 save method는 다음 sql statement와 같다.
INSERT INTO users (name, email) VALUES ('name value', 'email value');
Update
Repository를 통해 table의 데이터를 수정하고 싶다면 다음과 같이 기존 data를 담고 있는 user 객체를 전달 받은 dto 객체의 값으로 업데이트 한 뒤 save method를 통해 업데이트를 수행한다.
UserService.java
...
import org.springframework.transaction.annotation.Transactional;
...
@Transactional
public User updateUser(Long id, UpdateUserDto user) {
Optional<User> findUser = userRepository.findById(id);
if (findUser.isPresent()) {
User updateUser = findUser.get();
updateUser.setName(user.getName());
updateUser.setAddress(user.getAddress());
updateUser.setEmail(user.getEmail());
return userRepository.save(updateUser);
} else {
throw new RuntimeException("User not found");
}
}
...
UserRepository의 update method는 다음 sql statement와 같다. WHERE condition에 전달되는 값은 updateUser method가 전달 받는 id parameter이며 나머지 수정 값 역시 UpdateUserDto parameter로 전달 받는 값이 된다.
UPDATE users
SET address = 'new address',
name = 'new name',
email = 'new email'
WHERE id = 2;
만약 patch method request에 대한 partial update를 진행해야 한다면 다음과 같이 null 체크를 통해 dto 객체에 포함된 값만 설정하여 update를 진행한다.
UserService.java
...
@Transactional
public User partialUpdateUser(Long id, UpdateUserDto user) {
System.out.println(" :::: SERVICE - partialUpdateUser ::::: ");
Optional<User> optionalUser = userRepository.findById(id);
if (optionalUser.isPresent()) {
User updateUser = optionalUser.get();
if(user.getEmail() != null) {
updateUser.setEmail(user.getEmail());
}
if(user.getName() != null) {
updateUser.setEmail(user.getName());
}
return userRepository.save(updateUser);
} else {
throw new RuntimeException("User not found");
}
}
...
만약 이러한 null check가 비효율적이라 생각된다면 MapStruct라는 object mapper library를 통해 위의 예제와 같은 null check없이 partial update를 적용할 수 있다. 우선 build.gradle의 dependencies에 다음 dependencies를 추가하고 gradle을 reload 해준다.
...
dependencies {
...
implementation 'org.mapstruct:mapstruct:1.6.3'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
}
그리고 다음과 같이 UserMapper 파일을 생성하고 Mapper interface를 생성해준다.
UserMapper.java
package me.james.testapp.users;
import me.james.testapp.users.dto.UpdateUserDto;
import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy;
@Mapper(componentModel = "spring")
public interface UserMapper {
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void partialUpdate(UpdateUserDto updateDTO, @MappingTarget User user);
}
위의 mapper interface를 추가하고 다음과 같이 service에 주입하여 사용할 수 있다.
UserService.java
...
@Service
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
@Autowired
public UserService(UserRepository userRepository, UserMapper userMapper) {
this.userRepository = userRepository;
this.userMapper = userMapper;
}
@Transactional
public User partialUpdateUser(Long id, UpdateUserDto user) {
Optional<User> optionalUser = userRepository.findById(id);
if (optionalUser.isPresent()) {
User updateUser = optionalUser.get();
userMapper.partialUpdate(user, updateUser);
return userRepository.save(updateUser);
} else {
throw new RuntimeException("User not found");
}
}
...
Mapper interface의 nullValuePropertyMappingStrategy option을 NullValuePropertyMappingStrategy.IGNORE로 설정했기에 mapping source 객체인 Dto 객체의 property 중 값이 null이거나 presence 검사에서 absent로 평가되는 property는 null로 설정하는 대신 mapping target 객체의 기존 값을 그대로 사용한다.
즉, 위의 예제에선 findById로 찾은 user 객체에 updateDto 객체를 mapping할 때 Dto 객체에서 값이 null이거나 presence check에서 absent로 평가되는 property는 제외하고 실제 값이 존재하는 property만 적용된다. ( 위의 예제에선 Dto 객체의 field name과 User 객체의 field name이 아래와 같이 동일하다고 가정한다 : name, email )
다른 이름을 가진 field로 이루어진 객체를 서로 mapping하기 위해선 추가 설정이 필요하며 MapStruct에 대한 보다 자세한 사항은 별도의 포스트를 통해 살펴보도록 하자.
User.java
...
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
}
UpdateUserDto.java
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class UpdateUserDto {
private String name;
private String email;
}
Delete
Repository를 통해 table의 데이터를 삭제하고 싶다면 deleteById method를 사용할 수 있다.
...
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
...
UserRepository의 deleteBy method는 다음 sql statement와 같다. WHERE condition에 전달되는 값은 deleteUser method가 전달 받는 id parameter 값이다.
DELETE FROM users WHERE id = 3;
![[ 살펴보기 ] RDB - Relationships](https://cdn.hashnode.com/res/hashnode/image/upload/v1739711556668/48dc9e84-a621-42aa-9c9f-5fc5c436f0ec.jpeg)
![[ 살펴보기 ] MySQL - Data types](https://cdn.hashnode.com/res/hashnode/image/upload/v1739593589113/530f8704-4d27-42c9-a451-bb5c63150b99.jpeg)
![[ 살펴보기 ] TypeORM - Transactions, Migration](https://cdn.hashnode.com/res/hashnode/image/upload/v1739106042581/980b8133-61d4-406a-a026-65be9c28eace.jpeg)
![[ 살펴보기 ] TypeORM - Relations](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666874402/b688bd0b-b6bb-4f43-87d8-c1b46b59f1b7.jpeg)
![[ 살펴보기 ] TypeORM - Basics](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666803591/bef5df17-7dc7-4123-ae55-004d5042df39.jpeg)