JPA 기초 (2. 적용)

박영준·2022년 11월 28일
0

Java

목록 보기
9/111

Member Entity

//Member.java
package com.sparta.springjpa.entity;

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

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

@Entity     //1. Entity를 사용하겠다는 표시를 먼저 달기
@Getter     //2. 값을 가져올 때 사용
@NoArgsConstructor      //3. 기본생성자를 만들어줌

public class Member {       //user는 사용 불가. H2 DB가 2.0버전 되면서 user라고 하면 테이블 생성이 안됨 (대신에 Member 테이블을 사용)
    @Id     //4.
    @GeneratedValue(strategy = GenerationType.IDENTITY)     //5.
    private Long id;        //6.
    @Column(nullable = false)       //8. nullable = false 가 반드시 들어갈 수 있도록 추가
    private String memberName;      //7. Member에는 멤버이름이 필요하므로, memberName 추가

    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)        //9.
    private List<Orders> orders = new ArrayList<>();        //10.

    public Member(String memberName) {      //11. 생성자 추가
        this.memberName = memberName;
    }
}

Food Entity

//Food.java
package com.sparta.springjpa.entity;

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

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

@Entity     //1. Entity를 사용하겠다는 표시를 먼저 달기
@Getter     //2. 값을 가져올 때 사용
@NoArgsConstructor      //3. 기본생성자를 만들어줌

public class Food {
    @Id     //4.
    @GeneratedValue(strategy = GenerationType.IDENTITY)     //5.
    private Long id;        //6.
    @Column(nullable = false)       //8. nullable = false 가 반드시 들어갈 수 있도록 추가
    private String foodName;      //7. Member에는 멤버이름이 필요하므로, memberName 추가
    @Column(nullable = false)       //13. 음식 가격 설정을 위해 추가
    private int price;

    //9. Order.java에서 @ManyToMany 를 사용했으므로, 이걸 받아줄 부분이 필요 --> 음식 1개에 주문이 N개가 될 수 있으므로
    //11. mappedBy를 사용해서, food와 연관관계를 만듦 (연관관계의 주인을 지정해주는 것) --> Order.java에서 @JoinColumn(name = "food_id")에서 food_id의 주인이 누구인지 알려주는 부분
    //12. FetchType 은??
    @OneToMany(mappedBy = "food", fetch = FetchType.EAGER)
    private List<Orders> orders = new ArrayList<>();        //10. 여러가지의 order가 들어와야하므로 리스트 형식으로 받음. 초기값을 주기위해 new ArrayList<>() 로 설정

    public Food(String foodName, int price) {       //14. foodName과 price를 받아주는 생성자 추가
        this.foodName = foodName;
        this.price = price;
    }

}

Orders Entity

//Orders.java
package com.sparta.springjpa.entity;

//@Id에 따라 import org.springframework.data.annotation.Id; 로 했을 때, Error creating bean with name 'entityManagerFactory' defined in class path resource 오류 발생
    //--> Entity Id 참조가 잘못돼서 발생하는 오류 --> import javax.persistence.Id; 로 변경 후, 정상 작동
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter     //1. Entity를 사용하겠다는 표시를 먼저 달기
@Entity     //2. 값을 가져올 때 사용
@NoArgsConstructor      //3. 기본생성자를 만들어줌
public class Orders {       //Order는 예약어이기때문에, 부득이하게 Orders로 클래스명 설정
    @Id     //4.
    @GeneratedValue(strategy = GenerationType.IDENTITY)     //5.
    private Long id;        //6.

    @ManyToOne     //7. Food의 ID, Member의 ID를 받아야하므로
    @JoinColumn(name = "food_id")       //8. 'Order 테이블의 음식 ID와 Food 테이블의 ID가 JOIN 된다.' 라는 의미 --> 주인은 Food 테이블이다!
    private Food food;  //7.

    @ManyToOne     //9. member 도 food와 똑같이 설정해줌
    @JoinColumn(name = "member_id")
    private Member member;

    public Orders(Food food, Member member) {       //10. order을 만들때 Food와 Member를 추가해줘야하므로, Food와 Member를 함께 넣어주는 생성자 추가
        this.food = food;
        this.member = member;
    }
}

MemberRepository

//MemberRepository.java
//entity 패키지의 Food, Member, Orders 엔티티 객체를 데이터베이스와 연결해주기 위해 만듦
package com.sparta.springjpa.repository;

import com.sparta.springjpa.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {     //1.

}

FoodRepository

//FoodRepository.java
//entity 패키지의 Food, Member, Orders 엔티티 객체를 데이터베이스와 연결해주기 위해 만듦
package com.sparta.springjpa.repository;

