[수업 목표]
1. 스프링에서 DB를 다룰 때 사용되는 JPA(영속성 컨텍스트, JPA 연관관계)를 이해한다.
2. Spring Data JPA(Query Methods, 페이징 및 정렬)에 대해 이해한다.
3. 이를 '나만의 셀렉샵'에 폴더 기능을 추가하여 적용한다.
ORM : Object-Relational Mapping
웹서버에서와 DB에서 사용하는 언어가 다른데 이를 통용하는 역할
- Object: "객체"지향 언어 (자바, 파이썬)
- Relational: "관계형" 데이터베이스 (H2, MySQL)
Questions)
1. ORM이 없이 웹 서버 개발은 못 하나?
JPA: Java Persistence API
자바 ORM 기술에 대한 표준 명세 (자바의 ORM이다)
@Entity // DB 테이블 역할을 합니다.
public class User {
// ID가 자동으로 생성 및 증가합니다.
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
@Column(unique = true)
private Long kakaoId;
}
Questions)
1. JPA가 없으면?
사실상 표준 (de facto, 디팩토)
보통 기업간 치열한 경쟁을 통해 시장에서 결정되는 비 공식적 표준
출처: 위키백과
인텔리제이에서 File > New > Project
검색창을 클릭 후, 5개 포함하기
spring.h2.console.enabled=true
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:./myselectdb;AUTO_SERVER=TRUE
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type.descriptor.sql=trace
server.port=8090
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
@Setter
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class User {
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
@Id // PK다
@Column(name = "id", nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = false)
private String nickname;
@Column(nullable = false, unique = false)
private String favoriteFood;
public User(String username, String nickname, String favoriteFood) {
this.username = username;
this.nickname = nickname;
this.favoriteFood = favoriteFood;
}
}
jdbc:h2:file:./myselectdb;AUTO_SERVER=TRUE
myselectdb에서 오른쪽 버튼 > Database Tools > Manage Shown Schemas > All databases > refresh
PUBLIC > tables 확인
import com.sparta.jpa.model.User;
import com.sparta.jpa.repository.UserRepository;
import com.sparta.jpa.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private final UserService userService;
private final UserRepository userRepository;
@Autowired
public UserController(UserService userService, UserRepository userRepository) {
this.userService = userService;
this.userRepository = userRepository;
}
@GetMapping("/user/create")
public void createUser() {
User user = userService.createUser();
// 테스트 회원 데이터 삭제
userRepository.delete(user);
}
@GetMapping("/user/delete")
public void deleteUser() {
User user = userService.deleteUser();
// 테스트 회원 데이터 삭제
userRepository.delete(user);
}
@GetMapping("/user/update/fail")
public void updateUserFail() {
User user = userService.updateUserFail();
// 중요!) DB 에 변경 내용이 적용되었는지 확인!
// 테스트 회원 데이터 삭제
userRepository.delete(user);
}
@GetMapping("/user/update/1")
public void updateUser1() {
User user = userService.updateUser1();
// 테스트 회원 데이터 삭제
userRepository.delete(user);
}
@GetMapping("/user/update/2")
public void updateUse2() {
User user = userService.updateUser2();
// 중요!) DB 에 변경 내용이 적용되었는지 확인!
// 테스트 회원 데이터 삭제
userRepository.delete(user);
}
}
import com.sparta.jpa.model.User;
import com.sparta.jpa.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser() {
// 테스트 회원 "user1" 객체 추가
User beforeSavedUser = new User("user1", "정국", "불족발");
// 회원 "user1" 객체를 영속화
User savedUser = userRepository.save(beforeSavedUser);
// beforeSavedUser: 영속화되기 전 상태의 자바 일반객체
// savedUser:영속성 컨텍스트 1차 캐시에 저장된 객체
assert(beforeSavedUser != savedUser);
// 회원 "user1" 을 조회
User foundUser1 = userRepository.findById("user1").orElse(null);
assert(foundUser1 == savedUser);
// 회원 "user1" 을 또 조회
User foundUser2 = userRepository.findById("user1").orElse(null);
assert(foundUser2 == savedUser);
// 회원 "user1" 을 또또 조회
User foundUser3 = userRepository.findById("user1").orElse(null);
assert(foundUser3 == savedUser);
return foundUser3;
}
public User deleteUser() {
// 테스트 회원 "user1" 객체 추가
User firstUser = new User("user1", "지민", "엄마는 외계인");
// 회원 "user1" 객체를 영속화
User savedFirstUser = userRepository.save(firstUser);
// 회원 "user1" 삭제
userRepository.delete(savedFirstUser);
// 회원 "user1" 조회
User deletedUser1 = userRepository.findById("user1").orElse(null);
assert(deletedUser1 == null);
// -------------------
// 테스트 회원 "user1" 객체를 다시 추가
// 회원 "user1" 객체 추가
User secondUser = new User("user1", "지민", "엄마는 외계인");
// 회원 "user1" 객체를 영속화
User savedSecondUser = userRepository.save(secondUser);
assert(savedFirstUser != savedSecondUser); // DB입장 완전 다른 것(DB와 1차캐시에 삭제했다가 새로 생성해 저장)
assert(savedFirstUser.getUsername().equals(savedSecondUser.getUsername()));
assert(savedFirstUser.getNickname().equals(savedSecondUser.getNickname()));
assert(savedFirstUser.getFavoriteFood().equals(savedSecondUser.getFavoriteFood()));
// 회원 "user1" 조회
User foundUser = userRepository.findById("user1").orElse(null);
assert(foundUser == savedSecondUser);
return foundUser;
}
public User updateUserFail() {
// 회원 "user1" 객체 추가
User user = new User("user1", "뷔", "콜라");
// 회원 "user1" 객체를 영속화
User savedUser = userRepository.save(user);
// 회원의 nickname 변경
savedUser.setNickname("얼굴천재");
// 회원의 favoriteFood 변경
savedUser.setFavoriteFood("버거킹");
// 회원 "user1" 을 조회
User foundUser = userRepository.findById("user1").orElse(null);
// 중요!) foundUser 는 DB 값이 아닌 1차 캐시에서 가져오는 값 (but, DB는 반영이 안됨)
assert(foundUser == savedUser);
assert(foundUser.getUsername().equals(savedUser.getUsername()));
assert(foundUser.getNickname().equals(savedUser.getNickname()));
assert(foundUser.getFavoriteFood().equals(savedUser.getFavoriteFood()));
return foundUser;
}
public User updateUser1() {
// 테스트 회원 "user1" 생성
User user = new User("user1", "RM", "고기");
// 회원 "user1" 객체를 영속화
User savedUser1 = userRepository.save(user);
// 회원의 nickname 변경
savedUser1.setNickname("남준이");
// 회원의 favoriteFood 변경
savedUser1.setFavoriteFood("육회");
// user1 을 저장
User savedUser2 = userRepository.save(savedUser1);
assert(savedUser1 == savedUser2);
return savedUser2;
}
@Transactional
public User updateUser2() {
// 테스트 회원 "user1" 생성
// 회원 "user1" 객체 추가
User user = new User("user1", "진", "꽃등심");
// 회원 "user1" 객체를 영속화
User savedUser = userRepository.save(user);
// 회원의 nickname 변경
savedUser.setNickname("월드와이드핸섬 진");
// 회원의 favoriteFood 변경
savedUser.setFavoriteFood("까르보나라");
return savedUser;
}
}
import com.sparta.jpa.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, String> {
}
DB 변경내용 파악하기 위해 디버깅 (Debugging) 방법
1. UserService의 createUser() 함수 밑에 줄의 Line 번호 있는 곳을 클릭
-> Break Point (디버깅할 위치) 설정
2. BP 빨간 원에서 마우스 오른쪽 버튼 클릭
- SQL 언어로 바뀌기 전에 추가적으로 1차 캐시가 만들어짐
(Id, Entity를 매핑해서 가지고 있음)
- save에서 만든 User 객체 != 1차 캐시의 Entity 안의 User 객체
- save 하는 순간 1차 캐시에 저장 및 DB에 insert
- return 값은 1차 캐시에 있는 값
'1차 캐시' 사용의 장점
- findById는 1차 캐시에 Id가 존재하면 가져오는 것 (Entity의 값 가져옴)
- 존재하지 않는 경우, SQL 쿼리를 날림
-> 찾아와 1차 캐시를 만들어 반환
import com.sparta.jpa.model.User;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserRepositoryTest {
@Autowired
UserRepository userRepository;
@Order(1)
@Test
public void create() {
// 생성한 객체 3개는 모두 다 다른 객체 (1차캐시는 스프링 내부에서만 동작하기 때문)
// 회원 "user1" 객체 생성
User instance1 = new User("user1", "정국", "불족발");
// 회원 "user1" 객체 또 생성
User instance2 = new User("user1", "정국", "불족발");
assert(instance2 != instance1);
// 회원 "user1" 객체 또또 생성
User instance3 = new User("user1", "정국", "불족발");
assert(instance3 != instance2);
// 회원 "user1" 객체 추가
User user1 = new User("user1", "정국", "불족발");
// 회원 "user1" 객체의 ID 값이 없다가..
userRepository.save(user1);
// 테스트 회원 데이터 삭제
userRepository.delete(user1);
}
@Order(2)
@Test
public void findUser() {
// ------------------------------------
// 회원 "user1" 객체 추가
User beforeSavedUser = new User("user1", "정국", "불족발");
// 회원 "user1" 객체를 영속화
User savedUser = userRepository.save(beforeSavedUser);
// 회원 "user1" 을 조회
User foundUser1 = userRepository.findById("user1").orElse(null);
assert(foundUser1 != savedUser); // 주소가 다름 (원래는 select 하지 않는데 DB로 select, 1차 캐시에 없는 것, 그래서 매번 객체를 새로 만든다)
// 회원 "user1" 을 또 조회
User foundUser2 = userRepository.findById("user1").orElse(null);
assert(foundUser2 != savedUser);
// 회원 "user1" 을 또또 조회
User foundUser3 = userRepository.findById("user1").orElse(null);
assert(foundUser3 != savedUser);
// ------------------------------------
// 테스트 회원 데이터 삭제
userRepository.delete(beforeSavedUser);
}
}
- 1차 캐시는 DB와 계속 일관성을 맞추기 위해 노력
- delete 후에 findById하면 1차캐시 & DB 없으니까 NULL 반환
- JPA가 알아서 DB에 없으면 insert하고, 있으면 update 함
- save하면 DB에 update query 날림
@Transactional을 추가
굳이 userRepository.save() 함수를 호출하지 않아도, 함수가 끝나는 시점에 변경된 부분을 알아서 업데이트 해줌 (이를 "Dirty check" 라고 함)
간단히 함수가 종료되는 시점에 각 Entity에 save()가 호출된다라고 이해
정확한 이해 위해 필요한 추가학습 내용
: 쓰기 지연 SQL 저장소, flush, commit 등
[UserSerivce] Entity 업데이트 방법 (2)
updateUser2()
- SQL 문들을 모아 두었다가 한번에 실행
예를 들어, 우리가 음식 주문앱 DB를 설계한다고 가정하자!
일단 "고객이 1개의 음식을 주문할 수 있다"라는 요구사항을 받았다고 해보자
고객이 음식 주문 시, 주문 정보는 어느 테이블에 들어가야 할까?
고객 테이블? 음식 테이블?
Tip) 테이블 설계 시 실제 값을 넣어봄
시도1) "고객 테이블"에 주문 정보를 넣어보자!
-> 문제점: 회원 중복
시도2) "음식 테이블"에 주문 정보를 넣어보자!
-> 문제점: 음식 중복
'주문'을 위한 테이블 필요 -> Order 테이블 추가
(일단은 하나의 회원이 하나의 주문만 한다고 가정)
N : N의 관계를 가지기 위해서는 테이블 하나가 더 필요.
JPA의 경우는 Entity 클래스의 필드 위에 연관관계 어노테이션 (@) 을 설정해주는 것만으로 연관관계가 형성됨.
관계 | 코드 선언 | Entity |
---|---|---|
일대다 (1:N) | @OneToMany | Order(1) : Food(N) |
다대일 (N:1) | @ManyToOne | Owner(N) : Restaurant(1) |
일대일 (1:1) | @OneToOne | Order(1) : Coupon(1) |
다대다 (N:N) | @ManyToMany | User(N) : Restaurant(N) |
중요) 항상 Entity 본인 중심으로 관계를 생각!
@Entity
public class Order {
@OneToMany
private List<Food> foods;
@OneToOne
private Coupon coupon;
}
- Order 입장에서 OneToMany(One은 자기자신을 의미)
@Entity
public class Owner {
@ManyToOne
Restaurant restaurant;
}
@Entity
public class User {
@ManyToMany
List<Restaurant> likeRestaurants;
}
@Entity
public class Product extends Timestamped {
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
private Long userId;
private String title;
private String image;
private String link;
private int lprice;
private int myprice;
}
public interface ProductRepository extends JpaRepository<Product, Long> {
}
// 1. 상품 생성
Product product = new Product(...);
productRepository.save(product);
// 2. 상품 전체 조회
List<Product> products = productRepository.findAll();
// 3. 상품 전체 개수 조회
long count = productRepository.count();
// 4. 상품 삭제
productRepository.delete(product);
- save는 JpaRepository > PagingAndSortRepository가
extends하는 CrudRepository에 있음
- 여기서 GoTo Implementation으로 구현체 가보면 SimpleJpaRepository에 있음(em: 영속성 컨텍스트 매니저[persist, merge등을 함])
- 원래는 JPA사용하면 이러한 것들 코드로 구현해주는 데 PK 어떤건 지만 알려주면 알아서 해준다
public interface ProductRepository extends JpaRepository<Product, Long> {
// (1) 회원 ID 로 등록된 상품들 조회
List<Product> findAllByUserId(Long userId);
// (2) 상품명이 title 인 관심상품 1개 조회
Product findByTitle(String title);
// (3) 상품명에 word 가 포함된 모든 상품들 조회
List<Product> findAllByTitleContaining(String word);
// (4) 최저가가 fromPrice ~ toPrice 인 모든 상품들을 조회
List<Product> findAllByLpriceBetween(int fromPrice, int toPrice);
}
- By: By 다음에 오는 걸 가지고 조건이 들어감.
By 다음이 인자로 넘어와야함. (이는 Product에 선언되어있어야함)
(select 문 where 조건으로)
+ And로 조건 추가 가능
totalPages = totalElement / size 결과를 소수점 올림
1 / 10 = 0.1 => 총 1 페이지
9 / 10 = 0.9 => 총 1페이지
10 / 10 = 1 => 총 1페이지
11 / 10 => 1.1 => 총 2페이지
<1> 프론트엔드 개발자의 UI 작업 적용
<2> 스프링 서버 구현
// 로그인한 회원이 등록한 관심 상품 조회
@GetMapping("/api/products")
public Page<Product> getProducts(
@RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam("sortBy") String sortBy,
@RequestParam("isAsc") boolean isAsc,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
Long userId = userDetails.getUser().getId();
page = page - 1;
return productService.getProducts(userId, page, size, sortBy, isAsc);
}
// (관리자용) 전체 상품 조회
@Secured(UserRoleEnum.Authority.ADMIN)
@GetMapping("/api/admin/products")
public Page<Product> getAllProducts(
@RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam("sortBy") String sortBy,
@RequestParam("isAsc") boolean isAsc
) {
page = page -1;
return productService.getAllProducts(page, size, sortBy, isAsc);
}
// 회원 ID 로 등록된 상품 조회
public Page<Product> getProducts(Long userId, int page, int size, String sortBy, boolean isAsc) {
// 아래 방향은 Sort.Direction에 정의되어있음
// (그 안에 ASC, DESC 가 있음)
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
// 스프링 프레임워크가 만든 Sort
// direction(오름차순 or 내림차순) & sortBy(무엇으로 sort 할 건지)
Sort sort = Sort.by(direction, sortBy);
// 넘겨줄 Pageable 객체
// new PageRequest 대신에 static 함수인 PageRequest.of로 함
// Pageable은 인터페이스, PageRequest는 구현체
Pageable pageable = PageRequest.of(page, size, sort);
return productRepository.findAllByUserId(userId, pageable);
}
// (관리자용) 상품 전체 조회
public Page<Product> getAllProducts(int page, int size, String sortBy, boolean isAsc) {
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
Sort sort = Sort.by(direction, sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
return productRepository.findAll(pageable); // 이것도 원래 있는 것
}
import com.sparta.springcore.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findAllByUserId(Long userId, Pageable pageable);
}
```java
import com.sparta.springcore.dto.ItemDto;
import com.sparta.springcore.model.Product;
import com.sparta.springcore.model.User;
import com.sparta.springcore.model.UserRoleEnum;
import com.sparta.springcore.repository.ProductRepository;
import com.sparta.springcore.repository.UserRepository;
import com.sparta.springcore.service.ItemSearchService;
import com.sparta.springcore.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.sparta.springcore.service.ProductService.MIN_MY_PRICE;
// 스프링 빈으로 등록하겠다(스프링 기동 시에 스프링 빈으로 등록하겠다)
// ApplicationRunner 인터페이스 사용하겠다
@Component
public class TestDataRunner implements ApplicationRunner {
// 원래는 Autowired 안했었음, 스프링이 권장하진 않음
@Autowired
UserService userService;
@Autowired
ProductRepository productRepository;
@Autowired
UserRepository userRepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
ItemSearchService itemSearchService;
// run 하면, 스프링 기동 시에 아래 코드들이 실행된다
@Override
public void run(ApplicationArguments args) throws Exception {
// 테스트 User 생성
User testUser = new User("슈가", passwordEncoder.encode("123"), "sugar@sparta.com", UserRoleEnum.USER);
testUser = userRepository.save(testUser);
// 테스트 User 의 관심상품 등록
// 검색어 당 관심상품 10개 등록
createTestData(testUser, "신발");
createTestData(testUser, "과자");
createTestData(testUser, "키보드");
createTestData(testUser, "휴지");
createTestData(testUser, "휴대폰");
createTestData(testUser, "앨범");
createTestData(testUser, "헤드폰");
createTestData(testUser, "이어폰");
createTestData(testUser, "노트북");
createTestData(testUser, "무선 이어폰");
createTestData(testUser, "모니터");
}
private void createTestData(User user, String searchWord) throws IOException {
// 네이버 쇼핑 API 통해 상품 검색
List<ItemDto> itemDtoList = itemSearchService.getItems(searchWord);
List<Product> productList = new ArrayList<>();
// itemDtoList를 productList로 변환
for (ItemDto itemDto : itemDtoList) {
Product product = new Product();
// 관심상품 저장 사용자
product.setUserId(user.getId());
// 관심상품 정보
product.setTitle(itemDto.getTitle());
product.setLink(itemDto.getLink());
product.setImage(itemDto.getImage());
product.setLprice(itemDto.getLprice());
// 희망 최저가 랜덤값 생성
// 최저 (100원) ~ 최대 (상품의 현재 최저가 + 10000원)
int myPrice = getRandomNumber(MIN_MY_PRICE, itemDto.getLprice() + 10000);
product.setMyprice(myPrice);
productList.add(product);
}
productRepository.saveAll(productList); // 전부 DB에 저장된다
}
public int getRandomNumber(int min, int max) {
return (int) ((Math.random() * (max - min)) + min);
}
}
실행 후 네트워크 가서 확인해보면,
- 요청 시에 product?sortBy=id&isAsc=true ... 로 요청됨
- 요청 결과로 온 reponse(preview) 확인해보기
content : 상품 검색된 것
나머지는 스프링이 만든 것
- 원래는 pagenation 관련 테스트 코드 추가해야함
- But, 여기선 그냥 에러만 안나게 sample data로
page, size, sortBy, isAsc 선언만 해주기
@Test
@Order(3)
@DisplayName("회원이 등록한 모든 관심상품 조회")
void test3() {
// given
int page = 0;
int size = 10;
String sortBy = "id";
boolean isAsc = false;
...
@Test
@Order(5)
@DisplayName("회원이 등록한 모든 관심상품 조회")
void test5() {
// given
int page = 0;
int size = 10;
String sortBy = "id";
boolean isAsc = false;
...
요구사항
a. 폴더 생성
b. 관심상품에 폴더 설정
c. 폴더 별 조회
폴더명 : 회원이 등록한 폴더 이름 저장
회원 ID : 폴더를 등록한 회원의 ID를 저장
@Entity
public class Folder {
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Long userId;
}
// 1. 로그인한 회원 (user1) 의 id 를 조회
Long userId = user1.getId();
// 2. userId 로 저장된 모든 folder 조회
List<Folder> folders = folderRepository.findAllByUserId(userId);
public interface FolderRepository extends JpaRepository<Folder, Long> {
List<Folder> findAllByUserId(Long userId);
}
// 1. folder1 의 userId 를 조회
Long userId = folder1.getUserId();
// 2. userId 로 저장된 회원 조회
User user = userRepository.findById(userId);
-> 이를 JPA 연관관계로 해결할 수 있음
public class User {
@OneToMany
private List<Folder> folders;
}
List<Folder> folders = user.getFolders();
public class Folder {
private Long userId;
}
// 1. 로그인한 회원 (user1) 의 id 를 조회
Long userId = user1.getId();
// 2. userId 로 저장된 모든 folder 조회
List<Folder> folders = folderRepository.findAllByUserId(userId);
public class Folder{
@ManyToOne
private User user;
}
folder.getUser();
- 폴더에 user 객체 가진 것은 Has-a 관계라고함(객체가 객체를 가짐)
@ManyToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;
name : 외래키 명
nullable : 외래키 null 허용 여부
설명 | API | 입력 | 출력 |
---|---|---|---|
회원의 폴더 생성 | POST /api/folders | foldersNames (JSON) : 생성된 폴더명들 | folders (JSON) : 생성된 폴더의 정보들 |
회원의 폴더 조회 | GET / | index.html model 추가 -> folders |
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Setter
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class Folder {
// ID가 자동으로 생성 및 증가합니다.
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
@Column(nullable = false)
private String name;
@ManyToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;
// 클라이언트로부터 폴더 명이랑 사용자 정보 넘어온다
public Folder(String name, User user) {
this.name = name;
this.user = user;
}
}
import lombok.Getter;
import java.util.List;
@Getter
public class FolderRequestDto {
List<String> folderNames;
}
import com.sparta.springcore.dto.FolderRequestDto;
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.User;
import com.sparta.springcore.security.UserDetailsImpl;
import com.sparta.springcore.service.FolderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
// JSON 형태이므로
@RestController
public class FolderController {
private final FolderService folderService;
// FolderService DI 받기
@Autowired
public FolderController(FolderService folderService) {
this.folderService = folderService;
}
@PostMapping("api/folders")
public List<Folder> addFolders(
@RequestBody FolderRequestDto folderRequestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
// 그냥 DTO 대신에 Entity 객체를 List로 넘겨주자
List<String> folderNames = folderRequestDto.getFolderNames();
User user = userDetails.getUser();
List<Folder> folders = folderService.addFolders(folderNames, user);
return folders;
}
}
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.User;
import com.sparta.springcore.repository.FolderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class FolderService {
private final FolderRepository folderRepository;
@Autowired
public FolderService(FolderRepository folderRepository) {
this.folderRepository = folderRepository;
}
// 로그인한 회원에 폴더들 등록
public List<Folder> addFolders(List<String> folderNames, User user) {
List<Folder> folderList = new ArrayList<>();
for (String folderName : folderNames) {
Folder folder = new Folder(folderName, user);
folderList.add(folder);
}
return folderRepository.saveAll(folderList);
}
// 로그인한 회원이 등록된 모든 폴더 조회
public List<Folder> getFolders(User user) {
return folderRepository.findAllByUser(user); // model에 추가 위함
}
}
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface FolderRepository extends JpaRepository<Folder, Long> {
List<Folder> findAllByUser(User user); // model에 추가하기 위함
}
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.UserRoleEnum;
import com.sparta.springcore.security.UserDetailsImpl;
import com.sparta.springcore.service.FolderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
public class HomeController {
private final FolderService folderService;
@Autowired
public HomeController(FolderService folderService) {
this.folderService = folderService;
}
@GetMapping("/")
public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
model.addAttribute("username", userDetails.getUsername());
if (userDetails.getUser().getRole() == UserRoleEnum.ADMIN) {
model.addAttribute("admin_role", true);
}
// UI에 반영되도록
List<Folder> folderList = folderService.getFolders(userDetails.getUser());
model.addAttribute("folders", folderList);
return "index";
}
}
설명 | API | 입력 | 출력 |
---|---|---|---|
폴더 전체 조회 | GET /api/folders | List<Folder> | |
폴더 추가 | POST /api/products/{productId}/folder | {productId}: 관심상품 ID [Form 형태] folderId: 추가할 폴더 | 폴더가 추가된 관심상품 ID |
상품 조회 시 폴더정보 추가 | GET /api/products | product 정보에 folderList 추가 |
-> 폴더 추가 : 관심 상품에 추가하는 것, folderId는 Form 형태로 Body에 넣어줌
// 회원이 등록한 모든 폴더 조회
@GetMapping("api/folders")
public List<Folder> getFolders(
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
return folderService.getFolders(userDetails.getUser());
}
// 상품에 폴더 추가
@PostMapping("/api/products/{productId}/folder")
public Long addFolder(
@PathVariable Long productId, // 주소 안에 있는 거 가져옴
@RequestParam Long folderId, // Form 형태로 받아오는 것
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
User user = userDetails.getUser();
Product product = productService.addFolder(productId, folderId, user);
return product.getId(); // Client에게 보내줄 것
}
@Transactional // 객체에 저장된 애를 DB에 적용하기 위해 필요
public Product addFolder(Long productId, Long folderId, User user) {
// 1) 상품을 조회합니다.
Product product = productRepository.findById(productId)
.orElseThrow(() -> new NullPointerException("해당 상품 아이디가 존재하지 않습니다."));
// 2) 관심상품을 조회합니다.
Folder folder = folderRepository.findById(folderId)
.orElseThrow(() -> new NullPointerException("해당 폴더 아이디가 존재하지 않습니다."));
// 3) 조회한 폴더와 관심상품이 모두 로그인한 회원의 소유인지 확인합니다.
Long loginUserId = user.getId();
if (!product.getUserId().equals(loginUserId) || !folder.getUser().getId().equals(loginUserId)) {
throw new IllegalArgumentException("회원님의 관심상품이 아니거나, 회원님의 폴더가 아닙니다~^^");
}
// 4) 상품에 폴더를 추가합니다.
product.addFolder(folder);
return product;
}
- getProducts 함수에서 추가로 folderRepository에서 get 할
필요 없이 자동으로 Product 가져올 때 folderList 가져옴
(Product에서 ManyToMany 해서)
- getProducts에서 ProductController로 Page<Product>로 넘겨줘
JSON으로 변환하는 순간에 Folder들이 들어옴
@ManyToMany // fetch 타입이 있음(fetch= 이렇게 할 수도 있음)
private List<Folder> folderList;
// Product에 폴더를 추가
public void addFolder(Folder folder) {
this.folderList.add(folder);
}
-> N:N 관계를 해서 DB에 PRODUCT_FOLDER_LIST가 자동으로 생김(중간의 관계를 맺음)
설명 | API | 입력 | 출력 |
---|---|---|---|
폴더 별 상품 조회 | GET /api/folders/{folderId}/products | {folderId} : 조회를 원하는 폴더 id | Page<Product> |
-> Pagenation은 그대로 적용해야함
(Spring data JPA 이용, folderId & size 등등 들어가야함)
// 회원이 등록한 폴더 내 모든 상품 조회
@GetMapping("api/folders/{folderId}/products")
public Page<Product> getProductsInFolder(
@PathVariable Long folderId,
@RequestParam int page,
@RequestParam int size,
@RequestParam String sortBy,
@RequestParam boolean isAsc,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
page = page - 1;
return folderService.getProductsInFolder(
folderId,
page,
size,
sortBy,
isAsc,
userDetails.getUser()
);
}
// 회원 ID 가 소유한 폴더에 저장되어 있는 상품들 조회
public Page<Product> getProductsInFolder(
Long folderId,
int page,
int size,
String sortBy,
boolean isAsc,
User user
) {
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
Sort sort = Sort.by(direction, sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
Long userId = user.getId();
return productRepository.findAllByUserIdAndFolderList_Id(userId, folderId, pageable);
}
import com.sparta.springcore.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
Page<Product> findAllByUserId(Long userId, Pageable pageable);
// folderList_Id는 결국 folderList에서의 id를 가지고 조회
// folderId
Page<Product> findAllByUserIdAndFolderList_Id(Long userId, Long folderId, Pageable pageable);
}
해결방법: 생성할 폴더명이 입력으로 들어왔을 때, DB에 동일 폴더명이 없는 경우에만 생성
수정할 파일 : FolderService.java, FolderRepository.java
추가로 테스트 코드 에러 해결(ProductServiceTest.java)
service > FolderService
// 로그인한 회원에 폴더들 등록
public List<Folder> addFolders(List<String> folderNames, User user) {
// 1) 입력으로 들어온 폴더 이름을 기준으로, 회원이 이미 생성한 폴더들을 조회합니다.
List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);
List<Folder> folderList = new ArrayList<>();
for (String folderName : folderNames) {
// 2) 이미 생성한 폴더가 아닌 경우만 폴더 생성
if (!isExistFolderName(folderName, existFolderList)) {
Folder folder = new Folder(folderName, user);
folderList.add(folder);
}
}
return folderRepository.saveAll(folderList);
}
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface FolderRepository extends JpaRepository<Folder, Long> {
List<Folder> findAllByUser(User user);
List<Folder> findAllByUserAndNameIn(User user, List<String> names);
}
4주차에서는 앞서 언급만 했었던 Spring Data JPA에 대해 자세하게 배웠다. 데이터베이스 수업에서 배우던 연관관계를 직접 DB를 통해 보면서 실습하게 되어 흥미로웠다. 복습하면서 이해하기 어려운 부분이 많아서 강의를 다시 들으면서 정리해보았다. 드디어 1주 남았다.. 아즈아😬
출처: 스파르타 코딩 클럽