DongDD's IT

[Spring] Spring JPA 2 - Spring Data JPA, Spring Data JPA CRUD 본문

프로그래밍/Spring

[Spring] Spring JPA 2 - Spring Data JPA, Spring Data JPA CRUD

DongDD 2019. 4. 28. 17:39

[Spring] Spring JPA 2 - Spring Data JPA, Spring Data JPA CRUD



Spring Data JPA


Spring Data


- 데이터에 접근하는 코드를 줄이기 위한 스프링 프로젝트

- DDD(Domain-Driven Design, 도메인 주도 설계)에서의 데이터 접근 계층 구성 요소인 Repository 권장

- Repository 구현은 데이터와 관련된 아키텍쳐/제품에 따라 다름(각 종류별로 프로젝트가 만들어짐)


Spring Data JPA


- JPA를 이용해 데이터에 접근하기 위한 라이브러리

- Respository 인터페이스를 작성하여 간단하게 Entity를 참조/갱신할 수 있음


JPA Config


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.repository")
public class JPAConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .setScriptEncoding("UTF-8").build();
    }
 
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter bean = new HibernateJpaVendorAdapter();
        bean.setDatabase(Database.H2);
        bean.setGenerateDdl(true);
        return bean;
    }
 
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
DataSource dataSource, 
JpaVendorAdapter jpaVendorAdapter) {
        LocalContainerEntityManagerFactoryBean bean 
= new LocalContainerEntityManagerFactoryBean();
        bean.setDataSource(dataSource);
        bean.setJpaVendorAdapter(jpaVendorAdapter);
        bean.setPackagesToScan("com.model");
        return bean;
    }
 
    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}
cs

- Java JPA를 사용하기 위해 설정했던 config를 그대로 사용

- @EnableJpaRepositories(basePackages = "com.repository") 지정하여 @Repository를 탐색



Spring Data JPA Architecture


내부 처리


- Java의 JPA에서 EntityManager의 API 를 직접 호출한 것과 달리, Spring JPA에서는 framework 내부에서 자동으로 호출(사용자는 Spring JPA API 호출)

- Entity의 repository interface를 작성하고 JpaRespository를 상속받아 사용

- JpaRepository는 SimpleJpaRepository가 제공하는 API를 인터페이스화한 클래스

- Spring Data JPA는 DI container가 초기화되는 시점에 생성한 repository interface를 SimpleJpaRepository로 처리를 위임하는 proxy를 생성하고 bean으로 등록

- 실제 EntityManager의 API는 SimpleJpaRepository나 그 상위 클래스에서 호출

1

1. Repository

- 사용자가 JpaRepository를 상속받아 구현


2. RepositoryProxy

- Repository 생성 시, Spring Data JPA가 생성하는 proxy class

- Repository에 대해 SimpleJpaRepository 처리를 위임

- Service에서 호출


3. JpaRepository

- SimpleJpaRepository의 구현체


4. SimpleJpaRepository

- 실제로 EntityManager의 API를 호출

- RepositoryProxy 메소드가 호출될 때, 위임받아 처리


JpaRepository


- JpaRepository를 상속한 Repository 클래스를 생성하여 data 접근


1. Repository 작성

1
2
3
@Repository
public interface TestJpaRepository extends JpaRepository<Entity, PK> {

}
cs

- JpaRepository<Entity, PK>를 상속받은 interface 클래스 생성


2. JpaRepository API

- save : entity를 저장하는 메소드(insert, update)

- flush : EntityManager의 내용을 DB에 동기화하는 메소드

- saveAndFlush : entity에 대한 저장 작업 후 flush

- delete : entity를 삭제하는 메소드(delete)

- deleteAll : DB의 모든 레코드를 persistence context로 읽어와 삭제하는 메소드

- deleteInBatch : persistence context로 읽어오지 않고 DB의 모든 레코드를 삭제하는 메소드

