K-Digital Training/내일배움캠프

숙련 2주차/Spring

hoonssss 2023. 11. 21. 10:29
반응형
SMALL

RestTemplate란

RestTemplate은 간편하게 Rest방식의 API를 호출할 수 있는 Spring 내장 클래스이다. Spring 3.0부터 지원하는 Spring의 HTTP 통신 템플릿이다. Restful의 원칙을 지킬 수 있으며 HTTP 메서드들에 적합한 여러 메서드 제공

 

RestTemplate & Open API

Client, Server 프로젝트 2개 만들어서 진행

RestTemplate 생성자는 build로 만듬

    private final RestTemplate restTemplate;

    public RestTemplateService(RestTemplateBuilder builder) {
        this.restTemplate = builder.build();
    }
 

요청 받은 검색어를 Query String 방식으로 Server 입장의 서버로 RestTemplate를 사용하여 요청

    public ItemDto getCallObject(String query) {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:7070")
                .path("/api/server/get-call-obj")
                .queryParam("query", query)
                .encode()
                .build()
                .toUri();
        log.info("uri = " + uri);

        ResponseEntity<ItemDto> responseEntity = restTemplate.getForEntity(uri, ItemDto.class);

        log.info("statusCode = " + responseEntity.getStatusCode());

        return responseEntity.getBody();
    }
 

RestTemplate의 getForEntity는 Get 방식으로 해당 URI의 서버에 요청을 진행

첫 번째 파라미터에는 URI,

두 번째 파라미터에는 전달 받은 데이터와 매핑하여 인스턴스화할 클래스의 타입을 주면됨(요청을 한 서버에서 넘어오는 데이터를 받아 줄 Class타입으로 지정해주면 역직렬화가 되어 객체형태로 담김)

ResponseEntity HTTPEntity를 상속받고 있음. HTTP관련 데이터를 Response응답할 때 사용

대표사진 삭제

사진 설명을 입력하세요.

@RequestParam -> ?parameter=값

 

여러개의 값을 한번에 호출

Json Dependency

    // json
    implementation 'org.json:json:20230227'
 
    public List<ItemDto> getCallList() {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("http://localhost:7070")
                .path("/api/server/get-call-list")
                .encode()
                .build()
                .toUri();
        log.info("uri = " + uri);

        ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);

        log.info("statusCode = " + responseEntity.getStatusCode());
        log.info("Body = " + responseEntity.getBody());

        return fromJSONtoItems(responseEntity.getBody());
    }
 

fromJSONtoItems

    public List<ItemDto> fromJSONtoItems(String responseEntity) {
        JSONObject jsonObject = new JSONObject(responseEntity);
        JSONArray items  = jsonObject.getJSONArray("items");
        List<ItemDto> itemDtoList = new ArrayList<>();

        for (Object item : items) {
            ItemDto itemDto = new ItemDto((JSONObject) item);//item->JSON형식으로 캐스팅
            itemDtoList.add(itemDto);
        }

        return itemDtoList;
    }
 

 

Post 요청

public ItemDto postCall(String query) {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/post-call/{query}") //PathVariable 방식
            .encode()
            .build()
            .expand(query) //{}에 들어갈 값
            .toUri();
    log.info("uri = " + uri);

    User user = new User("Robbie", "1234"); 

    ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class);

    log.info("statusCode = " + responseEntity.getStatusCode());

    return responseEntity.getBody();
}
 

restTemplate.postForEntity

postForEntity는 getForEntity와는 다르게 첫 번째 parameter로 uri를 넣고 두 번째에는 HttpBody에 넣어줄 데이터를 넣으면 됨 세번째는 전달 받은 데이터랑 Mapping할 것을 넣으면 됨

 

    private final List<Item> itemList = Arrays.asList(
            new Item("Mac", 3_888_000),
            new Item("iPad", 1_230_000),
            new Item("iPhone", 1_550_000),
            new Item("Watch", 450_000),
            new Item("AirPods", 350_000)
    );

    public Item getCallObject(String query) {
        for(Item item : itemList){
            if(item.getTitle().equals(query)){
                return item;
            }
        }
        return null;
    }

   public Item postCall(String query, UserRequestDto userRequestDto) {
      System.out.println("userRequestDto.getUsername() = " + userRequestDto.getUsername());
      System.out.println("userRequestDto.getPassword() = " + userRequestDto.getPassword());

      return getCallObject(query);
}
 

postCall -> return getCallObject(query)를 통해 query값 일치하는 것 불러옴

 
사진 삭제

사진 설명을 입력하세요.

 

RestTemplate의 exchange

Header에 정보를 추가

public List<ItemDto> exchangeCall(String token) {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
            .fromUriString("http://localhost:7070")
            .path("/api/server/exchange-call")
            .encode()
            .build()
            .toUri();
    log.info("uri = " + uri);

    User user = new User("Robbie", "1234");

    RequestEntity<User> requestEntity = RequestEntity
            .post(uri)
            .header("X-Authorization", token)
            .body(user);

    ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);

    return fromJSONtoItems(responseEntity.getBody());
}
 

