저번 글에서는 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 들에 접근이 가능하다.