저번 글에서는 user의 CRUD를 생성해 보았다.
이번에는 유저가 적는 글 : article 을 생성해보면서 1:N을 어떻게 사용하는지 알아본다.
순서 : entity => repository => service => controller
순으로 차례대로 만들겠다.
public class User{
...
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private List<Article> articles = new ArrayList<>();
}
articles 가 생성되었다. 사람 한명이 여러 글을 쓸 수 있기 때문에 1:다 형태이다.
@OneToMany
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
- OneToMany : 하위 클래스에 대한 매핑 정보 제공
- mappedBy : 다른 엔티티에서 이 관계를 참조하는 필드 지정
- cascade : 연관된 엔티티의 변경 사항을 자동으로 전파,
ALL=> 해당 엔티티의 모든 변경 사항이 관련된 엔티티에 적용- orphanRemoval : 연관된 엔티티가 참조되지 않을때 자동으로 삭제
- @JsonIgnore : 아래에서 설명
@Entity
@Table(name="articles")
@Getter @Setter
public class Article {
@Id @GeneratedValue
private Long id;
private String title;
private String content;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id",nullable = false)
@JsonIgnoreProperties("articles")
User user;
}
@ManyToOne
다수의 Article 엔티티가 하나의 User 엔티티를 참조하는 관계
- fetch : 엔티티를 로드할 때 관련된 엔티티가 즉시 로드되도록 지정
- FetchType.EAGER => 즉시 로드
- @JoinColumn : 외래 키 정의
- name : 외래 키의 이름 지정 (user_id)
- nullable : 외래 키가 null 값을 허용하는지 여부
- @JsonIgnoreProperties("articles") : 아래에서 설명
왜 EAGER 를 사용했나요?
일반적으로 다대일에서 원래 FetchType.LAZY을 권장한다. 엔티티가 실제로 사용될 때까지 로딩을 지연시킬 수 있기 때문이다. 하지만 여기서는 USer 엔티티가 Article 엔티티와 함께 항상 필요한 데이터라고 생각했기 때문에 EAGER를 사용했다.
또한!@JsonIgnoreProperties를 사용해서 연관된 User 엔티티를 Article에서 제외시키기 때문에 직렬화 할때 성능 저하가 없다
@JsonIgnoreProperties 이란 무엇일까?
Jackson 라이브러리에서 제공하는 어노테이션 중 하나로 JSON (역)직렬화 시에 특정 필드를 무시하도록 지정할 수 있다.
예를 들어 패스워드와 카드번호를 제외하고 싶을 때 이렇게 사용한다.
@JsonIgnoreProperties({"password","creditNumber"})
public class User{
private String name;
private String password;
private String creditNumber;
...
}
@JsonIgnoreProperties("articles") 마찬가지로 이렇게 사용됐으니 user의 엔티티 안에 articles 라는 필드를 포함하지 않겠다라는 것이다.
같은 내용인데 사용법이 살짝 다르다. 아래와 같이 사용한다.
JsonIgnore은 속성 수준에만 사용 가능하고,
클래스 수준에서는 JsonIgnoreProperties를 사용해야 한다.
public class User {
private String username;
@JsonIgnore
private String password;
private String email;
// ...
}
유저와 마찬가지로 JpaRepository를 상속받아 사용한다.
public interface ArticleRepository extends JpaRepository<Article, Long> {
List<Article> findByUser(User user);
}
유저는 클래스를 만드는게 다였지만 인터페이스에서 하나의 메서드를 정의해주고 넘어간다.
@Service
public class ArticleService {
@Autowired
private ArticleRepository articleRepository;
public List<Article> getAllArticle() {
return articleRepository.findAll();
}
public Article getArticleById(Long id){
return articleRepository.findById(id)
.orElseThrow(()->new ArticleNotFoundException(id));
}
public List<Article> getArticlesByUser(User user) {
return articleRepository.findByUser(user);
}
public Article createArticle(String title, String content, User user) {
Article article = new Article();
article.setUser(user);
article.setTitle(title);
article.setContent(content);
return articleRepository.save(article);
}
public Article updateArticle(Long id,String title,String content) {
Article article = getArticleById(id);
article.setTitle(title);
article.setContent(content);
return articleRepository.save(article);
}
public void deleteArticle(Long id){
articleRepository.deleteById(id);
}
}
ArticleRepository를 연결해주고 의존성 주입을 해준다. (@Autowired)
다른건 다 비슷하고 좀 특이한 부분이라면
public Article getArticleById(Long id){
return articleRepository.findById(id).orElseThrow(()->new ArticleNotFoundException(id));
}
orElseThrow : 클래스 값이 있을 수도 있고 없을 수도 있는 값에 대해. Optional 객체가 비어있다면 ()-> 함수를 발생시킨다.
@RestController
@RequestMapping("/api/articles")
public class ArticleController {
@Autowired
public ArticleService articleService;
@Autowired
public UserService userService;
@GetMapping("/all")
public ResponseEntity<?> getAllArticles() {
List<Article> articles = articleService.getAllArticle();
return ResponseEntity.ok(articles);
}
@GetMapping(params = "user")// /articles?user=5
public ResponseEntity<List<Article>> getArticlesByUser(@RequestParam("user") Long userId) {
User user = userService.getUserById(userId);
List<Article> articles = articleService.getArticlesByUser(user);
System.out.println(articles.toString());
return ResponseEntity.ok(articles);
}
@PostMapping
public ResponseEntity<?> createArticle(@RequestBody ArticleRequest articleRequest) {
User user = userService.getUserById(articleRequest.getUserId());
Article article = articleService.createArticle(
articleRequest.getTitle(),
articleRequest.getContent(),
user
);
return ResponseEntity.ok("article 생성 성공");
}
@GetMapping("/{id}")
public ResponseEntity<Article> getArticleById(@PathVariable("id") Long id) {
Article article = articleService.getArticleById(id);
return ResponseEntity.ok(article);
}
@PutMapping("/{id}")
public ResponseEntity<Article> updateArticle(@PathVariable("id") Long id, @RequestBody ArticleRequest articleRequest) {
Article newArticle = articleService.updateArticle(id,articleRequest.getTitle(), articleRequest.getContent());
return ResponseEntity.ok(newArticle);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable("id") Long id) {
articleService.deleteArticle(id);
return ResponseEntity.noContent().build(); // 204 No content
}
}
post 를 자주 사용할 것이기 때문에 ArticleRequest 를 사용한다.
Article인데 client에서 어떤 값을 보낼 수 있는지 정의되어 있다고 생각하면 쉽겠다.
@Getter
public class ArticleRequest {
private String title;
private String content;
private Long userId;
}
@RequestBody 로 articleRequest를 받아와서
articleRequest.get~~ 로 접근하면 body에 보낸 Object 들에 접근이 가능하다.