새소식

Java

[스프링 부트와 JPA 활용 1] 정리

  • -
728x90
[스프링 부트와 JPA 활용 1] 정리

📌

김영한님의 강의, 스프링 부트와 JPA 활용 1을 수강하며 들은 내용을 정리하려고 한다. 코드 위주의 설명이나 중요한 개념에 대해서는 설명을 하고 넘어가겠다. 프로젝트 환경 설정과 데이터 베이스 설정 등의 개인적인 설정은 넘어가겠다.

 

스펙 : Java 11, Intellij IDEA, H2, thymeleaf .. 


 

📚 JPA와 DB 설정, 동작 확인

이 부분을 설명하고 넘어가는 것은 실제로 필자가 강의와 똑같이 설정했으나 많은 에러가 발생하여 삽질한 경험이 있어 간략하게 설명을 한다.

결과적으로는 강의에서 설명하는 것처럼 띄어쓰기에 주의해야 한다! 띄어쓰기에 유의하여 같은 실수를 반복하지 않길!

* yml 과 properties는 의미는 동일하고 작성 형식의 차이가 있다. 

 

application.yml

spring:
  datasource:
    url: jdbc:h2:mem:test # in Memory
    username: sa
    password:
    driver-class-name: org.h2.Driver

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show-sql: true
        format_sql: true

logging.level:
 org.hibernate.SQL: debug
 org.hibernate.type: trace

 

 

옵션설명

spring.jpa.hibernate.ddl-auto:create

: 이 옵션을 사용하여 애플리케이션 실행 시점에 테이블이 있으면 drop하고 재생성을 진행하였다.

spring.jpa.properties.hibernate.show_sql

: hibernate 가 DB 에 날리는 모든 쿼리 (DDL, DML) 을 콘솔에 남겨준다.

spring.jpa.properties.hibernate.format_sql

: 쿼리를 보기 편하게 ? 예쁘게 표기해준다.

 

 


💡 도메인 분석 설계, 요구사항 분석

전체적인 관계는 위와 같다. 회원, 주문, 상품의 관계에서 회원은 여러 상품을 주문할 수 있고, 한번의 주문에 여러 상품을 선택할 수 있기 때문에 주문과 상품은 다대다 관계다.

하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않는다. 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어냈다.

상품은 도서, 음반, 영화로 구분되는데 상품이라는 공통 속성을 사용하므로 상속 구조로 표현했다. 이를 엔티티로 표현하면 아래와 같다.

 

💡주의사항

실무에서는 가급적 Getter는 열어두고, Setter는 꼭 필요한 경우에만 사용하는 것을 추천한다! 회원이 주문을 하기 때문에, 회원이 주문리스트를 가지는 것은 얼핏 보면 잘 설계한 것 같지만 그렇지 않다.

실무에서 회원이 주문을 참조하지 않고, 주문이 회원을 참조하는 것으로 충분하다.

 

Member.java

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id") // PK컬럼명 = 테이블명_id
    private Long id; // 엔티티의 식별자

    @NotEmpty
    private String name;

    @Embedded
    private Address address;

    @OneToMany(mappedBy = "member") // 1대다 양방향 매핑
    private List<Order> orders  = new ArrayList<>();
}

...
create table member (
   member_id bigint not null,
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    name varchar(255),
    primary key (member_id)
)

회원과 주문 : 일대다, 다대일 양방향 관계다. 따라서 연관관계의 주인을 정해야 하는데, 외래 키가 있는 주문을 연관관계의 주인으로 정하는 것이 좋다.  그러므로 Order.member 를 ORDERS.MEMBER_ID 외래 키와 매핑한다.

 

@OneToMany(mappedBy = "member")

: mappedBy 설정을 통해 연관관계의 주인임을 나타내준다.

 

@Embeddable = 이 클래스는 클래스에 포함될 수 있다.

@Embedded = 이클래스는 이제 클래스에 필드로 포함됩니다.

 

컬렉션은 필드에서 바로 초기화 하는 것이 안전하다! (null 문제로부터 안전)

 

Order.java