- findOne : primary key로 DB에서 Entity를 찾아오는 메소드(select)

- findAll : 모든 entity를 찾아오는 메소드(select)

- exists : primary key에 해당하는 entity가 존재하는 확인하는 메소드

- count : entity의 갯수를 확인하는 메소드


예외 처리


- Spring Data JPA에서 제공

- Database 접근 시 발생하는 에러는 시스템 오류로 처리함


1. DataAccessException

- @Repository에서 발생한 예외를 DataAccessException으로 변환하는 구조 제공

- Data에 관련된 예외만 DataAccessException에서 처리


1) 예외 클래스

- DuplicateKeyException : 데이터 무결성 조건 위반 시 발생

- OptimisticLockingFailureException : optimistic lock 실패 시 발생(OptimisticLockException 발생 시)

- PessimisticLockingFailureException : pessimisic lock 실패 시 발생(PessimisticLockException 발생 시)



Repository 사


Repository CRUD


1. Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entity
@Table(name = "user")
public class User {
    @Id
    @Column(name = "id")
    private String id;
  
    @Column(name = "name")
    private String name;
  
    public User() {
    }
  
    public String getId() {
        return id;
    }
  
    public void setId(String id) {
        this.id = id;
    }
  
    public String getName() {
        return name;
    }
  
    public void setName(String name) {
        this.name = name;
    }
}
cs

- id와 name을 갖는 entity 생성


2. Repository

1
2
3
4
@Repository
public interface UserRepository extends JpaRepository<User, String> {
 
}
cs

- @Repository 지정

- JpaRepository<User, String>을 상속받는 interface 클래스 생성


3. Create(POST)

1) Controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping(value = "/springjpa")
public class SpringJpaController {
    @Autowired
    private SpringJpaService springJpaService;
 
    @RequestMapping(value = "/", method = RequestMethod.POST)
    public void createUser(@RequestBody User user) {
        springJpaService.createUser(user);
    }
}
cs

- POST 메소드로 요청을 받고 SpringJpaService의 createUser 호출


2) Service

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class SpringJpaService {
    @Autowired
    private UserRepository userRepository;
 
    public void createUser(User user) {
        Optional<User> exist = userRepository.findById(user.getId());
        if(exist.isPresent()) {
            throw new TestException(HttpStatus.CONFLICT, "존재하는 ID 입니다.");
        }
        userRepository.save(user);
    }
}
cs

- UserRepository의 findById로 Entity를 읽어옴

- Optional로 반환되어 존재하는지 확인하고 있을 시, Exception

- 없을 경우, save로 create


3) 결과

- 성공

17

- 실패

18


4. Read(GET)

1) Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping(value = "/springjpa")
public class SpringJpaController {
    @Autowired
    private SpringJpaService springJpaService;
 
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public User getUserOne(@PathVariable String id) {
        return springJpaService.getUserOne(id);
    }
 
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public List<User> getUserAll() {
        return springJpaService.getUserAll();
    }
}
cs

- 하나의 user를 가져오는 핸들러와 모든 user를 가져오는 handler


2) Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class SpringJpaService {
    @Autowired
    private UserRepository userRepository;
 
    public User getUserOne(String id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new TestException(HttpStatus.NOT_FOUND,
                "없는 ID 입니다."));
    }
 
    public List<User> getUserAll() {
        List<User> users = userRepository.findAll();
        return users;
    }
}
cs

- UserRepository의 findById로 Entity를 읽어옴

- 받은 Optional로 존재하지 않는다면 Exception

- 있을 경우, get으로 User 반환

- UserRepository의 findAll로 모든 Entity를 읽어오고 반환


3) 결과

- GetOne(성공)

19

- GetOne(실패)

20

- GetAll

21


5. Update(PUT)

1) Controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping(value = "/springjpa")
public class SpringJpaController {
    @Autowired
    private SpringJpaService springJpaService;
 