RestTemplate.exchange 첫번째에 requestEntity타입의 필드를 줄 수 있음 두번째는 받아올 데이터

    @GetMapping("/exchange-call")
    public List<ItemDto> exchangeCall(@RequestHeader("Authorization") String token) {
        return restTemplateService.exchangeCall(token);
    }
 

key,value형태로 특정한 값 토큰을 넣어서 보내면 DispatcherServlet를 타고 Controller로 들어올때 @RequestHeader 사용하여 쉽게 사용 가능

내부에다가 가지고 오고 싶은 key값 입력 뒤에는 원하는 변수명 입력

 

 
사진 삭제

사진 설명을 입력하세요.

 

네이버 OPEN API

package com.sparta.springresttemplateclient.naver.controller;

import com.sparta.springresttemplateclient.naver.dto.ItemDto;
import com.sparta.springresttemplateclient.naver.service.NaverApiService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api")
public class NaverApiController {

    private final NaverApiService naverApiService;

    public NaverApiController(NaverApiService naverApiService) {
        this.naverApiService = naverApiService;
    }

    @GetMapping("/search")
    public List<ItemDto> searchItems(@RequestParam String query)  {
        return naverApiService.searchItems(query);
    }
}
 
package com.sparta.springresttemplateclient.naver.service;

import com.sparta.springresttemplateclient.naver.dto.ItemDto;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;

@Slf4j(topic = "NAVER API")
@Service
public class NaverApiService {

    private final RestTemplate restTemplate;

    public NaverApiService(RestTemplateBuilder builder) {
        this.restTemplate = builder.build();
    }

    public List<ItemDto> searchItems(String query) {
        // 요청 URL 만들기
        URI uri = UriComponentsBuilder
                .fromUriString("https://openapi.naver.com")
                .path("/v1/search/shop.json")
                .queryParam("display", 15)
                .queryParam("query", query)
                .encode()
                .build()
                .toUri();
        log.info("uri = " + uri);

        RequestEntity<Void> requestEntity = RequestEntity
                .get(uri)
                .header("X-Naver-Client-Id", "개인id값")
                .header("X-Naver-Client-Secret", "개인secret값")
                .build();

        ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);

        log.info("NAVER API Status Code : " + responseEntity.getStatusCode());

        return fromJSONtoItems(responseEntity.getBody());
    }

    public List<ItemDto> fromJSONtoItems(String responseEntity) {
        JSONObject jsonObject = new JSONObject(responseEntity);
        JSONArray items  = jsonObject.getJSONArray("items");
        List<ItemDto> itemDtoList = new ArrayList<>();

        for (Object item : items) {
            ItemDto itemDto = new ItemDto((JSONObject) item);
            itemDtoList.add(itemDto);
        }

        return itemDtoList;
    }
}
 
package com.sparta.springresttemplateclient.naver.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;
import org.json.JSONObject;

@Getter
@NoArgsConstructor
public class ItemDto {
    private String title;
    private String link;
    private String image;
    private int lprice;

    public ItemDto(JSONObject itemJson) {
        this.title = itemJson.getString("title");
        this.link = itemJson.getString("link");
        this.image = itemJson.getString("image");
        this.lprice = itemJson.getInt("lprice");
    }
}
 
 
사진 삭제

사진 설명을 입력하세요.

 

Entity 연관 관계

DB연동

spring.datasource.url=jdbc:mysql://localhost:3306/orderapp
spring.datasource.username=root
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
 

orderapp DB생성

cd /usr/local/mysql/bin -> ./mysql -u root -p -> password -> create database orderapp;

 

table생성

create table users
(
    id bigint not null auto_increment,
    name varchar(255),
    primary key (id)
);

create table food
(
    id bigint not null auto_increment,
    name varchar(255),
    price float(53) not null ,
    primary key (id)
);
 

주문 음식 정보를 파악하기 위해 food_id 컬럼 추가

alter table users add food_id bigint;
 

users table value 값

insert into users (name, food_id) VALUE ('Robbie',1);
insert into users (name, food_id) VALUE ('Robbert',1);
insert into users (name, food_id) VALUE ('Robbie',2);
 

food

음식과 고객은 1:N관계임(여러명의 고객이 하나의 음식을 주문할 수 있음)

주문고객 파악을 위해 user_id컬럼 추가

alter table food add user_id bigint;
 

food table value 값

INSERT INTO food (name, price, user_id) VALUES ('후라이드 치킨', 15000, 1);
INSERT INTO food (name, price, user_id) VALUES ('후라이드 치킨', 15000, 2);
INSERT INTO food (name, price, user_id) VALUES ('양념 치킨', 20000, 1);
 

불필요하게 고객의 이름과 메뉴가 중복되는 문제가 발생

주문에 대한 정보를 기록할 orders 테이블을 추가

create table orders
(
    id bigint not null auto_increment,
    user_id bigint,
    food_id bigint,
    order_date date,
    primary key (id)
)
 

주문 테이블을 사용하여 테이블들의 연관 관계를 해결

drop table if exists food; //food삭제
drop table if exists users; //users삭제
create table users
(
    id   bigint not null auto_increment,
    name varchar(255),
    primary key (id)
);

create table food
(
    id    bigint    not null auto_increment,
    name  varchar(255),
    price float(53) not null,
    primary key (id)
);