@Entity
@Table(name="orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {
    @Id @GeneratedValue
    @Column(name = "order_id") // 테이블 명의 아이디 값을 주로 선언
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY) // 다대일
    @JoinColumn(name = "member_id")
    private Member member; // FK (외래키), 연간관계의 주인, 주문 회원

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) // order를 저장하면 orderitem 도 저장되게끔!
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) @JoinColumn(name = "delivery_id")
    private Delivery delivery; //배송정보

    private LocalDateTime orderDate; //주문시간 @Enumerated(EnumType.STRING)
    private OrderStatus status; //주문상태 [ORDER, CANCEL]

    // 연관관계 편의 메서드, 양방향
    public void setMember(Member member) {
        this.member = member;
        member.getOrders().add(this);
    }
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }
    public void setDelivery(Delivery delivery) {
        this.delivery = delivery;
        delivery.setOrder(this);
    }

    // 생성 메서드
    public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems) {
        Order order = new Order();
        order.setMember(member);
        order.setDelivery(delivery);
        for(OrderItem orderItem : orderItems) {
            order.addOrderItem(orderItem);
        }
        order.setStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());
        return order;
    }

    public void cancel() {
        // 이미 배송된 상품에 대한 예외처리
        if(delivery.getStatus() == DeliveryStatus.COMP) {
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }

        // 주문 수량만큼 원복해 준환
        this.setStatus(OrderStatus.CANCEL);
        for(OrderItem orderItem : orderItems) {
            orderItem.cancel();
        }
    }

    public int getTotalPrice() {
        return orderItems.stream().mapToInt(OrderItem::getTotalPrice).sum();
    }
}

...
 create table orders (
   order_id bigint not null,
    order_date timestamp,
    status integer,
    delivery_id bigint,
    member_id bigint,
    primary key (order_id)
)

 

OrderItem.java

@Entity
@Getter @Setter
public class OrderItem {
    @Id @GeneratedValue
    @Column(name = "order_item_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;
    private int orderPrice; // 주문 가격
    private int count; // 주문 수량다

    // 생성 메서드
    public static OrderItem createOrderItem(Item item, int orderPrice, int count) {
        OrderItem orderItem = new OrderItem();
        orderItem.setItem(item);
        orderItem.setOrderPrice(orderPrice);
        orderItem.setCount(count); // 주문 수량만큼 count를 잡아주고

        item.removeStock(count); // 주문 수량만큼 재고를 빼줌
        return orderItem;
    }

    // 비즈니스 로직
    public void cancel() {
        getItem().addStock(count); // 주문 수량만큼 원복해준다!
    }

    // 조회 로직, 주문상품 전체 가격 조회
    public int getTotalPrice() {
        return getOrderPrice() * getCount();
    }
}

...
create table order_item (
   order_item_id bigint not null,
    count integer not null,
    order_price integer not null,
    item_id bigint,
    order_id bigint,
    primary key (order_item_id)
)

 

주문 상품과 주문 : 다대일 단방향 관계다. OrderItem.item 을 ORDER_ITEM.ITEM_ID 외래 키와 매핑한다.

 

Item.java

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 싱글 테이블 전략
@DiscriminatorColumn(name = "dtype") // 구분자
@Getter @Setter
public abstract class Item {
    @Id
    @GeneratedValue
    @Column(name = "item_id") // 테이블 명의 아이디 값을 주로 선언
    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<Category>();

    // 비즈니스 로직, 엔티티 안에 넣는게 좋음
    /*
    * stock 증가
     */
    public void addStock(int quantity) {
        this.stockQuantity += quantity;
    }

    public void removeStock(int quantity) {
        int restStock = this.stockQuantity - quantity;
        if(restStock < 0) {
            throw new NotEnoughStockException("need more stock");
        }

        this.stockQuantity = restStock;
    }
}

...
create table item (
   dtype varchar(31) not null,
    item_id bigint not null,
    name varchar(255),
    price integer not null,
    stock_quantity integer not null,
    artist varchar(255),
    etc varchar(255),
    author varchar(255),
    isbn varchar(255),
    actor varchar(255),
    director varchar(255),
    primary key (item_id)
)

 

 

Delivery.java

@Entity
@Getter @Setter
public class Delivery {
    @Id @GeneratedValue
    @Column(name = "delivery_id")
    private Long id;

    @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
    private Order order;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]
}

...
create table delivery (
   delivery_id bigint not null,
    city varchar(255),
    street varchar(255),
    zipcode varchar(255),
    status varchar(255),
    primary key (delivery_id)
)

 

주문과 배송 : 일대일 양방향 관계다. Order.delivery 를 ORDERS.DELIVERY_ID 외래 키와 매핑한다.

 

Category.java

