JPA

[JPA] 연관관계를 가진 엔티티 save 하기

voider 2021. 1. 16. 21:16

연관관계를 가진 엔티티 save 하기

 

대충 이런 관계

insert하려는 테이블이 여러 테이블과 연관관계를 가지고 있을 때 어떻게 해야 할까? Recipe를 추가할 때, 재료는 Ingredient에, 요리 과정은 CookingMethod 테이블에 보관해야 한다. 유형이나 분류는 이미 정해져있는 것을 가져다 사용하면 되므로 제외했다. 프로세스를 짜보자. 사용자 입장에서.

  1. 등록하려는 레시피가 어떤 유형인지 선택한다.
  2. 등록하려는 레시피가 어떤 분류인지 선택한다.
  3. 요리 이름
  4. 완성된 요리 썸네일 삽입
  5. 간략한 설명
  6. 재료를 입력.(여러 개)
  7. 요리 과정 입력.(여러 개)
  8. 요리 과정 이미지 삽입(여러 개)

이렇게 입력하고 등록했을 때 서버에서 일어나는 일은

  1. 입력 받은 데이터를 분류한다.
  2. Ingredient를 등록한다.
  3. CookingMethod를 등록한다.
  4. 2, 3번 데이터를 가지고 Recipe를 등록한다.

이 세가지 과정을 순차적으로 진행하면 될 것 같다. 그런데 매번 세 엔티티를 save해주는 것은 번거롭다. CasCadeALL으로 설정하면 Recipe를 등록할 때 자동으로 Ingredient와 CookingMethod까지 저장할 수 있을 것 같다.

JPA에서 엔티티를 save할 때, 연관관계를 가진 모든 엔티티는 영속 상태여야 한다. 따라서 Recipe를 저장하는 시점에 Ingredient와 CookingMethods는 이미 영속 상태여야 한다. 엔티티를 save할 때 연관된 엔티티도 함께 save하고 싶다면 영속성 전이 기능인 CASCADE를 이용한다.
CascadeType에 몇 가지 종류가 있는데 ALL을 사용하면 모든 부분에 적용된다.

Recipe.java

@Entity
public class Recipe {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(columnDefinition = "varchar(255) default ''")
    private String description;

    @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    Set<Ingredient> ingredients = new HashSet<>();

    @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @OrderBy("sequence ASC")
    Set<CookingMethod> cookingMethods = new HashSet<>();

    @ManyToOne(fetch = FetchType.LAZY)
    private FoodNation foodNation; //한중일양식

    @ManyToOne(fetch = FetchType.LAZY)
    private FoodType foodType;  //국, 반찬, 찌개...

    @Column(columnDefinition = "integer default 0")
    private Integer cookingTime;

    @Column(columnDefinition = "integer default 0")
    private int calorie;

    @Column(columnDefinition = "integer default 0")
    private int servings;

    @Column(columnDefinition = "varchar(255) default ''")
    private String thumbnail;

 

Test

@DisplayName("레시피 등록 스트")
    @Test
    void testRecipeSave() throws Exception {
    	//레시피 등록 시점에 이미 존재하고 있어야 하는 데이터들 등록
        FoodType foodType = new FoodType("test food type");
        FoodNation foodNation = new FoodNation("test nation");
        IngredientType ingredientType = new IngredientType("test ingredient type");

        ingredientTypeRepository.save(ingredientType);
        foodNationRepository.save(foodNation);
        foodTypeRepository.save(foodType);

        List<FoodNation> allNation = foodNationRepository.findAll();
        List<FoodType> allTypes = foodTypeRepository.findAll();
        List<IngredientType> allIngredientTypes = ingredientTypeRepository.findAll();
		
	//Recipe를 등록할 때 함께 등록할 연관 엔티티 컬렉션
		Set<Ingredient> ingredients = new HashSet<>();
        Set<CookingMethod> cookingMethods = new HashSet<>();

        for (int i=0; i < 10; i++) { //더미 데이터 
            Ingredient ingredient = Ingredient.builder()
                    .ingredient(i+"-test")
                    .quantity(i+"g")
                    .ingredientType(allIngredientTypes.get(0))
                    .build();

            CookingMethod cookingMethod = CookingMethod.builder()
                    .description(i+" method des")
                    .image(i+" method Image")
                    .sequence(i)
                    .build();

            ingredients.add(ingredient);
            cookingMethods.add(cookingMethod);
        }
        
  	//화면에서 요청하는 form
        RecipeSaveForm recipeSaveForm = new RecipeSaveForm();
        recipeSaveForm.setName("Test Recipe!!");
        recipeSaveForm.setDescription("Test Description");
        recipeSaveForm.setThumbnail("image URL");
        recipeSaveForm.setCookingTime(20);
        recipeSaveForm.setServings(2);
        recipeSaveForm.setFoodType(allTypes.get(0));
        recipeSaveForm.setFoodNation(allNation.get(0));
        recipeSaveForm.setIngredients(ingredients);
        recipeSaveForm.setCookingMethods(cookingMethods);

	//JSON으로 변경
        String requesetJson = objectMapper.writeValueAsString(recipeSaveForm);
        System.out.println("requesetJson : " + requesetJson);

	//등록 요청
        mockMvc.perform(post("/register")
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .with(csrf())
                .content(requesetJson))
                .andExpect(status().isOk());

	//검증
        List<Recipe> recipes = recipeRepository.findAll();

        System.out.println(recipes.get(0).getCookingMethods().isEmpty());
        recipes.get(0).getCookingMethods().forEach(row -> System.out.println(row.getDescription()));

        assertTrue(!recipes.get(0).getCookingMethods().isEmpty());
        assertTrue(!recipes.get(0).getIngredients().isEmpty());
        assertEquals(recipes.get(0).getName(), recipeSaveForm.getName());

?

Cascade설정을 하지 않으면 예외가 나지 않을까 싶어서 빼봤는데 정상 작동한다. 왜일까? 위 테스트 코드에서 Ingredient와 CookingMethod는 Trensient상태라고 생각했는데 내가 잘못 알고 있는 걸까?...