alter table orders
    add constraint orders_user_fk
        foreign key (user_id) //orders 테이블 user_id는 users id를 참조
            references users (id);

alter table orders
    add constraint orders_food_fk
        foreign key (food_id) //orders테이블의 food_id는 food의 id를 참조
            references food (id);

INSERT INTO users (name) VALUES ('Robbie');
INSERT INTO users (name) VALUES ('Robbert');

INSERT INTO food (name, price) VALUES ('후라이드 치킨', 15000);
INSERT INTO food (name, price) VALUES ('양념 치킨', 20000);
INSERT INTO food (name, price) VALUES ('고구마 피자', 30000);
INSERT INTO food (name, price) VALUES ('아보카도 피자', 50000);

INSERT INTO orders (user_id, food_id, order_date) VALUES (1, 1, SYSDATE());
INSERT INTO orders (user_id, food_id, order_date) VALUES (2, 1, SYSDATE());
INSERT INTO orders (user_id, food_id, order_date) VALUES (2, 2, SYSDATE());
INSERT INTO orders (user_id, food_id, order_date) VALUES (1, 4, SYSDATE());
INSERT INTO orders (user_id, food_id, order_date) VALUES (2, 3, SYSDATE());
 

고객 1명은 음식 N개를 주문할 수 있음

고객 : 음식 = 1 : N 관계

음식 1개는 고객 N명에게 주문될 수 있음

음식 : 고객 = 1 : N 관계

결론적으로 고객과 음식은 N : M 관계

고객 : 음식 = N : M 관계(다대다)

N:M(다대다)관계인 데이터들의 연관 관계를 해결하기 위해 중간 테이블을 사용할 수 있음

 

DB 관계에서는 방향의 개념이 있을까?

단방향 : users테이블에서만 food테이블을 참조할 수 있을 때

양방향 : users테이블과 food테이블이 서로를 참조할 수 있을 때

DB 테이블간의 관계에서는 방향의 개념이 없음(DB에서는 어떤 테이블을 기준으로 하든 원하는 정보를 JOIN을 사용하여 조회할 수 있음)

SELECT u.name as username, f.name as foodname, o.order_date as orderdate
FROM users u
         INNER JOIN orders o on u.id = o.user_id
         INNER JOIN food f on o.food_id = f.id
WHERE o.user_id = 1;
----------------------------------------------------------------------------
SELECT u.name as username, f.name as foodname, o.order_date as orderdate
FROM food f
         INNER JOIN orders o on f.id = o.food_id
         INNER JOIN users u on o.user_id = u.id
WHERE o.user_id = 1;
 

JPA Entity에서의 테이블간의 연관 관계 표현

음식 : 고객 = N : 1

음식

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}
 

고객

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user")
    private List<Food> foodList = new ArrayList<>(); //존재하지않음, 조회를 위한 표현
}
 

한명의 고객은 여러번 주문이 가능한 상황

이를 Entity에서 여러번 가능함을 표현하기 위해 Java 컬렉션을 사용하여 List<Food> foodList = new ArrayList<>() 이처럼 표현할 수 있음

DB테이블에 실제 컬럼으로 존재하지는 않지만 Entity상태에서 다른 Entity를 참조하기 위해 이러한 방법을 사용

현재 음식 Entity와 고객 Entity는 서로를 참조하고 있음(N : 1 양방향 관계)

 

음식

@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}
 

고객

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
 

음식Entity에서만 고객Entity를 참조할 수 있음

이러한 관계를(N : 1) 단방향 관계라 부름

고객Entity에는 음식Entity의 정보가 없기 때문에 음식 정보를 조회할 수 없음

정리

DB테이블에서는 테이블 사이의 연관관계를 FK로 맺을 수 있고 방향 상관없이 조회 가능

Entity에서는 상대 Entity를 참조하여 Entity 사이의 연관관계를 맺을 수 있음(양방향)

상대 Entity를 참조하지 않고 있다면 상대 Entity를 조회할 수 있는 방법은 없음(단방향)

따라서 Entity에서는 DB테이블에는 없는 방향의 개념이 존재함

 

1:1관계(@OneToOne)

단방향 관계

Entity에서 외래 키의 주인은 일반적으로 N(다)의 관계인 Entity이지만 1 대 1 관계에서는 직접 정해줘야함

외래 키 주인은 외래 키를 등록, 수정, 삭제할 수 있으며 주인이 아닌쪽에서는 외래 키를 읽기만 가능

@JoinColumn()은 외래 키의 주인이 활용하는 애너테이션입니다.

컬럼명, null 여부, unique 여부 등을 지정할 수 있습니다.

 

음식 Entity가 외래 키의 주인인 경우

 
사진 삭제

사진 설명을 입력하세요.

음식
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}
------------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
 

반대인 경우(고객 Entity가 외래키의 주인인 경우)

 
사진 삭제

사진 설명을 입력하세요.

음식
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;
}
----------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = "food_id")
    private Food food;
}
 

 

양방향 관계

JPA에서 양방향일때 직접 외래키의 주인은 누구이다 라고 알려줘야함(mappedBy)

단방향이라면 외래 키의 주인만 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 속성 설정

양방향이라면 외래 키의 주인의 상대 Entity에 mappedBy 설정(즉 @JoinColumn() 반대)

