Spring MVC JPA

김병수·2022년 11월 7일
0
post-thumbnail

JPA

Java에서 사용하는 ORM(Object Relational Mapping) 기술 표준으로 사용되는 인터페이스이다. 표준 사양을 구현한 구현체는 따로 있다는 것을 의미한다. 가장 대표적인 구현체로 Hibernate ORM이 있다.

데이터 엑세스 계층에서의 JPA 위치

  • JPA는 데이터 엑세스 계층의 상단에 위치한다.
  • 데이터 저장, 조회 등의 작업은 JPA를 거쳐 JPA의 구현체인 Hibernate ORM을 통해서 이루어지며 Hibernate ORM은 내부적으로 JDBC API를 이용해서 데이터베이스에 접근하게 된다.

영속성 컨텍스트(Persistence Context)

JPA에서는 태이블과 매핑되는 엔티니 객체 정보를 영속성 컨텍스트라는 곳에 보관해서 애플리케이션 네에서 오래 지속 되도록 한다. 영속성 컨텍스트내에서 1차 캐시라는 영역과 쓰기 지연 SQL 저장소라는 영역이 있습니다.

JPA API로 영속성 컨텍스트 이해하기

build.gragle 설정

dependencies {
		...
		implementation 'org.springframework.boot:spring-boot-starter-data-jpa'		
}

JPA 설정(application.yml)

spring:
	h2:
		console:
			enabled: true
			path: /h2    # Context path
		datasource:
			url: jdbc:h2:mem:test    # JDBC URL
		jpa:
			hibernate:
				ddl-auto: create   # 스키마 자동 생성
			show-sql: true    # SQL 쿼리 출력

영속성 컨텍스트에 엔티티 저장

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

/**
 * @Entity 애너테이션과 @Id 애너테이션을 추가해주면 JPA에서 해당 클래스를 엔티티 클래스로 인식한다.
 */
@Getter
@Setter
@NoArgsConstructor
@Entity
public class Member {
    /**
     * @GenerateValue 애너테이션은 식별자를 생성해주는 전략을 지정할 때 사용한다.
     * 식별자에 해당하는 멤버 변수에 @GeneratedValue 애너테이션을 추가하면 데이터베이스 테이블에서 기본키가 되는 식별자를 자동으로 설정해준다.
     **/
    @Id

    @GeneratedValue
    private Long memberId;
    private String email;

    public Member(String email) {
        this.email = email;
    }
}

샘플 코드 실행을 위한 Configuration 클래스 생성

import com.codestates.member.entity.Member;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;

/**
 * @Configuration 애너테이션을 추가하면 Spring에서 Bean 검색 대상인 Configuration 클래스로 간주해서
 * @Bean 애너테이션이 추가된 메서드를 검색한 후, 해당 메서드에서 리턴하는 객체를 Spring Bean으로 추가해준다.
 */
@Configuration
public class JpaBasicConfig {
    /**
     * JPA의 영속성 컨텍스트를 관리하는 클래스이다.
     * EntityManager 클래스의 객체는 EntityMangerFactory 객체를 Spring으로부터 DI 받을 수 있다.
      */
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) {
        this.em = emFactory.createEntityManager();
        this.tx = em.getTransaction();

        /**
         * EntityManageFactory의 createEntityManager()메서드를 이용해서 EntityManager 클래스의 객체를 얻을 수 있다.
         * 이제 이 EntityManager 클래스의 객체를 통해서 JPA의 API메서드를 사용할 수 있다.
         */
        return args -> {
            example01();
        };
    }

    private void example01() {
        Member member = new Member("gksmfcksqls@gmail.com");

        // persist(member) 메서드를 호출하면 영속성 컨텍스트에 member 객체의 정보들이 저장된다.
        em.persist(member);

        /**
         *  find(Member.class, 1L) 메서드로 영속성 컨텍스트에 member 객체가 잘 저장되었는지 조회할 수 있다.
         *  첫 번째 파라미터는 조회할 엔티티 클래스의 타입이다.
         *  두 번째 파라미터는 조회할 엔티티 클래스의 식별자 값이다.
         */
        Member resultMember = em.find(Member.class, 1L);
        System.out.println("Id: " + resultMember.getMemberId() +
                ", email: " + resultMember.getEmail());
    }
}

em.persist(member)를 호출하면 1차 캐시에 member 객체가 저장되고, 이 객체는 쓰기 지연 SQL 저장소에 INSERT 쿼리 형태로 등록된다. 여기서 tx.commit()을 사용하면 INSERT 쿼리가 실행되어 쓰기 지연 SQL 저장소에서 사라진다. em.find()를 사용하면 1차 캐시에서 해당 객체가 있는지 조회하고, 없으면 테이블에 SELECT 쿼리를 전송해서 조회한다.

