DongDD's IT

[Spring] Spring JPA 1 - JPA, JPA CRUD 본문

프로그래밍/Spring

[Spring] Spring JPA 1 - JPA, JPA CRUD

DongDD 2019. 4. 28. 17:16

[Spring] Spring JPA 1 - JPA, JPA CRUD



JPA(Java Persistence API)


ORM/JPA


- 자바 표준 ORM(Object Relational Mapping)


1. ORM(Object Relational Mapping)

- RDB에 데이터를 읽고 쓰는 것과 같이 객체에 데이터를 읽고 쓰는 방식의 기술

- 객체지향언어와 객체 지향이 아닌 RDB의 격차를 메워줌

- 객체에 접근하기 위한 코드로 DB table에도 접근할 수 있게 해줌


2. JPA(Java Persistence API)

- 자바 표준 ORM

- Java EE의 구성 요소와 결합도가 낮음

1
2
3
4
5
6
7
8
9
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
 
    public TestModel getId(String id) {
        return entityManager.find(TestModel.class, id);
    }
}
cs

- @PersistenceContext를 사용하여 EntityManager 의존성 주입

- EntityManager의 find를 사용하여 TestModel 객체를 찾음

- JdbcTemplate과 달리 SQL문을 기술할 필요가 없고, 객체와 table을 mapping해줌


3. EntityManager 설정

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
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

- EntityManager를 사용하기 위해 설정을 해줘야함

- JpaVendorAdapter : 사용할 Database의 설정 정보를 지정

- LocalContainerEntityManagerFactoryBean : EntityManagerFactory를 생성

- EntityManagerFactory : EntityManager를 생성

- JpaTransactionManager : JPA의 트랜잭션 관리, @Transactional을 지정하여 사용


Entity


- 데이터베이스에 저장된 데이터를 자바 객체로 mapping한 것

- 메모리 상 자바 객체의 인스턴스 형태로 존재

ex) TestModel 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
30
@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

- @Entity : 대상 클래스를 Entity 클래스로 지정

- @Table : Mapping할 table 이름 지정(생략 시, 대문자 클래스명의 table mapping -> TESTMODEL table)

- @Id : primary key 설정

- @Column : table에서 사용하는 column명과 변수를 mapping(생략 시, 대문자 변수명으로 mapping -> id(ID), name(NAME))


EntityManager


- Entity를 데이터베이스와 동기화하는 역할

- Persistence Context인 entity를 관리하기 위한 영역이 있음

- 어플리케이션이 DB에 접근하는 경우, EntityManager에서 entity를 얻거나 새로 생성한 entity를 persistence context에 등록해야함

- 구조

1

1. EntityManager API

- find : primary key를 기반으로 Entity 검색(SELECT)

- persist : 어플리케이션에서 생성한 인스턴스를 Entity로 persistence context에 삽입(INSERT)

- merge : 분리 상태가 된 entity를 persistence context에  merge(UPDATE)

- remove : entity를 persistence context에서 삭제, db에서 삭제(DELETE)

- flush : persistence context의 변경 내용을 DB에 동기화

- refresh : persistence context의 내용을 DB의 내용으로 변경

- detach : entity를 persistence context에서 분리

- clear : persistence context에 있는 모든 entity를 분리


2. Persistence Context-Database

- persistence context는 DB의 캐시와 같은 역할

- entity에 대한 작업이 수행되어도 DB에 즉시 반영되지는 않음

- 트랜잭션이 commit되거나 application에서 flush 호출 시 DB에 반영


Entity State


1. 상태

1) new

- 새로운 entity 인스턴스가 생성되고 persistence context에 등록되지 않은 상태


2) managed

- persistence context에 entity가 등록된 상태

- DB 동기화 활성화


3) detached

- managed 상태였던 entity가 persistence context에서 분리된 상태

- DB와 동기화되지 않음


4) removed

- DB에서 삭제될 것이 예정된 상태


2. Lifecycle

2


연관관계


