모델(엔티티)은 DB의 데이터를 그대로 받아서 데이터를 넣을 모델링에 사용된다.
개발자는 setter
를 이용해서 데이터를 오브젝트에 넣지만 JPA의 ORM은 기본생성자를 이용한다.
이때 JPA의 ORM은 리플렉션을 사용해 엔티티 클래스를 찾는다.
예를들어 @Entity
클래스를 엔티티로 인식하고, @Id
필드를 기본키로 인식한다.
로그인에 사용할 유저 엔티티를 만들어 보면
package shop.mtcoding.hiberapp.model;
import java.sql.Timestamp;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.CreationTimestamp;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@Getter
@Table(name = "user_tb")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 생성 전략
private Long id;
private String username;
private String password;
private String email;
private Timestamp createdAt;
// @Setter 를 만들지 않는다 필요한 값만 넣어줄 생성자만 있으면 된다.
public User(String username, String password, String email){
this.username = username;
this.password = password;
this.email = email;
}
// 필요한 데이터만 setter 로 바꿔준다.
public void update(String password, String email){
this.password = password;
this.email = email;
}
필요한 데이터만 생성자를 넣어서 만들면 되지만 빌더 패턴을 이용하면 필요한 데이터만 넣을 수 있어서 사용하기 더 좋아진다.
// 빌더 패턴을 이용한 생성자
@Builder
public User(Long id, String username, String password, String email, Timestamp createdAt) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.createdAt = createdAt;
}
우리가 넣고 싶은 데이터는 username
, password
, email
이지만 혹여나 몇개의 데이터만 넣게 된다면 다음과 같이 넣으면 된다.
User user = User.builder()
.username("ssar")
.password("1234")
.build();
yml 설정을 하면 서버 실행시 콘솔에 생성되는 테이블을 확인할 수 있다.
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
'[format_sql]': true
@RequiredArgsConstructor
@Repository
public class UserRepository {
private final EntityManager em;
// 저장후 리턴이 필요할 경우 void -> User // 물론 DTO를 만들어서 리턴하면 더 좋다.
@Transactional
public User save(User user){
em.persist(user);
return user;
}
// jpa 의 update는 pk를 주지 않으면 insert를 해버린다 !!!!! 주의
@Transactional
public User update(User user){
return em.merge(user);
}
@Transactional
public void delete(User user){
em.remove(user);
}
public User findById(Long id){
return em.find(User.class, id);
}
public List<User> findAll(){
return em.createQuery("select u from User u", User.class)
.getResultList();
}
// 이렇게 사용시 자동적으로 페이징이 된다. 처음은 0페이지 , 5개씩 조회
public List<User> findAll(int page){
return em.createQuery("select u from User u", User.class)
.setFirstResult(page * 5)
.setMaxResults(5)
.getResultList();
}
// 오버로딩해서 한번에 볼 데이터 조절
public List<User> findAll(int page, int row){
return em.createQuery("select u from User u", User.class)
.setFirstResult(page * row)
.setMaxResults(row)
.getResultList();
}
}
EntityManager
가 제공하는 여러 메소드를 이용해서 CRUD를 수행할 수 있다.
조회가 아닐때는 @Transactional
를 붙여서 트랜잭션 관리를 해야한다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class UserApiController {
private final UserRepository userRepository;
@PostMapping("/users")
public ResponseEntity<?> addUser(User user){
User userPS = userRepository.save(user);
return new ResponseEntity<>(userPS, HttpStatus.CREATED);
}
@PutMapping("/users/{id}")
public ResponseEntity<?> updateUser(@PathVariable Long id, User user){
User userPS = userRepository.findById(id);
if(ObjectUtils.isEmpty(userPS)){
return new ResponseEntity<>("해당 유저가 없습니다.", HttpStatus.BAD_REQUEST);
}
userPS.update(user.getPassword(), user.getEmail());
User updateUserPS = userRepository.update(userPS);
return new ResponseEntity<>(updateUserPS, HttpStatus.OK);
}
@DeleteMapping("/users/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id){
User userPS = userRepository.findById(id);
if(ObjectUtils.isEmpty(userPS)){
return new ResponseEntity<>("해당 유저가 없습니다.", HttpStatus.BAD_REQUEST);
}
userRepository.delete(userPS);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping("/users")
public ResponseEntity<?> findUsers(@RequestParam(defaultValue = "0") int page){
List<User> userList = userRepository.findAll(page, 2);
return new ResponseEntity<>(userList, HttpStatus.OK);
}
@GetMapping("/users/{id}")
public ResponseEntity<?> findUserOne(@PathVariable Long id){
User userPS = userRepository.findById(id);
if(ObjectUtils.isEmpty(userPS)){
return new ResponseEntity<>("해당 유저가 없습니다.", HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(userPS, HttpStatus.OK);
}
}
postman을 이용한 테스트를 해보면
포스트맨을 날리기 전에 사용자의 비밀번호를 보이지 않게 설정하자
@JsonIgnore // 설정하면 비밀번호는 리턴되지 않는다.
private String password;
@CreationTimestamp // 간단하게 시간 나오게 설정
private Timestamp createdAt;
다음과 같이 요청
get 요청
0번째 페이지에 2명씩 보기로 설정했으므로 2명의 데이터가 조회된다.
@GetMapping("/users")
public ResponseEntity<?> findUsers(@RequestParam(defaultValue = "0") int page){
List<User> userList = userRepository.findAll(page, 2);
return new ResponseEntity<>(userList, HttpStatus.OK);
}
[
{
"id": 1,
"username": "ssar1111",
"email": "ssar@nate.com",
"createdAt": "2023-03-16T13:21:04.704+00:00"
},
{
"id": 2,
"username": "ssar2222",
"email": "ssar@nate.com",
"createdAt": "2023-03-16T13:21:08.174+00:00"
}
]
JpaRepository
는 Spring Data JPA
의 일부이고 Spring Data JPA
는 Java Persistence API ( JPA )를 기반으로하기 때문에 JpaRepository
는 내부적으로 EntityManager
를 사용한다.
JpaRepository
는 다음과 같은 인터페이스를 상속하고 CRUD 에 사용될 메소드를 가지고 있다.
상속한 레파지토리를 만들고 컨트롤러에 의존성을 주입한다.
아래의 레파지토리는 User객체를 영속화 하므로 <User, Long>
으로 만든다. ( Long -> id )
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserJpaRepository extends JpaRepository<User, Long> {
}
의존성을 바꾸면 컨트롤러에 오류가 발생하는데 임시방편으로 다음과 같은 방법으로 오류를 없앤다.
private final UserJpaRepository userRepository;
User userPS = userRepository.findById(id).get();
get()
은 java.util.Optional
의 메소드로서 값이 존재할경우 리턴해준다.
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
여기서 중요한 포인트는 JpaRepository를 의존했을때는 save()
메소드를 사용하게 되는데 이 save()
메소드는 post 와 put 요청에 동시에 사용된다.
즉, save()
메소드가 호출될때 전달된 오브젝트의 id가 null 이라면 insert
를 수행하고 null이 아니라면 update
를 수행하게 된다.
User userPS = userRepository.save(user);
이제 포스트맨을 이용해서 테스트해본다면
기본 페이징 설정을 다음과 같이 했으므로
@GetMapping("/users")
public ResponseEntity<?> findUsers(@RequestParam(defaultValue = "0") int page){
Page<User> userList = userRepository.findAll(PageRequest.of(page, 2));
return new ResponseEntity<>(userList, HttpStatus.OK);
}
아래의 리턴결과가 날라온다.
{
"content": [
{
"id": 1,
"username": "ssar1",
"email": "ssar@nate.com",
"createdAt": "2023-03-16T13:41:05.003+00:00"
},
{
"id": 2,
"username": "ssar2",
"email": "ssar@nate.com",
"createdAt": "2023-03-16T13:41:08.279+00:00"
}
],
"pageable": {
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"pageNumber": 0,
"pageSize": 2,
"offset": 0,
"paged": true,
"unpaged": false
},
"totalPages": 3,
"totalElements": 6,
"last": false,
"sort": {
"sorted": false,
"unsorted": true,
"empty": true
},
"numberOfElements": 2,
"size": 2,
"number": 0,
"first": true,
"empty": false
}
리턴 결과를 보면 페이징에 사용되는 여러 변수들이 보이는데 JPA를 이용하면 페이징처리를 보다 쉽게 할 수 있다.
localhost:8080/api/users?page=2 로 요청을 해보면 "last": true,
를 응답받을 수 있는데 마지막 페이지를 의미한다.
총 페이지 수가 "pageSize": 2,
이므로 localhost:8080/api/users?page=3 을 요청하면 "content": [],
을 응답받는데 아무런 데이터가 담겨오지 않는걸 볼 수 있다.