영속성 컨텍스트와 테이블에 엔티티 업데이트

    public void example04() {
        tx.begin();
        // member 객체를 영속성 컨텍스트의 1차 캐시에 저장한다.
        em.persist(new Member("gksmfcksqls@gmail.com"));
        // 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리문을 실행한다.
        tx.commit();

        tx.begin();
        /**
         * 저장된 member 객체를 영속성 컨텍스트의 1차 캐시에서 조회한다. (테이블에서 조회하는 것이 아님)
         * 영속성 컨텍스트의 1차 캐시에 이미 저장된 객체가 있기 때문에 영속성 컨텍스트에서 조회한다.
         */
        Member member1 = em.find(Member.class, 1L);
        /**
         * setter 메서드로 이메일 정보를 변경한다.
         * em.update() 같은 JPA API가 있을 것 같지만 setter 메서도르 값을 변경하기만 하면 업데이트 로직이 완성된다.
         */
        member1.setEmail("Ekdcksqls@gmail.com");
        // tx.commit()을 실행하면 쓰기 지연 SQL 저장소에 등록된 UPDATE 쿼리가 실행된다.
        tx.commit();
    }

em.find()로 해당 객체를 찾고 set메서드를 사용하여 업데이트 후 커밋하면 UPDATE 쿼리가 실행된다.

영속성 컨텍스트와 테이블의 엔티티 삭제

private void example05() {
        tx.begin();
        // Member 클래스의 객체를 영속성 컨텍스트의 1차 캐시에 저장한다.
        em.persist(new Member("gksmfcksqls@gmail.com"));
        // tx.commit()을 호출해서 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 등록된 INSERT 쿼리를 실행한다.
        tx.commit();

        tx.begin();
        // 테이블에 저장된 Member 클래스의 객체를 영속성 컨텍스트의 1차 캐시에서 조회한다.
        Member member = em.find(Member.class, 1L);
        // em.remove()를 통해 영속성 컨텍스트의 1차 캐시에 있는 엔티티 제거를 요청한다.
        em.remove(member);
        // tx.commit()을 실행하면 영속성 컨텍스트의 1차 캐시에 있는 엔티티를 제공하고 쓰기 지연 SQL 저장소에 등록된 DELETE 쿼리가 실행된다.
        tx.commit();
    }

em.remove()로 해당 객체를 삭제하고 DELETE 쿼리가 실행된다.

JPA 연관 관계

연관 관계 정의 규칙

  • 방향 : 단방향, 양방향
  • 연관 관계의 주인 : 양방향일 때, 연관 관계에서 관리 주체
  • 다중성 : 다대일(N:1), 일대일(1:1), 다대다(N:M)

방향

DB 테이블은 외래키 하나로 양쪽 테이블 조인이 가능하지만 객체는 참조용 필드가 있을 때만 다른 객체를 참조하는 것이 가능하다. 하나의 객체만 참조용 필드을 갖고 있다면 단방향, 두 객체 모두 갖고 있다면 양방향 관계라고 한다. 방향을 매핑할 때는 기본적으로 단방향 매핑을 우선적으로 하고 역방향으로 객체 탐색이 필요하다면 추가하는 것이 좋다.

연관 관계의 주인

연관 관계의 주인은 두 객체 사이에서 조회, 수정, 삭제, 저장을 할 수 있지만, 주인이 아니면 조회만 가능하다. 외래 키가 있는 곳을 연관 관계의 주인으로 지정해주고 주인이 아닌 개체에 mappedBy 속성을 사용하여 주인을 지정해줘야 한다.

다중성

다대일(N:1) 단방향

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

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

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

    @Column(nullable = false, updatable = false, unique = true)
    private String email;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

다대일(N:1) 양방향

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

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

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

    @Column(nullable = false, updatable = false, unique = true)
    private String email;

    @Column(length = 100, nullable = false)
    private String name;

    @Column(length = 13, nullable = false, unique = true)
    private String phone;

    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<>();

단방향의 경우 order 클래스의 필드로 member 클래스를 참조하였고 @ManyToOne 애너테이션으로 관계를 매핑하였다. 양방향의 경우 member 클래스의 참조 필드 orders를 생성하고 @OneToMany 애너테이션의 mappedBy 속성을 지정하여 연관 관계의 주인을 지정하였다.

일대일(1:1) 연관 관계

다대일 연관 관계 매핑과 방법은 동일하되 @OneToOne 애너테이션을 사용한다는 차이가 있다.

다대다(N:M)
다대다의 경우 두 테이블 간의 관계을 정의하는 테이블을 생성하여 그 테이블과 다대일 관계를 매핑한다.

@Getter
@Setter
@Entity(name = "ORDERS")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderId;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST;

    @OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST)
    private List<OrderCoffee> orderCoffees = new ArrayList<>();
}

@Getter
@Setter
@Entity
public class Coffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long coffeeId;

    @Column(length = 100, nullable = false)
    private String korName;

    @Column(length = 100, nullable = false)
    private String engName;

    @Column(length = 5, nullable = false)
    private Integer price;

    @Column(length = 3, nullable = false, unique = true)
    private String coffeeCode;

    @OneToMany(mappedBy = "coffee")
    private List<OrderCoffee> orderCoffees = new ArrayList<>();
}

@Getter
@Setter
@Entity
public class OrderCoffee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderCoffeeId;

    @Column(nullable = false)
    private int quantity;

    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "COFFEE_ID")
    private Coffee coffee;
}

order과 coffee가 다대다 관계이므로 해당 관계를 정의하는 ordercoffee과 다대일 관계로 매핑한다.

profile
BE 개발자를 꿈꾸는 대학생

0개의 댓글