음식Entity가 외래 키의 주인인 경우

음식
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}
-----------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne(mappedBy = "user") //user(속성의값)는 외래 키의 주인(음식) user을 의미함
    private Food food;
}
 

고객 Entity가 외래 키의 주인인 경우

음식
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToOne(mappedBy = "food")
    private User user;
}
------------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = "food_id")
    private Food food;
}
 

단방향 테스트코드

package com.sparta.jpaadvance.relation;

import com.sparta.jpaadvance.entity.Food;
import com.sparta.jpaadvance.entity.User;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@Transactional//insert,delete,update..
public class OneToOneTest {

    @Autowired
    FoodRepository foodRepository;
    @Autowired
    UserRepository userRepository;

    @Test
    @Rollback(value = false) //test에서는 Transactional에 의해 자동 rollback 됨으로 false설정
    @DisplayName("1대1 단방향 테스트")
    void test1(){

        User user = new User();
        user.setName("Robbie");

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);
        food.setUser(user); //외래키(연관 관계)설정

        userRepository.save(user);
        foodRepository.save(food);
    }
}
 

양방향 테스트코드

package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToOne(mappedBy = "user")
    private Food food;

    public void addFood(Food food){
        this.food = food;
        food.setUser(this);
    }
}
 
@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패")
void test2() {
    Food food = new Food();
    food.setName("고구마 피자");
    food.setPrice(30000);

    // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
    User user = new User();
    user.setName("Robbie");
    user.setFood(food);

    userRepository.save(user);
    foodRepository.save(food);

    // 확인해 보시면 user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다.
}

@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
void test3() {
    Food food = new Food();
    food.setName("고구마 피자");
    food.setPrice(30000);

    // 외래 키의 주인이 아닌 User 에서 Food 를 저장하기 위해 addFood() 메서드 추가
    // 외래 키(연관 관계) 설정 food.setUser(this); 추가
    User user = new User();
    user.setName("Robbie");
    user.addFood(food);

    userRepository.save(user);
    foodRepository.save(food);
}

@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트")
void test4() {
    User user = new User();
    user.setName("Robbert");

    Food food = new Food();
    food.setName("고구마 피자");
    food.setPrice(30000);
    food.setUser(user); // 외래 키(연관 관계) 설정

    userRepository.save(user);
    foodRepository.save(food);
}
 

외래키의 주인만이 외래키를 컨트롤할 수 있다

외래키의 주인이 아닌데 컨트롤하기 위해서는 위코드 ex) add를 따로 만들어서 설정해야 함

외래키의 주인이 아니라면 조회만 가능함

 

조회테스트

@Test
@DisplayName("1대1 조회 : Food 기준 user 정보 조회")
void test5() {
    Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 음식 정보 조회
    System.out.println("food.getName() = " + food.getName());

    // 음식을 주문한 고객 정보 조회
    System.out.println("food.getUser().getName() = " + food.getUser().getName());
}

@Test
@DisplayName("1대1 조회 : User 기준 food 정보 조회")
void test6() {
    User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 고객 정보 조회
    System.out.println("user.getName() = " + user.getName());

    // 해당 고객이 주문한 음식 정보 조회
    Food food = user.getFood();
    System.out.println("food.getName() = " + food.getName());
    System.out.println("food.getPrice() = " + food.getPrice());
}
 

 

 

⚠️ 주의!

1. 외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 사용하지 않아도 default 옵션이 적용되기 때문에 생략이 가능합니다.

- 다만 1 대 N 관계에서 외래 키의 주인 Entity가 @JoinColumn() 애너테이션을 생략한다면 JPA가 외래 키를 저장할 컬럼을 파악할 수가 없어서 의도하지 않은 중간 테이블이 생성됩니다.

- 따라서 외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 활용하시는게 좋습니다.

2. 양방향 관계에서 mappedBy 옵션을 생략할 경우 JPA가 외래 키의 주인 Entity를 파악할 수가 없어 의도하지 않은 중간 테이블이 생성되기 때문에 반드시 설정해주시는게 좋습니다.

 

@ManyToOne N:1관계(다대일)

단방향 관계

음식
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}
-------------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
 

양방향관계

양방향 참조를 위해 고객 Entity에서 Java 컬렌션을 사용하여 음식 Entity 참조

음식
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}
---------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user")
    private List<Food> foodList = new ArrayList<>();
}
 

테스트코드

@Test
    @Rollback(value = false)
    @DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패")
    void test2() {

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        Food food2 = new Food();
        food2.setName("양념 치킨");
        food2.setPrice(20000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
        User user = new User();
        user.setName("Robbie");
        user.getFoodList().add(food);
        user.getFoodList().add(food2);

        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);

        // 확인해 보시면 user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다.(Null)
    }

    @Test
    @Rollback(value = false)
    @DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
    void test3() {

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        Food food2 = new Food();
        food2.setName("양념 치킨");
        food2.setPrice(20000);

        // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드 생성하고
        // 해당 메서드에 외래 키(연관 관계) 설정 food.setUser(this); 추가
        User user = new User();
        user.setName("Robbie");
        user.addFoodList(food);
        user.addFoodList(food2);

        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);
    }

    @Test
    @Rollback(value = false)
    @DisplayName("N대1 양방향 테스트")
    void test4() {
        User user = new User();
        user.setName("Robbert");

        Food food = new Food();
        food.setName("고구마 피자");
        food.setPrice(30000);
        food.setUser(user); // 외래 키(연관 관계) 설정

        Food food2 = new Food();
        food2.setName("아보카도 피자");
        food2.setPrice(50000);
        food2.setUser(user); // 외래 키(연관 관계) 설정

        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food2);
    }
}
 