    @RequestMapping(value = "/", method = RequestMethod.PUT)
    public void updateUser(@RequestBody User user) {
        springJpaService.updateUser(user.getId(), user.getName());
    }
}
cs

- PUT 메소드로 요청을 받고 SpringJpaService의 updateUser 호출


2) Service

1
2
3
4
5
6
7
8
9
10
11
@Service
public class SpringJpaService {
    @Autowired
    private UserRepository userRepository;
 
    public void updateUser(String id, String name) {
        User user = getUserOne(id);
        user.setName(name);
        userRepository.save(user);
    }
}
cs

- getUserOne 메소드를 호출하여 User를 반환받음

- User가 존재할 시, name을 요청 받은 name으로 변환한 후 UserRepository의 save를 사용하여 update


3) 결과

- update

22

- update 내용 확인(get)

23


6. Delete(DELETE)

1) Controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping(value = "/springjpa")
public class SpringJpaController {
    @Autowired
    private SpringJpaService springJpaService;
 
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public void deleteUser(@PathVariable String id) {
        springJpaService.deleteUser(id);
    }
}
cs

- DELETE 메소드로 요청 받고 SpringJpaService의 deleteUser 호출


2) Service

1
2
3
4
5
6
7
8
9
10
@Service
public class SpringJpaService {
    @Autowired
    private UserRepository userRepository;
 
    public void deleteUser(String id) {
        User user = getUserOne(id);
        userRepository.delete(user);
    }
}
cs

- getUserOne 메소드를 호출하여 User를 반환받음

- User가 존재할 시, UserRepository의 delete를 사용하여 해당 Entity를 delete


3) 결과

- delete

24

- delete 결과 확인(get)

25


7. Controller(전체)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RestController
@RequestMapping(value = "/springjpa")
public class SpringJpaController {
    @Autowired
    private SpringJpaService springJpaService;
 
    @RequestMapping(value = "/", method = RequestMethod.POST)
    public void createUser(@RequestBody User user) {
        springJpaService.createUser(user);
    }
 
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public User getUserOne(@PathVariable String id) {
        return springJpaService.getUserOne(id);
    }
 
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public List<User> getUserAll() {
        return springJpaService.getUserAll();
    }
 
    @RequestMapping(value = "/", method = RequestMethod.PUT)
    public void updateUser(@RequestBody User user) {
        springJpaService.updateUser(user.getId(), user.getName());
    }
 
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public void deleteUser(@PathVariable String id) {
        springJpaService.deleteUser(id);
    }
}
cs


8. Service(전체)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Service
public class SpringJpaService {
    @Autowired
    private UserRepository userRepository;
 
    public void createUser(User user) {
        Optional<User> exist = userRepository.findById(user.getId());
        if(exist.isPresent()) {
            throw new TestException(HttpStatus.CONFLICT, "존재하는 ID 입니다.");
        }
        userRepository.save(user);
    }
 
    public User getUserOne(String id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new TestException(HttpStatus.NOT_FOUND,
                "없는 ID 입니다."))
    }
 
    public List<User> getUserAll() {
        List<User> users = userRepository.findAll();
        return users;
    }
 
    public void updateUser(String id, String name) {
        User user = getUserOne(id);
        user.setName(name);
        userRepository.save(user);
    }
 
    public void deleteUser(String id) {
        User user = getUserOne(id);
        userRepository.delete(user);
    }
}
cs


JPQL


- JPA에서 사용했던 것처럼 Spring Data JPA에서도 Query를 작성하여 사용할 수 있음

- 일반적인 Repository select 시, 반환 값이 Optional이지만 생성하여 사용할 경우 원하는대로 지정할 수 있음


1. @Query

- @Query annotation의 속성으로 query문을 입력하고 해당 query문이 동작하게 할 method 지정

- JPA와 달리 LIKE 절에 "%" 와일드카드 사용 가능

- Jpql에 SpEL을 포함할 수 있음