@Entity
@Getter @Setter
public class Category {
    @Id @GeneratedValue
    @Column(name = "category_id")
    private Long id;

    private String name;

    @ManyToMany
    @JoinTable(name = "category_item", // 중간 테이블에 있는
            joinColumns = @JoinColumn(name = "category_id"), // 카테고리 아이디
            inverseJoinColumns = @JoinColumn(name = "item_id"))
    private List<Item> items = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent; // 부모

    @OneToMany(mappedBy = "parent") // 셀프로 양방향 관계를 가짐
    private List<Category> child = new ArrayList<>(); // 자식, 여러개를 가질 수 있음
    //==연관관계 메서드==//
    public void addChildCategory(Category child) {
        this.child.add(child);
        child.setParent(this);
    }
}

...
create table category (
   category_id bigint not null,
    name varchar(255),
    parent_id bigint,
    primary key (category_id)
)

 

카테고리와 상품 : @ManyToMany 를 사용해서 매핑한다. 실무에서 @ManyToMany는 사용하지말자

 

Address.java

@Embeddable
@Getter
public class Address {
    private String city;
    private String street;
    private String zipcode;

    protected Address() {
    }
    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}
외래 키가 있는 곳을 연관관계의 주인으로 지정

연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제이지 비즈니스상 우위에 있다고 주인으로 정하면 안된다!

 

💡 레포지토리 & 서비스 

기술 설명

@Repository

: 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환

 

@PersistenceContext

: 엔티티 매니저 주입

 

@PersistenceUnit

: 엔티티 매니저 팩토리 주입

 

@Transactional

: 트랜잭션, 영속성 컨텍스트

- readOnly=true : 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상(읽기 전용에는 다 적용)

 

MemberRepository

@Repository
@RequiredArgsConstructor
public class MemberRepository {
    private final EntityManager em;

    public void save(Member member) {
        em.persist(member);
    }

    public Member findOne(Long id) {
        return em.find(Member.class, id); // type, PK
    }

    public List<Member> findAll() {
        // JPQL, 엔티티 객체를 대상으로 쿼리
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    public List<Member> findByName(String name) {
        return em.createQuery("select m from Member m where m.name = :name", Member.class)
                .setParameter("name", name)
                .getResultList();
    }
}

 

- 생성자가 하나면 @AutoWired를 생략할 수 있다.

- @RequiredArgsConstructor : final 혹은 @NotNull 이 붙은 필드의 생성자를 자동으로 만들어 준다.

(스프링 데이터 JPA를 사용하면 EntityManager도 주입 가능!)

 

MemberService

@Service
@Transactional(readOnly = true) // 읽기 전용인 경우, 최적화
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;

    // 회원 가입
    @Transactional // 기본 옵션은 readOnly = false, 쓰기에 주로 지정
    public Long join(Member member) {
        // 중복 회원 검증
        validateDuplicateMember(member);

        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        // Exception
        List<Member> findMembers = memberRepository.findByName(member.getName());
        if(!findMembers.isEmpty()) {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    // 회원 전체 조회
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }

    @Transactional
    public void update(Long id, String name) {
        Member member = memberRepository.findOne(id);
        member.setName(name);
    }
}

 

ItemRepository

@Repository
@RequiredArgsConstructor
public class ItemRepository {
    private final EntityManager em;

    public void save(Item item) {
        if(item.getId() == null) {
            em.persist(item);
        } else {
            // 이미 DB 에 등록된 것을 가져온 경우, update
            em.merge(item);
        }
    }

    public Item findOne(Long id) {
        return em.find(Item.class, id);
    }

    public List<Item> findAll() {
        return em.createQuery("select  i from Item i", Item.class)
                .getResultList();
    }
}

- id 가 없으면 신규로 보고 persist() 를 실행, id 가 있으면 이미 데이터베이스에 저장된 엔티티를 수정한다고 보고 merge를 실행!

 

ItemService

// itemRepository를 단순히 위임!
@Service
@Transactional(readOnly = true) // 읽기 전용인 경우, 최적화
@RequiredArgsConstructor
public class ItemService {
    private final ItemRepository itemRepository;

    @Transactional
    public void saveItem(Item item) {
        itemRepository.save(item);
    }

    /**
     * 영속성 컨텍스트가 자동 변경
     */
    @Transactional
    public void updateItem(Long id, String name, int price, int stockQuantity) {
        Item item = itemRepository.findOne(id);
        item.setName(name);
        item.setPrice(price);
        item.setStockQuantity(stockQuantity);
    }
    public List<Item> findItems() {
        return itemRepository.findAll();
    }