import com.sparta.springjpa.entity.Food;        //<Food, Long>에서 import 자동 추가 안되면 Alt + Enter 눌러서 수동으로 추가하기!
import org.springframework.data.jpa.repository.JpaRepository;
//Java Class - interface로 만듦
//1. JpaRepository의 상속을 받는 FoodRepository interface를 만듦(extends JpaRepository 를 추가)
//1. <Food, Long>: 어떤 테이블과 연결할지 명시 --> '이 JpaRepository는 FoodRepository와 연결하겠다.' 는 의미
//1. ID를 Long 타입으로 지정했으므로, 여기서도 Long 타입으로 주면 됨
public interface FoodRepository extends JpaRepository<Food, Long> {
}

OrdersRepository

//OrdersRepository.java
//entity 패키지의 Food, Member, Orders 엔티티 객체를 데이터베이스와 연결해주기 위해 만듦
package com.sparta.springjpa.repository;

import com.sparta.springjpa.entity.Orders;
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrdersRepository extends JpaRepository<Orders, Long> {     //1.
}

ApplicationRunner

//Restaurant.java
//앞서 만든 것들이 잘 사용되는지 확인하기 위한 Java class
package com.sparta.springjpa;

import com.sparta.springjpa.entity.Food;
import com.sparta.springjpa.entity.Member;
import com.sparta.springjpa.entity.Orders;
import com.sparta.springjpa.repository.FoodRepository;
import com.sparta.springjpa.repository.MemberRepository;
import com.sparta.springjpa.repository.OrdersRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

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

@Component
@RequiredArgsConstructor
public class Restaurant implements ApplicationRunner {

    private final FoodRepository foodRepository;
    private final OrdersRepository ordersRepository;
    private final MemberRepository memberRepository;

    @Override
    public void run(ApplicationArguments args) throws Exception {       //1. 스프링부트가 시작 될때, run 메소드가 실행됨

        List<Food> foods = new ArrayList<>();
        Food food1 = new Food("후라이드", 10000);
        foods.add(food1);
        Food food2 = new Food("양념치킨", 12000);
        foods.add(food2);
        Food food3 = new Food("반반치킨", 13000);
        foods.add(food3);
        Food food4 = new Food("고구마피자", 9000);
        foods.add(food4);
        Food food5 = new Food("아보카도피자", 110000);
        foods.add(food5);
        foodRepository.saveAll(foods);      //foodRepository에 saveAll를 사용해서, List 안에 있는 Food들을 한번에 저장

        List<Member> members = new ArrayList<>();
        Member member1 = new Member("삼식이");
        members.add(member1);
        Member member2 = new Member("먹깨비");
        members.add(member2);
        memberRepository.saveAll(members);

        System.out.println("==================================================================");

        System.out.println("Member 데이터");
        List<Member> findMembers = memberRepository.findAll();      //memberRepository와 연결하고, findAll() 메소드를 사용해서 테이블에 있는 Member들을 List 형식으로 받아와서 사용
        for (Member findMember : findMembers) {
            System.out.println("findMember = " + findMember.getMemberName());
        }

        System.out.println("==================================================================");

        System.out.println("Food 데이터");
        List<Food> findFoods = foodRepository.findAll();        //foodRepository와 연결하고, findAll() 메소드를 사용해서 테이블에 있는 Food들을 List 형식으로 받아와서 사용
        for (Food findFood : findFoods) {
            System.out.println("findFood = " + findFood.getFoodName());
        }

        List<Orders> ordersList = new ArrayList<>();
        Orders orders1 = new Orders(findFoods.get(0), findMembers.get(0));      //orders를 만들 때, Food 객체와 Member 객체를 넣어줌 --> Orders.java에서 public Orders(Food food, Member member) 를 보면 확인 가능
        ordersList.add(orders1);
        Orders orders2 = new Orders(findFoods.get(3), findMembers.get(1));
        ordersList.add(orders2);
        Orders orders3 = new Orders(findFoods.get(4), findMembers.get(1));
        ordersList.add(orders3);
        Orders orders4 = new Orders(findFoods.get(2), findMembers.get(0));
        ordersList.add(orders4);
        Orders orders5 = new Orders(findFoods.get(2), findMembers.get(0));
        ordersList.add(orders5);
        Orders orders6 = new Orders(findFoods.get(1), findMembers.get(1));
        ordersList.add(orders6);
        Orders orders7 = new Orders(findFoods.get(1), findMembers.get(0));
        ordersList.add(orders7);
        Orders orders8 = new Orders(findFoods.get(3), findMembers.get(1));
        ordersList.add(orders8);
        ordersRepository.saveAll(ordersList);       //위에서 findAll() 메소드로 가져온 Member들과 Food들을 인덱스(리스트형식이므로. 임의로 지정한 순서대로 오더를 만든.)를 통해,
        //saveAll() 메소드를 사용해서 데이터베이스에 Insert한다
        System.out.println("==================================================================");
        int num = 1;

        System.out.println("Orders 데이터");       //Orders 테이블에 데이터들이 어떻게 들어가있는지 확인하는 코드
        List<Orders> orderList = ordersRepository.findAll();

        for (Orders orders : orderList) {
            System.out.println(num);
            System.out.println("주문한 사람 = " + orders.getMember().getMemberName());
            System.out.println("주문한 음식 = " + orders.getFood().getFoodName());
            num++;
        }

        System.out.println("==================================================================");
        System.out.println("삼식이 주문한 음식");       //삼식이라는 member가 어떤 음식을 주문했는지 확인하는 코드
        Member samsik = memberRepository.findById(1L).orElseThrow(
                ()->new RuntimeException("없음")		//예외 처리
        );

        num = 1;
        for (Orders orders : samsik.getOrders()) {
            System.out.println(num);
            System.out.println("주문한 음식 = " + orders.getFood().getFoodName());
            System.out.println("주문한 음식 가격 = " + orders.getFood().getPrice());
            num++;
        }


        System.out.println("==================================================================");
        System.out.println("아보카도피자 주문한 사람");        //음식 中 아보카도피자를 주문한 사람이 누구인지 찾는 코드
        Food abocado = foodRepository.findById(5L).orElseThrow(
                ()->new RuntimeException("없음")		//예외 처리
        );

        for (Orders order : abocado.getOrders()) {
            System.out.println("주문한 사람 = " + order.getMember().getMemberName());
        }


    }
}