조회

@Test
@DisplayName("N대1 조회 : Food 기준 user 정보 조회")
void test5() {
    Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 음식 정보 조회
    System.out.println("food.getName() = " + food.getName());

    // 음식을 주문한 고객 정보 조회
    System.out.println("food.getUser().getName() = " + food.getUser().getName());
}

@Test
@DisplayName("N대1 조회 : User 기준 food 정보 조회")
void test6() {
    User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 고객 정보 조회
    System.out.println("user.getName() = " + user.getName());

    // 해당 고객이 주문한 음식 정보 조회
    List<Food> foodList = user.getFoodList();
    for (Food food : foodList) {
        System.out.println("food.getName() = " + food.getName());
        System.out.println("food.getPrice() = " + food.getPrice());
    }
}
 

 

@OneToMany 1대다(1:N) 관계

외래 키를 관리하는 주인은 음식 Entity이지만 실제 외래 키는 고객 Entity가 가지고 있음

단방향

음식
@Entity
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToMany
    @JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼(N:1 -> 1)
    private List<User> userList = new ArrayList<>();
}
----------------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
 

외래 키를 음식 Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만, 실제 DB에서 외래 키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 존재(1:N 관계)

 

양방향 관계

1 대 N 관계에서는 일반적으로 양방향 관계가 존재하지 않음

1 대 N 관계에서 양방향 관계를 맺으려면 음식 Entity를 외래 키의 주인으로 정해주기 위해 고객 Entity에서 mappedBy 옵션을 사용해야 하지만 @ManyToOne 애너테이션은 mappedBy 속성을 제공하지 않음

N 관계의 Entity인 고객 Entity에서 @JoinColum의 insertable 과 updatable 옵션을 false로 설정하여 양쪽으로 JOIN 설정을 하면 양방향처럼 설정할 수는 있음

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

		@ManyToOne
		@JoinColumn(name = "food_id", insertable = false, updatable = false)
		private Food food;
}
 

단방향 테스트

 

package com.sparta.jpaadvance.relation;

import com.sparta.jpaadvance.entity.Food;
import com.sparta.jpaadvance.entity.User;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@SpringBootTest
public class OneToManyTest {

    @Autowired
    FoodRepository foodRepository;
    @Autowired
    UserRepository userRepository;

    @Test
    @DisplayName("1:N 단방향")
    void test1(){

        User user = new User();
        user.setName("Robbie");

        User user2 = new User();
        user2.setName("Robbert");

        Food food = new Food();
        food.setName("후라이드치킨");
        food.setPrice(15000);
        food.getUserList().add(user);
        food.getUserList().add(user2);

        userRepository.save(user);
        userRepository.save(user2);
        foodRepository.save(food);
    }
}
 

다대일에서는 setUser로 연관관계를 설정하지만 일대다에서는 List형식이기 때문에 add로 실행

UPDATA 쿼리가 발생함

외래키는 users테이블이 가지고 있음

 
사진 삭제

사진 설명을 입력하세요.

조회

    @Test
    @DisplayName("1대N 조회테스트")
    void test2(){
        Food food = foodRepository.findById(2L).orElseThrow(NullPointerException::new);
        System.out.println("food.getName() = " + food.getName());

        //해당 음식을 주문한 고객 정보
        List<User> users = food.getUserList();
        for(User user : users){
            System.out.println("user.getName() = " + user.getName());
        }
    }
 

 

@ManyToMany N:M관계(다대다 관계)

다대다관계는 중간테이블을 생성하여 사용함

단방향

음식
package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToMany
    @JoinTable(name = "orders", //중간테이블 생성
            joinColumns = @JoinColumn(name = "food_id"), //현재 위치에서 중간 테이블 조인
            inverseJoinColumns = @JoinColumn(name = "user_id")) //반대 위치에서 중간 테이블 조인
    private List<User> userList = new ArrayList<>();
}
---------------------------------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}
 

중간테이블을 컨트롤하기 어렵기 때문에 추후에 중간 테이블의 변경이 발생할 경우 문제가 발생할 가능성이 있음

JPA에 의해서 만들어진 테이블은 컨트롤이 어려움

 

양방향

음식
package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToMany
    @JoinTable(name = "orders", //중간테이블 생성
            joinColumns = @JoinColumn(name = "food_id"), //현재 위치에서 중간 테이블 조인
            inverseJoinColumns = @JoinColumn(name = "user_id")) //반대 위치에서 중간 테이블 조인
    private List<User> userList = new ArrayList<>();
}
------------------------------------------------------------------------------------
고객
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "userList")
    private List<Food> foodList = new ArrayList<>();
}
 

test

package com.sparta.jpaadvance.relation;