    public Item findOne(Long itemId) {
        return itemRepository.findOne(itemId);
    }
}

 

OrderRepository

: findAll 메서드는 검색 조건에 동적으로 쿼리를 생성해서 주문 엔티티를 조회한다.

@Repository
@RequiredArgsConstructor
public class OrderRepository {
    private final EntityManager em;

    public void save(Order order) {
        em.persist(order);
    }

    public Order findOne(Long id) {
        return em.find(Order.class, id);
    }

    public List<Order> findAllByString(OrderSearch orderSearch) {
        // language=JPAQL
        String jpql = "select o From Order o join o.member m";
        boolean isFirstCondition = true;

        //주문 상태 검색
        if (orderSearch.getOrderStatus() != null) {
            if (isFirstCondition) {
                jpql += " where";
                isFirstCondition = false;
            } else {
                jpql += " and";
            }
            jpql += " o.status = :status";
        }

        //회원 이름 검색
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            if (isFirstCondition) {
                jpql += " where";
                isFirstCondition = false;
            } else {
                jpql += " and";
            }
            jpql += " m.name like :name";
        }

        TypedQuery<Order> query = em.createQuery(jpql, Order.class) .setMaxResults(1000); //최대 1000건
        if (orderSearch.getOrderStatus() != null) {
            query = query.setParameter("status", orderSearch.getOrderStatus());
        }
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            query = query.setParameter("name", orderSearch.getMemberName());
        }

        return query.getResultList();
    }

    public List<Order> findAllByCriteria(OrderSearch orderSearch) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Order> cq = cb.createQuery(Order.class);
        Root<Order> o = cq.from(Order.class);
        Join<Order, Member> m = o.join("member", JoinType.INNER); //회원과 조인
        List<Predicate> criteria = new ArrayList<>();

        //주문 상태 검색
        if (orderSearch.getOrderStatus() != null) {
            Predicate status = cb.equal(o.get("status"),
                    orderSearch.getOrderStatus());
            criteria.add(status);
        }

        //회원 이름 검색
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            Predicate name =
                    cb.like(m.<String>get("name"), "%" +
                            orderSearch.getMemberName() + "%");
            criteria.add(name);
        }

        cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
        TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000); //최대 1000건
        return query.getResultList();
    }

    public List<Order> findAllWithMemberDelivery() {
        return em.createQuery(
                "select o from Order o" +
                        " join fetch o.member m" +
                        " join fetch o.delivery d", Order.class
        ).getResultList();
    }
}

 

OrderService

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;
    // 주문
    @Transactional
    public Long order(Long memberId, Long itemId, int count) {
        // 엔티티 조회
        Member member = memberRepository.findOne(memberId);
        Item item = itemRepository.findOne(itemId);

        // 배송정보 생성
        Delivery delivery = new Delivery();
        delivery.setAddress(member.getAddress());

        // 주문상품 생성
        OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

        // 주문생성
        Order order = Order.createOrder(member, delivery, orderItem);

        //주문 저장
        orderRepository.save(order);

        // 식별자값을 반환
        return order.getId();
    }

    // 취소
    @Transactional
    public void cancelOrder(Long orderId) {
        // 주문 엔티티 조회
        Order order = orderRepository.findOne(orderId);

        // 주문 취소
        order.cancel();
    }

    // 검색
     public List<Order> findOrders(OrderSearch orderSearch) {
        return orderRepository.findAllByString(orderSearch);
     }
}

 

  💡 웹 계층 개발, 컨트롤러

: 간단한 타임리프를 사용한 html 파싱은 넘어가고 로직파트에 집중해서 정리하겠다.

 

회원 등록 폼 객체

: @NotEmpty 를 사용하여 필수 값을 나타낸다.(bundle.gradle에 아래 코드 추가)

implementation 'org.springframework.boot:spring-boot-starter-validation'
@Getter @Setter
public class MemberForm {
    @NotEmpty(message = "회원 이름은 필수 입니다.")
    private String name;

    private String city;
    private String street;
    private String zipcode;
}

 

회원 등록 컨트롤러

: 조회한 상품을 뷰에 전달하기 위해 스프링 MVC가 제공하는 모델(Model) 객체에 보관

: 요구사항이 정말 단순할 때는 폼 객체(MemberForm) 없이 엔티티(Member) 를 직접 등록과 수정화면에서 사용해도 된다.