- RDB에서 Table간 관계를 정의하는 것처럼 JPA에서는 Entity 간 참조 관계로 매핑하여 사용


1. 종류

3


1) OneToMany

- 하나의 entity가 여러개의 다른 entity를 가질 수 있음


2) ManyToOne

- 여러개의 entity가 하나의 다른 entity를 가질 수 있음


3) ManyToMany

- 여러개의 entity가 여러개의 다른 entity를 가질 수 있음


4) OneToOne

- 하나의 entity가 하나의 다른 entity를 가질 수 있음


2. 예제

1) User Model

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
36
37
38
39
40
@Entity
@Table(name = "user")
public class User {
    @Id
    @Column(name = "id")
    private String id;
 
    @Column(name = "name")
    private String name;
 
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Book> books;
 
    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;
    }
 
    public List<Book> getBook() {
        return books;
    }
 
    public void setBook(List<Book> book) {
        this.books = book;
    }
}
cs

- 하나의 유저가 책을 빌린다고 했을 때, 여러 개의 책을 빌릴 수 있음

- 유저(One)-책(Many)이기 때문에 @OneToMany로 지정

- mappedBy : 다른 entity에 mapping(다른 entity의 변수명과 mappedBy의 값이 일치해야함)

- cascade : 적용할 cascade type을 지정


2) Book Model

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
36
37
38
39
40
41
@Entity
@Table(name = "book")
public class Book {
    @Id
    @Column(name = "book_id")
    private String bookId;
 
    @Column(name = "book_name")
    private String name;
 
    @ManyToOne
    @JoinColumn(name = "id")
    private User user;
 
    public Book() {
    }
 
    public String getBookId() {
        return bookId;
    }
 
    public void setBookId(String bookId) {
        this.bookId = bookId;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public User getUser() {
        return user;
    }
 
    public void setUser(User user) {
        this.user = user;
    }
}
cs

- 여러 개의 책은 한명의 유저에게 빌려질 수 있음

- 책(Many)-유저(One)이므로 @ManyToOne 지정


3) Service 구현

1
2
3
4
5
6
7
8
9
10
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
 
    public List<Book> getBook(String id) {
        User user = entityManager.find(User.class, id);
        return user.getBook();
    }
}
cs

- EntityManager를 통해 user를 불러오고 user가 가진 책을 조회하기 위해 getBook() 호출


4) Fetch

- Eager fetch : Entity 자체에서 API가 사용될 때 SQL 실행(User user = entityManager.find(User.class, id);)

- Lazy fetch : Entity와 매핑된 프로퍼티에 처음 접근할 때 SQL 실행(user.getBook();)


JPQL(Java Persistence Query Language)


- 기본키 지정없이 데이터베이스의 데이터를 처리할 때 사용


1. 종류

1) JPQL(Java Persistence Query Language)

- JPA 쿼리 언어를 사용하여 Entity를 가져오거나 값을 변경할 때 사용


2) Criteria Query

- JPQL보다 더 객체지향적

- Builder 패턴의 CriteriaQuery 객체를 이용하기 때문에 컴파일 시점에 타입 검사를 할 수 있음


3) Native Query

- SQL을 직접 사용하여 entity를 얻거나 갱신할 때 사용


2. Query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
 
    public List<Book> getBooksByName(String bookName) {
        String jpql = "SELECT b FROM Book b WHERE b.name = :name";
        TypedQuery<Book> query = entityManager.createQuery(jpql, Book.class);
        query.setParameter("name", bookName);
        return query.getResultList();
    }
 
    public List<Book> getBooks() {
        String jpql = "SELECT b FROM Book b";
        TypedQuery<Book> query = entityManager.createQuery(jpql, Book.class);
        return query.getResultList();
    }
}
cs

- String으로 query문 생성

- *과 같은 와일드카드가 불가능하고 alias를 설정하여 전체 column을 불러올 수 있음

- 생성한 query문을 createQuery의 인자로 넣어주고 반환받을 entity class를 등록

- parameter는 :param으로 설정하고 setParameter로 등록



JPA 사용


JPA 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. Create(POST)

