Spring Boot myBatis with JPA - 예제코드

2020. 8. 23. 22:50Spring Data/MyBatis With JPA

반응형

myBatis와 JPA 동시에 적용하기 위한 예제코드를 작성해 보자

개발순서는 아래와 같다.

  1. MySQL과 PostgreSQL에 테이블을 생성한다.
  2. Domain 작성
  3. Mapper 작성
  4. JPA Respository 작성
  5. Service 작성
  6. Controller 작성

개발 흐름도

1. 테이블생성

PostgreSQL 테이블 생성 스크립트

DROP TABLE public.orderm;

CREATE TABLE public.orderm (
  order_id        character varying(15) NOT NULL, 
  orderer_id      character varying(15) NOT NULL, 
  order_dtm       character varying(14) NOT NULL, 
  total_order_amt bigint                NOT NULL, 
  order_status    character varying(1)  NOT NULL DEFAULT 'T'::character varying
)
TABLESPACE pg_default;

ALTER TABLE public.orderm ADD
  CONSTRAINT orderm_pk PRIMARY KEY ( order_id );
DROP TABLE public.order_detail;

CREATE TABLE public.order_detail (
  order_id  character varying(15) NOT NULL, 
  order_seq smallint              NOT NULL, 
  prod_cd   character varying(10) NOT NULL, 
  qty       smallint              NOT NULL, 
  order_amt bigint                NOT NULL
)
TABLESPACE pg_default;

ALTER TABLE public.order_detail ADD
  CONSTRAINT order_detail_pk PRIMARY KEY ( order_id, order_seq );

MySQL 테이블 생성 스크립트