- 일반 쿼리 처럼 사용하고 싶은 경우, @Query의 속성에 "nativeQuery = true"를 설정

1
2
3
4
5
@Repository
public interface UserRepository extends JpaRepository<User, String> {
    @Query("SELECT u FROM User u WHERE u.name = :name")
    List<User> getAllByName(@Param("name"String name);
}
cs  

- jpql을 @Query안에 작성하고 method에 지정

- 결과

26


2. Method name

- method의 이름을 보고 JPA가 판단하면 query를 생성

1
2
3
4
@Repository
public interface UserRepository extends JpaRepository<User, String> {
    List<User> findByNameOrderByIdDesc(String name);
}
cs

- find/read/query/count/get By으로 시작하는 method를 생성하는 경우, Spring JPA에서 select문의 jpql을 생성

- 일반 쿼리문처럼 여러 가지 설정 값을 줄 수 있음

- 결과

27

배타 제어


- JPA에서 사용했던 것과 마찬가지로 Spring JPA에서도 배타 제어를 사용할 수 있음

1
2
3
4
5
@Repository
public interface UserRepository extends JpaRepository<User, String> {
    @Lock(LockModeType.OPTIMISTIC)
    List<User> findByNameOrderByIdDesc(String name);
}
cs

- @Lock으로 설정한 modetype을 설정하고 method에 지정


Custom Repository


- jpql로 일일이 query문을 설정해도 되지만 custom repository를 생성하여 사용할 수 있음

- JpaRepository와 같은 custom interface 생성

- custom interface의 구현체를 생성하고 method 구현

- 해당 custom interface를 JpaRepostiory와 함께 상속받아 사용


Auditing


- Spring Data가 제공하는 Database의 Data에 대한 audit 정보를 남기기 위한 기능


1. Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
@Table(name = "user")
@EntityListeners(AuditingEntityListener.class)
public class User {
    @Id
    @Column(name = "id")
    private String id;
 
    @Column(name = "name")
    private String name;
 
    @CreatedDate
    @Column(name = "created_date")
    private LocalDate createdDate;
 
    @CreatedBy
    @Column(name = "created_by")
    private String createdBy;
}
cs

- audit 기능을 사용하기 위해 Spring data가 제공하는 event listener 등록

- @EntityListener로 AuditingEntityListener를 등록

- @CreateBy, @CreatedDate, @LastModifiedBy, @LastModifiedDate와 같이 audit하고 싶은 데이터 항목을 변수에 지정

- audit가 적용되는 변수에 자동으로 값을 넣고 싶은 경우, AuditorAware interface의 구현체를 구현하고 Bean으로 등록


2. AuditorAware

- audit가 적용되는 변수에 임의의 값을 설정할 수 있음

1
2
3
4
5
6
public class UserAuditorAware implements AuditorAware<String> {
    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.of("tester");
    }
}
cs

- LocalDate의 경우, 자동으로 들어감

- CreateBy, LastModifiedBy는 auditoraware를 생성하여 어떤 값을 넣을 지 구현해야함


3. Config

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.repository")
@EnableJpaAuditing
public class JPAConfig {
    @Bean
    public AuditorAware<String> auditorAware() {
        return new UserAuditorAware();
    }  
}
cs

- audit 기능을 사용하기 위해 @EnableJpaAuditing 지정

- 생성한 UserAuditorAware을 bean으로 등록


4. 결과

- create(POST)

28

- create 후 결과(GET)

29


'프로그래밍 > Spring' 카테고리의 다른 글

[Spring] Spring Boot  (0) 2019.05.04
[Spring] Spring MyBatis  (0) 2019.05.04
[Spring] Spring JPA 1 - JPA, JPA CRUD  (0) 2019.04.28
[Spring] Spring Security - 인증, 인가, CSRF, Test  (0) 2019.04.20
[Spring] Spring Test  (0) 2019.04.20
Comments