import com.sparta.jpaadvance.entity.Food;
import com.sparta.jpaadvance.entity.User;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@SpringBootTest
public class ManyToManyTest {

    @Autowired
    FoodRepository foodRepository;
    @Autowired
    UserRepository userRepository;

    @Test
    @DisplayName("N:M 단방향 테스트")
    void test1(){
        User user = new User();
        user.setName("Robbie");

        User user2 = new User();
        user2.setName("Robbert");

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);
        food.getUserList().add(user);
        food.getUserList().add(user2);

        userRepository.save(user);
        userRepository.save(user2);
        foodRepository.save(food);
    }
}
 

양방향

음식
package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToMany
    @JoinTable(name = "orders", // 중간 테이블 생성
            joinColumns = @JoinColumn(name = "food_id"), // 현재 위치인 Food Entity 에서 중간 테이블로 조인할 컬럼 설정
            inverseJoinColumns = @JoinColumn(name = "user_id")) // 반대 위치인 User Entity 에서 중간 테이블로 조인할 컬럼 설정
    private List<User> userList = new ArrayList<>();

    public void addUserList(User user) {
        this.userList.add(user); // 외래 키(연관 관계) 설정
        user.getFoodList().add(this);
    }
}
-------------------------------------------------------------------------------------
고객
package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToMany(mappedBy = "userList")
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.getUserList().add(this); // 외래 키(연관 관계) 설정
    }
}
 

test

@Test
@Rollback(value = false)
@DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패")
void test2() {

    Food food = new Food();
    food.setName("후라이드 치킨");
    food.setPrice(15000);

    Food food2 = new Food();
    food2.setName("양념 치킨");
    food2.setPrice(20000);

    // 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
    User user = new User();
    user.setName("Robbie");
    user.getFoodList().add(food);
    user.getFoodList().add(food2);

    userRepository.save(user);
    foodRepository.save(food);
    foodRepository.save(food2);

    // 확인해 보시면 orders 테이블에 food_id, user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다.
}

@Test
@Rollback(value = false)
@DisplayName("N대M 양방향 테스트 : 외래 키 저장 실패 -> 성공")
void test3() {

    Food food = new Food();
    food.setName("후라이드 치킨");
    food.setPrice(15000);

    Food food2 = new Food();
    food2.setName("양념 치킨");
    food2.setPrice(20000);

    // 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드를 생성해서 사용합니다.
    // 외래 키(연관 관계) 설정을 위해 Food 에서 userList 를 호출해 user 객체 자신을 add 합니다.
    User user = new User();
    user.setName("Robbie");
    user.addFoodList(food);
    user.addFoodList(food2);


    userRepository.save(user);
    foodRepository.save(food);
    foodRepository.save(food2);
}

@Test
@Rollback(value = false)
@DisplayName("N대M 양방향 테스트")
void test4() {

    User user = new User();
    user.setName("Robbie");

    User user2 = new User();
    user2.setName("Robbert");


    Food food = new Food();
    food.setName("아보카도 피자");
    food.setPrice(50000);
    food.getUserList().add(user); // 외래 키(연관 관계) 설정
    food.getUserList().add(user2); // 외래 키(연관 관계) 설정

    Food food2 = new Food();
    food2.setName("고구마 피자");
    food2.setPrice(30000);
    food2.getUserList().add(user); // 외래 키(연관 관계) 설정

    userRepository.save(user);
    userRepository.save(user2);
    foodRepository.save(food);
    foodRepository.save(food2);

    // User 를 통해 food 의 정보 조회
    System.out.println("user.getName() = " + user.getName());

    List<Food> foodList = user.getFoodList();
    for (Food f : foodList) {
        System.out.println("f.getName() = " + f.getName());
        System.out.println("f.getPrice() = " + f.getPrice());
    }

    // 외래 키의 주인이 아닌 User 객체에 Food 의 정보를 넣어주지 않아도 DB 저장에는 문제가 없지만
    // 이처럼 User 를 사용하여 food 의 정보를 조회할 수는 없습니다.
}

@Test
@Rollback(value = false)
@DisplayName("N대M 양방향 테스트 : 객체와 양방향의 장점 활용")
void test5() {

    User user = new User();
    user.setName("Robbie");

    User user2 = new User();
    user2.setName("Robbert");


    // addUserList() 메서드를 생성해 user 정보를 추가하고
    // 해당 메서드에 객체 활용을 위해 user 객체에 food 정보를 추가하는 코드를 추가합니다. user.getFoodList().add(this);
    Food food = new Food();
    food.setName("아보카도 피자");
    food.setPrice(50000);
    food.addUserList(user);
    food.addUserList(user2);

    Food food2 = new Food();
    food2.setName("고구마 피자");
    food2.setPrice(30000);
    food2.addUserList(user);


    userRepository.save(user);
    userRepository.save(user2);
    foodRepository.save(food);
    foodRepository.save(food2);

    // User 를 통해 food 의 정보 조회
    System.out.println("user.getName() = " + user.getName());

    List<Food> foodList = user.getFoodList();
    for (Food f : foodList) {
        System.out.println("f.getName() = " + f.getName());
        System.out.println("f.getPrice() = " + f.getPrice());
    }
}
 

조회