1) Controller

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

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


2) Service

1
2
3
4
5
6
7
8
9
10
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
     
    @Transactional
    public void createUser(User user) {
        entityManager.persist(user);
    }
}
cs

- persist 수행 시, @Transactional을 지정해주어야함

- EntityManager의 persist method를 사용하여 insert


3) 결과

- 성공

4

- 실패(중복)

5


3. Read(GET)

1) Controller

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

- 한개의 user를 가져오는 핸들러와 모든 user를 가져오는 handler


2) Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
 
 
    public User getUserOne(String id) {
        User user = entityManager.find(User.class,id);
        return user;
    }
 
    public List<User> getUserAll() {
        String jpql = "SELECT u FROM User u";
        TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
        return query.getResultList();
    }
}
cs

- find의 경우는 @Transactional이 필요없음

- Primary key값으로 EntityManager를 사용하여 1개의 user를 가져옴

- jpql을 작성하여 모든 User를 가져옴


3) 결과

- GetOne

6

- GetAll

7


4. Update(PUT)

1) Controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping(value = "/jpa")
public class JpaController {
    @RequestMapping(value = "/users", method = RequestMethod.PUT)
    public void updateUser(@RequestBody User user){
        if(jpaService.getUserOne(user.getId()) == null) {
            throw new TestException(HttpStatus.NOT_FOUND, "존재하지 않습니다.");
        }
        jpaService.updateUser(user.getId(),user.getName());
    }
}
cs

- id가 없을 시, ExceptionHandler에서 처리


2) Service

1
2
3
4
5
6
7
8
9
10
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
     
    @Transactional
    public void updateUser(String id, String name) {
        getUserOne(id).setName(name);
    }
}
cs

- get을 하여 managed 상태로 만들고 managaed 상태인 entity의 name을 변경

- update는 entity를 변경하기 때문에 @Transactional을 지정해야함


3) 결과

- update

8

- update 내용 확인(get)

9


5. Delete(DELETE)

1) Controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping(value = "/jpa")
public class JpaController {
    @RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
    public void removeUser(@PathVariable String id){
        if(jpaService.getUserOne(id) == null) {
            throw new TestException(HttpStatus.NOT_FOUND, "존재하지 않습니다.");
        }
        jpaService.removeUser(id);
    }
}
cs

- id가 없을 시, ExceptionHandler에서 처리


2) Service

1
2
3
4
5
6
7
8
9
10
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
     
    @Transactional
    public void removeUser(String id) {
        entityManager.remove(getUserOne(id));
    }
}
cs

- remove는 entity를 삭제하기때문에 @Transactional을 지정해야함

- id를 사용하여 get으로 entity 불러옴


3) 결과

- remove

10

- remove 결과 확인(get)

11


6. 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
32
33
34
35
36
37
@RestController
@RequestMapping(value = "/jpa")
public class JpaController {
    @Autowired
    private JpaService jpaService;
 
    @RequestMapping(value = "/users/{id}", method = RequestMethod.GET)
    public User getUserOne(@PathVariable String id){
        return jpaService.getUserOne(id);
    }
 
    @RequestMapping(value = "/users", method = RequestMethod.GET)
    public List<User> getUserAll(){
        return jpaService.getUserAll();
    }
 
    @RequestMapping(value = "/users", method = RequestMethod.POST)
    public void createUser(@RequestBody User user){
        jpaService.createUser(user);
    }
 
    @RequestMapping(value = "/users", method = RequestMethod.PUT)
    public void updateUser(@RequestBody User user){
        if(jpaService.getUserOne(user.getId()) == null) {
            throw new TestException(HttpStatus.NOT_FOUND, "존재하지 않습니다.");
        }
        jpaService.updateUser(user.getId(),user.getName());
    }
 
    @RequestMapping(value = "/users/{id}", method = RequestMethod.DELETE)
    public void removeUser(@PathVariable String id){
        if(jpaService.getUserOne(id) == null) {
            throw new TestException(HttpStatus.NOT_FOUND, "존재하지 않습니다.");
        }
        jpaService.removeUser(id);
    }
}
cs