하지만 화면 요구사항이 복잡해지기 시작하면, 엔티티 화면을 처리하기 위한 기능이 점점 증가하고 유지보수가 어려워진다.

실무에서 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 한다.

@Controller
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

	// 회원가입 폼 뷰
    @GetMapping("/members/new")
    public String createForm(Model model) {
        model.addAttribute("memberForm", new MemberForm());
        return "members/createMemberForm";
    }

	// 회원가입 로직
    @PostMapping("/members/new")
    public String create(@Valid MemberForm form,  BindingResult result) {
        if(result.hasErrors()) {
            return "members/createMemberForm";
        }

        Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());

        Member member = new Member();
        member.setName(form.getName());
        member.setAddress(address);

        memberService.join(member);
        return "redirect:/";
    }

	// 회원 리스트 조회 뷰
    @GetMapping("/members")
    public String list(Model model) {
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
}

 

상품 등록 컨트롤러

@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemService itemService;

	// 상품 등록 뷰
    @GetMapping("/items/new")
    public String createForm(Model model) {
        model.addAttribute("form", new BookForm());
        return "items/createItemForm";
    }

	// 상품 등록 로직
    @PostMapping("/items/new")
    public String create(BookForm form) {
        Book book = new Book();
        book.setName(form.getName());
        book.setPrice(form.getPrice());
        book.setStockQuantity(form.getStockQuantity());
        book.setAuthor(form.getAuthor());
        book.setIsbn(form.getIsbn());

        itemService.saveItem(book);

        return "redirect:/items";
    }

	// 상품 리스트 조회
    @GetMapping("/items")
    public String list(Model model) {
        List<Item> items = itemService.findItems();
        model.addAttribute("items", items);
        return "items/itemList";
    }

	// (단일) 상품 수정
    @GetMapping("/items/{itemId}/edit")
    public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) {
        Book item = (Book) itemService.findOne(itemId);

        BookForm form = new BookForm();
        form.setId(item.getId());
        form.setName(item.getName());
        form.setPrice(item.getPrice());
        form.setStockQuantity(item.getStockQuantity());
        form.setAuthor(item.getAuthor());
        form.setIsbn(item.getIsbn());

        model.addAttribute("form", form);
        return "items/updateItemForm";
    }

    /**
     *상품 수정,권장 코드
     */
    @PostMapping(value = "/items/{itemId}/edit")
    public String updateItem(@PathVariable Long itemId, @ModelAttribute("form")
    BookForm form) {
        itemService.updateItem(
                itemId,
                form.getName(),
                form.getPrice(),
                form.getStockQuantity()
        );
        return "redirect:/items";
    }
}

여기서 주의할 사항은 컨트롤러에 파라미터로 넘어온 item 엔티티 인스턴스는 현재 준영속 상태이다. 다라서 영속성 컨텍스트의 지원을 받을 수 없고 데이터를 수정해도 변경 감지 기능이 동작하지 않는다.

 

💡 변경 감지와 병합

준영속 엔티티란 ?

: 영속성 컨텍스트는 더는 관리하지 않는 엔티티를 의미한다. 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.

 

준영속 엔티티를 수정하는 2가지 방버에는 변경감지 기능과 병합이 있다.

 

변경 감지 기능 사용

@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
	//같은 엔티티를 조회한다.
	Item findItem = em.find(Item.class, itemParam.getId()); 
	findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다. 
}

: 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법

즉.  트랜잭션 안에서 엔티티를 다시 조회 & 변경할 값 선택 -> 트랜잭션 커밋 시점에 변경 감지가 동작해서 데이터베이스에 UPDATE SQL 실행

 

병합 사용

병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기술이다.

@Transactional
	void update(Item itemParam) { 
    //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티 
    Item mergeItem = em.merge(itemParam);
}

 

변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합 시 값이 없으면 null  로업데이트 될 위험도 있으므로 가능하면 변경 감지를 사용하자!

728x90

'Java' 카테고리의 다른 글

자바 ORM 표준 JPA 기본편 정리 [2]  (2) 2023.12.04
자바 ORM 표준 JPA 기본편 정리 [1]  (0) 2023.11.23
싱글톤 패턴이란  (0) 2023.10.25
[JAVA] 스프링과 객체지향  (0) 2023.10.15
커넥션 풀(Connection Pool)이란  (0) 2023.07.31
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.