@Test
@DisplayName("N대M 조회 : Food 기준 user 정보 조회")
void test6() {
    Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 음식 정보 조회
    System.out.println("food.getName() = " + food.getName());

    // 음식을 주문한 고객 정보 조회
    List<User> userList = food.getUserList();
    for (User user : userList) {
        System.out.println("user.getName() = " + user.getName());
    }
}

@Test
@DisplayName("N대M 조회 : User 기준 food 정보 조회")
void test7() {
    User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
    // 고객 정보 조회
    System.out.println("user.getName() = " + user.getName());

    // 해당 고객이 주문한 음식 정보 조회
    List<Food> foodList = user.getFoodList();
    for (Food food : foodList) {
        System.out.println("food.getName() = " + food.getName());
        System.out.println("food.getPrice() = " + food.getPrice());
    }
}
 

 

중간테이블(orders) 컨트롤하기

중간테이블 직접 Entity로 만듬

package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "food_id")
    private Food food;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}
 

Food, User Entity도 변경

음식
package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToMany(mappedBy = "food") //order를 통해서 food
    private List<Order> orderList = new ArrayList<>();
}
-----------------------------------------------------------------
고객
package com.sparta.jpaadvance.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user")
    private List<Order> orderList = new ArrayList<>();
}
 

테스트코드

package com.sparta.jpaadvance.relation;

import com.sparta.jpaadvance.entity.Food;
import com.sparta.jpaadvance.entity.Order;
import com.sparta.jpaadvance.entity.User;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.OrderRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@SpringBootTest
public class OrderTest {

    @Autowired
    UserRepository userRepository;
    @Autowired
    FoodRepository foodRepository;
    @Autowired
    OrderRepository orderRepository;

    @Test
    @Rollback(value = false)
    @DisplayName("중간 테이블 Order Entity 테스트")
    void test1() {

        User user = new User();
        user.setName("Robbie");

        Food food = new Food();
        food.setName("후라이드 치킨");
        food.setPrice(15000);

        // 주문 저장
        Order order = new Order();
        order.setUser(user); // 외래 키(연관 관계) 설정
        order.setFood(food); // 외래 키(연관 관계) 설정

        userRepository.save(user);
        foodRepository.save(food);
        orderRepository.save(order);
    }

    @Test
    @DisplayName("중간 테이블 Order Entity 조회")
    void test2() {
        // 1번 주문 조회
        Order order = orderRepository.findById(1L).orElseThrow(NullPointerException::new);

        // order 객체를 사용하여 고객 정보 조회
        User user = order.getUser();
        System.out.println("user.getName() = " + user.getName());

        // order 객체를 사용하여 음식 정보 조회
        Food food = order.getFood();
        System.out.println("food.getName() = " + food.getName());
        System.out.println("food.getPrice() = " + food.getPrice());
    }
}
 

주문일 추가

@Entity
@Getter
@Setter
@Table(name = "orders")
@EntityListeners(AuditingEntityListener.class)
public class Order {    

@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime orderDate;
}
 
//Application에 추가
@EnableJpaAuditing
 

지연로딩, 즉시로딩

JPA는 연관관계가 설정된 Entity의 정보를 바로 가져올지, 필요할 때 가져올지 정할 수 있음

즉, 가져오는 방법을 정하게되는데 JPA에서는 Fetch Type이라 부름.

Fetch Type에는 2가지 종류가 있는데 하나는 LAZY, 다른 하나는 EAGER

- LAZY는 지연 로딩으로 필요한 시점에 정보를 가져옴

- EAGER는 즉시 로딩으로 이름의 뜻처럼 조회할 때 연관된 모든 Entity의 정보를 즉시 가져옴

기본적으로 @OneToMany 애너테이션은 Fetch Type의 default 값이 LAZY로 지정되어있고 반대로 @ManyToOne 애너테이션은 EAGER임

@ManyToOne -> EAGER

@OneToMany -> LAZY (@OneToMany collection(ex. List<Food> food)이기 때문에 효율적으로 정보를 읽기 위해서 지연로딩 방식이 Defautl임)

fetchType 변경

    @ManyToOne(fetch = FetchType.LAZY)
 

지연로딩도 영속성 컨텍스트의 기능 중 하나임.

-> 지연 로딩이 된 Entity를 조회할려고 할때 영속성 컨텍스트가 존재해야함. 즉 트랜잭션이 적용되어야있어야함.

 

영속성 전의

Cascade -> SQL에서 사용하는 방법을 JPA에서 지원해줌

영속성 전이를 적용하여 해당 Entity를 저장할 때 연관된 Entity까지 자동으로 저장하기 위해서는 자동으로 저장하려고 하는 연관된 Entity에 추가한 연관관계 애너테이션에 CASCADE의 PERSIST 옵션을 설정

주인 반대편에 설정해주면 됨

일대다 관계라면 보통 다 쪽이 주인이니 @OnaToMany쪽에 입력

    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
    private List<Food> foodList = new ArrayList<>();
 
        userRepository.save(user);
        foodRepository.save(food);
        foodRepository.save(food1);
------------------------------------------
        userRepository.save(user);
 