CREATE TABLE `orderm` (
  `order_id` varchar(15) NOT NULL,
  `orderer_id` varchar(15) NOT NULL,
  `order_dtm` varchar(14) NOT NULL,
  `total_order_amt` bigint(20) NOT NULL,
  `order_status` varchar(1) NOT NULL DEFAULT 'T',
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `order_detail` (
  `order_id` varchar(15) NOT NULL,
  `order_seq` smallint(5) NOT NULL,
  `prod_cd` varchar(10) NOT NULL,
  `qty` smallint(5) NOT NULL,
  `order_amt` bigint(20) NOT NULL,
  PRIMARY KEY (`order_id`,`order_seq`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2. Domain 작성

JPA 연동을 위한 Entity Bean 기준으로 작성한다. Entity Bean 으로 작성하더라도 MyBatis 연동시 DTO로 사용가능 하다.

Domain 클래스 작성시 Lombok 을 사용하여서 별도의 get/set 메소드는 생성하지 않는다.

package com.roopy.domain;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import org.apache.commons.lang3.builder.MultilineRecursiveToStringStyle;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity
@Data
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Orderm {

	@Id
	@Column(name = "order_id", nullable = false, length = 15)
	public String orderId;
	
	@Column(name = "orderer_id", nullable = false, length = 35)
	public String ordererId;
	
	@Column(name = "order_dtm", nullable = false, length = 14)
	public String orderDtm;
	
	@Column(name = "total_order_amt", nullable = false)
	public Integer totalOrderAmt;
	
	@Column(name = "order_status", nullable = false, length = 1)
	public String orderStatus;
	
	@OneToMany(mappedBy="orderm",cascade=CascadeType.ALL)
	public List<OrderDetail> orderDetails;
	
	@Override
	public String toString() {
		return new ReflectionToStringBuilder(this, new MultilineRecursiveToStringStyle()).toString();
	}

orderm 에 대응되는 Entity 클래스

 

package com.roopy.domain;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.JoinColumns;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import org.apache.commons.lang3.builder.MultilineRecursiveToStringStyle;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity
@Data
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "order_detail")
@IdClass(OrderDetailIdentity.class)
public class OrderDetail implements Serializable {

	private static final long serialVersionUID = -178105276989733750L;
	
	@Id
	@Column(name = "order_id", nullable = false, length = 15)
        public String orderId;

	@Id
	@Column(name = "order_seq", nullable = false)
	public int orderSeq;
	
	@Column(name = "prod_cd", nullable = false, length = 10)
	public String prodCd;
	
	@Column(name = "qty", nullable = false)
	public int qty;
	
	@Column(name = "order_amt", nullable = false)
	public int orderAmt;
	
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumns(value = {
		@JoinColumn(name = "order_id", referencedColumnName = "order_id", insertable = false, updatable = false)
	})
	public Orderm orderm;
	
	@Override
	public String toString() {
		return new ReflectionToStringBuilder(this, new MultilineRecursiveToStringStyle()).toString();
	}
}

order_detail 에 대응되는 Entity 클래스

 

package com.roopy.domain;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Embeddable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Data
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class OrderDetailIdentity implements Serializable {
	
	private static final long serialVersionUID = 2339510329475348248L;

	@Column(name = "order_id", nullable = false, length = 15)
        public String orderId;

	@Column(name = "order_seq", nullable = false)
	public int orderSeq;
	
    
	public OrderDetailIdentity() {
	}

	public OrderDetailIdentity(String orderId, int orderSeq) {
		super();
		this.orderId = orderId;
		this.orderSeq = orderSeq;
	}
	
}

order_detail 테이블은 복합키로 이루어 지므로 복합키 적용을 위한 별도의 Key 클래스 작성

 

JPA 개발시 가장 중요한 부분이 Domain 클래스 작성이다. 특히나 복합키 처리는 까다로운 문제일수도 있다.

이와 관련하여서 배달의민족 블로그에 잘정리된 글이 있어서 공유합니다.

https://woowabros.github.io/experience/2019/01/04/composit-key-jpa.html

 

Legacy DB의 JPA Entity Mapping (복합키 매핑 편) - 우아한형제들 기술 블로그

안녕하세요. 우아한형제들에서 배달의민족 서비스의 광고시스템을 개발하고 있습니다. 시스템을 점진적으로 Spring Boot / JPA 기반으로 이관하면서 경험했던 내용을 공유하고자 합니다.

woowabros.github.io

3. Mapper 작성

주문정보조회를 위한 myBatis 인터페이스 클래스 생성 및 Mapper 파일을 작성한다.

package com.roopy.persistence.pgsql.mapper;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import com.roopy.domain.Orderm;

@Repository
public interface OrderMapper {

	public Orderm findOrder(@Param("orderId") String orderId);
	
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
	
<mapper namespace="com.roopy.persistence.pgsql.mapper.OrderMapper">

	<!--
	=====================================================================
	주문정보조회결과 ResultMap
	=====================================================================
	-->
	<resultMap type="Orderm" id="orderResultMap">
		<id property="orderId" column="order_id" />
		<result property="orderer_id" column="ordererId" />
		<result property="order_dtm" column="orderDtm" />
		<result property="total_order_amt" column="totalOrderAmt" />
		<result property="order_status" column="orderStatus" />
				
		<collection property="orderDetails" 
			column="{orderId=order_id}" 
			javaType="java.util.ArrayList" 
			ofType="OrderDetail"
			select="findOrderDetailByOrderId"/>
	</resultMap>

	<!--
	=====================================================================
	주문정보조회
	=====================================================================
	-->
	<select id="findOrder" resultMap="orderResultMap">
		select order_id
		     , orderer_id
		     , order_dtm
		     , total_order_amt
		     , order_status
		  from orderm
		 where order_id = #{orderId}
	</select>

	<!--
	=====================================================================
	주문상세정보조회
	=====================================================================
	-->
	<select id="findOrderDetailByOrderId" resultType="OrderDetail">
		select order_id
		     , order_seq
		     , prod_cd
		     , qty
		     , order_amt
		  from order_detail
		 where order_id = #{orderId}
	</select>

</mapper>

하나의 쿼리로 Join 으로 만들 수 도 있지만, 개발 사이트가 큰경우 쿼리의 재사용성 및 여러가지 이유로 위와 같이 ResultMap을 사용하는 것이 좋다는 생각이 든다.

 

4. JPA Repository 작성

package com.roopy.persistence.mysql.repository;

import com.roopy.domain.Orderm;

public interface OrderRepositoryCustom {

	public Orderm saveOrder(Orderm order);
	
}
package com.roopy.persistence.mysql.repository;

import org.springframework.data.repository.Repository;

import com.roopy.domain.Orderm;

public interface OrderRepository extends Repository<Orderm, String>, OrderRepositoryCustom {

}
package com.roopy.persistence.mysql.repository;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import com.roopy.domain.Orderm;

public class OrderRepositoryImpl implements OrderRepositoryCustom {
	
	@PersistenceContext
	EntityManager em;

	@Override
	public Orderm saveOrder(Orderm order) {
		return em.merge(order);
	}

}

5. Service 작성

package com.roopy.service;

import com.roopy.domain.Orderm;

public interface OrderService {

	public Orderm saveOrder(String orderId) throws Exception;
	
}
package com.roopy.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.roopy.domain.Orderm;
import com.roopy.persistence.mysql.repository.OrderRepository;
import com.roopy.persistence.pgsql.mapper.OrderMapper;
import com.roopy.service.OrderService;

@Service
public class OrderServiceImpl implements OrderService {
	
	@Autowired
	private OrderMapper orderMapper;
	
	@Autowired
	private OrderRepository orderRepository;

	@Override
	@Transactional
	public Orderm saveOrder(String orderId) throws Exception {
		
		// 주문정보 조회
		Orderm order = orderMapper.findOrder(orderId);
		
		// 주문정보 저장
		if (null != order) {
			order = orderRepository.saveOrder(order);
		}

		return order;
	}

}

6. Controller 작성

package com.roopy.controller;

import java.util.HashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.roopy.domain.Orderm;
import com.roopy.service.OrderService;

@RestController
public class OrderController {
	
	@Autowired
	private OrderService orderService;

	@GetMapping(value = "/order/{orderId}")
	public Orderm saveOrder(HttpServletRequest request, @PathVariable String orderId, HttpServletResponse response,
			@RequestParam HashMap<String, String> param) throws Exception {
		
		Orderm order = orderService.saveOrder(orderId);;
		
		return order;
	}
	
}

7. 테스트

http://localhost:9091/order/{orderId}

테스트결과
orderm 저장결과
order_detail 저장결과

반응형