데이터의 생성(created_at), 수정(last_modified_at) 시간 관리

  • '상속' 을 이용
  • 포스팅, 게시글, 댓글 등 다양한 데이터에 매우 자주 활용
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) 
public class Timestamped {		//1. Timestamped라는 클래스를 만들고

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;	//2. Timestamped 클래스 안에 createdAt 컬럼, modifiedAt 컬럼을 만들어서 

    @LastModifiedDate
    @Column
    private LocalDateTime modifiedAt;
}
@Entity // 게시글
public class Post extends Timestamped {		//3. Timestamped 클래스를 Post 클래스라는 Entity에 상속함(extends Timestamped)
	@Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false, unique = true)
    private String content;
}
  • 1 ~ 3
    Post 클래스는 실제로는 reatedAt, modifiedAt이 없지만, Timestamped를 상속하는 것만으로도 데이터베이스에 추가될 때 자동으로 추가되는 시간이 설정됨

  • 그 외에 @ 어노테이션들
    위의 기능들을 사용하기 위해 달아주는 것들

추가적으로

@SpringBootApplication이 있는 class에 @EnableJpaAuditing 추가!

  • @EnableJpaAuditing 어노테이션
    - Spring Audit 기능을 활용하기 위한 것
    - Audit: Spring Data JPA에서 시간에 대해서 자동으로 값을 넣어주는 기능

Spring Data JPA

Spring Data JPA 란?

  • 예상 가능하고 반복적인 코드들 → Spring Data JPA 가 대신 작성
  • epostiory 인터페이스만 작성하면, 필요한 구현은 스프링이 대신 알아서 처리

예시

  1. 상품 Enitity 선언

    @Entity
    public class Product extends Timestamped {
            @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        private Long id;
        private Long userId;
        private String title;
        private String image;
        private String link;
        private int lprice;
        private int myprice;
    }
  2. Spring Data JPA) 상품 Repository 생성

    public interface ProductRepository extends JpaRepository<Product, Long> {
    }
  3. Spring Data JPA) 기본 제공해 주는 기능

    // 1. 상품 생성
    Product product = new Product(...);
    productRepository.save(product);	//save()는 하나의 객체만 저장할 때 사용
    
    // 2. 상품 전체 조회
    List<Product> products = productRepository.findAll();
    
    // 3. 상품 전체 개수 조회
    long count = productRepository.count();
    
    // 4. 상품 삭제
    productRepository.delete(product);
  4. ID 외의 필드에 대한 추가 기능은 interface 만 선언해 주면, 구현은 Spring Data JPA 가 대신!!

    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);
    }

적용해보기

위에서 했던 걸 이용해보는데, 내가 어떤 멤버를 찾고 싶다면?

  1. ApplicationRunner(Restaurant.java)에서 삼식이 같은 이름을 통해서 조회해보려고 하는데, findById() 만 있고 별로 적당한 게 안 보였음

  2. MemberRepository에서 아래 코드 추가

    Optional<Member> findByMemberName(String memberName);
  3. 다시 ApplicationRunner에서 아래 코드 추가

    Member member = memberRepository.findByMemberName("삼식이").orElseThrow(
            () -> new RuntimeException("삼식이 없음")
    );
    
    System.out.println("member.getMemberName() = " + member.getMemberName());
    System.out.println("member.getId() = " + member.getId());

    RUN 돌려보면, 추가돼있음!


추가 공부

  • JPA 영속성 컨텍스트 이해
  • 1차 캐시 이해

참고

Spring Data JPA 의 Query Methods(Spring Data JPA 추가기능 구현방법): https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods

profile
개발자로 거듭나기!

0개의 댓글