삭제

    @Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("Robbie 탈퇴")
    void test3() {
        // 고객 Robbie 를 조회합니다.
        User user = userRepository.findByName("Robbie");
        System.out.println("user.getName() = " + user.getName());

        // Robbie 가 주문한 음식 조회
        for (Food food : user.getFoodList()) {
            System.out.println("food.getName() = " + food.getName());
        }

        // 주문한 음식 데이터 삭제
        foodRepository.deleteAll(user.getFoodList());

        // Robbie 탈퇴
        userRepository.delete(user);
    }
 

 

영속성 전이 삭제

중첩적용

중괄호안에 설정

    @OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
    private List<Food> foodList = new ArrayList<>();
 

연관된 데이터 삭제

Robbie 고객 Entity 객체를 조회한 후 해당 객체를 delete 하자 자동으로 연관된 음식 데이터들이 삭제

    @Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("영속성 전이 삭제")
    void test4() {
        // 고객 Robbie 를 조회합니다.
        User user = userRepository.findByName("Robbie");
        System.out.println("user.getName() = " + user.getName());

        // Robbie 가 주문한 음식 조회
        for (Food food : user.getFoodList()) {
            System.out.println("food.getName() = " + food.getName());
        }

        // Robbie 탈퇴
        userRepository.delete(user);
    }
 

고아 Entity

연관관계 제거

실제 DB에서는 제거되지 않고 연관관계만 제거

    @Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("연관관계 제거")
    void test1() {
        // 고객 Robbie 를 조회합니다.
        User user = userRepository.findByName("Robbie");
        System.out.println("user.getName() = " + user.getName());

        // 연관된 음식 Entity 제거 : 후라이드 치킨
        Food chicken = null;
        for (Food food : user.getFoodList()) {
            if(food.getName().equals("후라이드 치킨")) {
                chicken = food;
            }
        }
        if(chicken != null) {
            user.getFoodList().remove(chicken);
        }

        // 연관관계 제거 확인
        for (Food food : user.getFoodList()) {
            System.out.println("food.getName() = " + food.getName());
        }
    }
 

연관관계를 제거하는 것 만으로도 해당 Entity를 삭제할 수 있음(후라이드 치킨 삭제)

추가로 orphanRemoval 옵션도 REMOVE 옵션과 마찬가지로 해당 Entity 즉, Robbie Entity 객체를 삭제하면 연관된 음식 Entity들이 자동으로 삭제

    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<Food> foodList = new ArrayList<>();
 

⚠️ 주의!

orphanRemoval이나 REMOVE 옵션을 사용할 때 삭제하려고 하는 연관된 Entity를 다른 곳에서 참조하고 있는지 아닌지를 꼭 확인해야함

A와 B에 참조되고 있던 C를 B를 삭제하면서 같이 삭제하게 되면 A는 참조하고 있던 C가 사라졌기 때문에 문제가 발생

따라서 orphanRemoval 같은 경우 @ManyToOne 같은 애너테이션에서는 사용할 수 없음

ManyToOne이 설정된 Entity는 해당 Entity 객체를 참조하는 다른 Entity 객체들이 있을 수 있기 때문에 속성으로 orphanRemoval를 가지고 있지 않음

 

My Select Shop 구축

naver api이용해서 필요 항목 검색

 

Scheduler기능 구현

특정 시간 마다업데이트를 위해서 설정

@Slf4j(topic = "Scheduler")
@Component
@RequiredArgsConstructor
public class Scheduler {

    private final NaverApiService naverApiService;
    private final ProductService productService;
    private final ProductRepository productRepository;

                    // 초,분,시,일,월,주 순서
    @Scheduled(cron = "0 0 1 * * *") // 매일 새벽 1시 메서드 작동
    public void updatePrice() throws InterruptedException {
        log.info("가격 업데이트 실행");
        List<Product> productList = productRepository.findAll();
        for (Product product : productList) {
            // 1초에 한 상품 씩 조회합니다 (NAVER 제한)
            TimeUnit.SECONDS.sleep(1);

            // i 번째 관심 상품의 제목으로 검색을 실행합니다.
            String title = product.getTitle();
            List<ItemDto> itemDtoList = naverApiService.searchItems(title);

            if (itemDtoList.size() > 0) {
                ItemDto itemDto = itemDtoList.get(0);
                // i 번째 관심 상품 정보를 업데이트합니다.
                Long id = product.getId();
                try {
                    productService.updateBySearch(id, itemDto);
                } catch (Exception e) {
                    log.error(id + " : " + e.getMessage());
                }
            }
        }
    }
}
 

Auditiong처럼 @EnableScheduling 설정

@SpringBootApplication
@EnableJpaAuditing
@EnableScheduling
public class MyselectshopApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyselectshopApplication.class, args);
    }
}
 

 

회원가입, 로그인, naverAPI, 관심상품 폴더 구현

https://github.com/hoonssss/project/tree/MySelectShop

 

 

 

 

반응형
LIST

'K-Digital Training > 내일배움캠프' 카테고리의 다른 글

숙련 1주차/Spring  (2) 2023.11.21
입문 2주차/Spring  (2) 2023.11.21
입문 1주차/Spring  (1) 2023.11.21
자바 5주차  (1) 2023.11.01
자바 4주차  (1) 2023.11.01