7. 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
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
 
    @Transactional
    public void createUser(User user) {
        entityManager.persist(user);
    }
 
    public User getUserOne(String id) {
        User user = entityManager.find(User.class,id);
        return user;
    }
 
    public List<User> getUserAll() {
        String jpql = "SELECT u FROM User u";
        TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
        return query.getResultList();
    }
 
    @Transactional
    public void updateUser(String id, String name) {
        getUserOne(id).setName(name);
    }
 
    @Transactional
    public void removeUser(String id) {
        entityManager.remove(getUserOne(id));
    }
}
cs


연관 관계 Entity


- OneToMany의 연관 관계를 가진 entity의 경우, JOIN을 사용하여야 함

- 사용하지 않으면 error 발생

1
2
3
4
5
6
public List<User> getUser(String id) {
    String jpql = "SELECT u FROM User u LEFT JOIN FETCH u.books WHERE u.id = :id";
    TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
    query.setParameter("id",id);
    return query.getResultList();
}
cs

- jpql 문에 LEFT JOIN FETCH를 사용하여 여러 개의 Book List를 가져올 수 있게함

- 결과(Join)

12

- 결과(Join X)

13


배타 제어


- 동시에 여러 트랜잭션이 실행될 때, 갱신 처리에서 배타 제어 필요

- JPA의 배타 제어 기능 사용


1. Optimistic Lock

- Entity에 버전 번호를 지정

- Transaction이 끝날 때, DB와의 버전 번호가 다르면 다른 트랜잭션이 동작했다고 판단하여 에러

1) Version

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
36
37
38
39
40
41
42
43
44
45
 
@Entity
@Table(name = "user")
public class User {
    @Id
    @Column(name = "id")
    private String id;
 
    @Column(name = "name")
    private String name;
 
    @OneToMany(mappedBy = "bookId", cascade = CascadeType.ALL)
    private List<Book> books;
 
    @Version
    @Column(name ="version")
    private Integer version;
 
    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;
    }
 
    public List<Book> getBook() {
        return books;
    }
 
    public void setBook(List<Book> book) {
        this.books = book;
    }
}
cs

- Entity에 @Version과 함께 version 변수 추가

- 갱신 작업 성공 시, version 값 증가

- DB에 반영하려 할 때, 버전 값이 다르면 exception 발생 후 rollback


2) lock

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
     
    @Transactional
    public void updateUser(String id, String name) {
        User user = getUserOne(id);
        entityManager.lock(user, LockModeType.OPTIMISTIC);
        user.setName(name);
    }
}
cs

- 갱신 작업을 시작하기전 EntityManager의 lock 수행

- LockModeType

13

-> OPTIMISTIC : 해당 Entity를 갱신하는 동안 해당 Entity에 대한 다른 트랜잭션의 갱신이 불가능(변경 내용이 없으면 Version 그대로)

-> OPTIMISTIC_FORCE_INCREMENT : OPTIMISTIC과 같지만 변경 내용이 없어도 Version 증가


2. Pessimistic Lock

- 공유 잠금, 배타 잠금 

- Version이 불필요하지만 version을 사용할 수 있는 Type도 존재


1) lock

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class JpaService {
    @PersistenceContext
    private EntityManager entityManager;
     
    @Transactional
    public void updateUser(String id, String name) {
        User user = getUserOne(id);
        entityManager.lock(user, LockModeType.PESSIMISTIC_READ);
        user.setName(name);
    }
}
cs

- LockModeType

14

-> PESSIMISTIC_READ : 공유 잠금 -> 다른 트랜잭션에서 읽기는 가능하지만 update는 불가능

-> PESSIMISTIC_WRITE : 배타 잠금 -> 다른 트랜잭션에서 해당 Entity에 접근 불가능

-> PESSIMISTIC_FORCE_INCREMENT : PERSSIMISTIC_WRITE + OPTIMISTIC_FORCE_INCREMENT


Comments