mybatis로 jpa 흉내내기
mybatis의 경우 jdbc를 통해 코드와 db를 혼합하여 작성하는 방식에서 둘을 분리하게 구현함으로써 유지 보수 및 개발을 함에 있어 간편하고 쉽게 만들었다
그 이후 JPA가 도입됨에 따라 DB table에 따라 구현하는 방식이 서비스를 구현하는 것이 아닌 실 서비스를 세분화하여 ORM에 좀 더 다가갈 수 있게 되었다
그러나 특정 이유로 JPA를 사용하지 못하는 경우 mybatis에서 제공하는 new SQL() 과 ProviderMethodResolver를 통해서 별도 쿼리 작성 없이 동적으로 만들 수 있는 방법이 있다.
import org.apache.ibatis.builder.annotation.ProviderMethodResolver;
//mybatis 인터페이스 ProviderMethodResolver를 implements할 경우 new SQL을 통해 동적으로 쿼리를 만들 수 있다.
//아래를 풀이하면 select * from table id ='id' order by id;가 될 것이다
class MyBatisProvider implements ProviderMethodResolver {
public static String findById(String id) {
return new SQL(){{
SELECT("*");
FROM("table");
WHERE("id = #{id}");
ORDER_BY("id");
}}.toString();
}
}
쿼리는 항상 앨리어스를 통해 전체를 출력하는 것도 아니고 조건도 id가 아닌 주문번호 , 회원번호 , 주문접수일 등으로 조회를 하는게 일반적일 것이다.
mybatis에서 제공하는 ProviderContext의 경우 조회를 하는 대상 클래스의 정보를 추론 할 수 있다
아래와 같이 orderInfoRdbDao를 Mybatis를 통해 조회하게 되면 ProviderContext를 통해 OrderInfo class의 정보가 무엇인지 알 수 있다. 예를 들어 OrderInfo에는 3개의 컬럼이 있고 그 컬럼에는 orderNumber , orderSequence , orderName이 있다와 같은 정보를 알 수 있다.
OrderInfo orderInfo = orderInfoRdbDao.getOrderInfoInfo(orderNumber);
@RdbTable("orderDetail")
public class OrderInfo{
@columnName("odNo")
private String orderNumber;
@columnName("odSeq")
private String orderSequence;
@columnName("odNm")
private String orderName;
}
좀 더 OrderInfo를 살펴보자
@RdbTable을 통해 table 명은 orderDetail 이고 @columnName을 통해 각각을 컬럼이 RDS에는 어떤 컬럼명으로 되는지 또한 알 수 있다. 이제 이를 통해서 동적으로 테이블과 컬럼을 뽑는 예제를 살펴보자
public static String findById(ProviderContext context,String id) {
return new SQL(){{
SELECT(this.column(context))
.FROM(this.table(context))
.WHERE("id=#{id}")
.ORDER_BY("id");
}}.toString();
}
//Class에 있는 @ColumnName name 정보를 반환한다 , 반환 시 각 컬럼을 Array로 반환해야한다
private String[] column(ProviderContext context){
return Arrays.stream(domainType(context).getDeclaredFields())
.filter(t -> t.isAnnotationPresent(ColumnName.class))
.collect(Collectors.collectingAndThen(Collectors.toList(), Optional::of))
.toArray(String[]::new);
}
//Class에 있는 @RdbTable을 name 정보를 반환한다
private String table(ProviderContext context){
return classInfo(context).getAnnotation(RdbTable.class).map(RdbTable::name);
}
//Context를 통해 해당 Class 정보를 가져온다
private Class<?> classInfo(ProviderContext context){
return Arrays.stream(Arrays.stream(context.getMapperType().getGenericInterfaces())
.filter(ParameterizedType.class::isInstance)
.map(ParameterizedType.class::cast)
.findFirst().map(ParameterizedType::getActualTypeArguments)
.get()).findFirst().filter(Class.class::isInstance)
.map(Class.class::cast)
.orElseThrow(NoSuchElementException::new)
}
남은건 이제 where 조건이다
기본적으로는 id라는 Primary Key로 조회하는게 일반적이겠지만 여러가지 index가 있을 수 있다는것을 고려하여 조건에는 한도가 없다
그리고 조건은 같다 , 크다 , 작다와 같이 여러 조건이 될 수 있다, 또한 크다 , 작다의 경우 쌍으로 될 수도 있고 개별로만 쓰일 수도 있다
예를 들어 주문접수일이 2024년 9월 1일 부터 2024년 9월 3일까지의 정보를 조회할 수도 있고 주문금액 100원 이상을 구매한 고객의 정보를 추출 할 수도 있다
public class Condition<T> {
private String condition; //등호 조건 , = , > , <
private T value; // 조건값
private String columnName; //컬럼명
private Condition(Builder<T> builder) {
if(builder.condition == null) builder.condition = "=";
this.condition = builder.condition;
this.value = builder.value;
this.columnName = builder.columnName;
}
public class RequestModel implements Serializable {
private Condition<String> odNo;
where 조건에 이를 넣어보자
//condition의 컬럼명(odNo) condition(=)의 상태 field의 name(odNo)
private String where(RequestModel id){
return Arrays.stream(id.getClass().getDeclaredFields())
.map(field -> {
Condition condition = field.get(id);
return condition.getColumnName()
.concat(" ")
.concat(condition.getCondition())
.concat("#{")
.concat(field.getName())
.concat(".value}");
})
.filter(StringUtils::isNotEmpty).toArray(String[]::new);
}
이제 해당 Mybatis를 통해 RDB를 구축할때는 annotation기반으로 구축이 가능하며 Condition을 통해 where 조건을 추론할 수 있다.
@Autowired
private OrderInfoMapper orderInfoMapper;
public List<OrderInfo> getOrderInfoList(String odNo){
RequestModel build = OrderRequestModel.builder()
.odNo(Condition.<String>builder().value(odNo).condition("=").build())
.build();
List<OrderInfo> byId = orderInfoMapper.findById(build);
}
@Dao
public interface OrderInfoMapper extends MybatisMapper<OrderInfo, RequestModel> {
}
public interface MybatisMapper<T, ID extends Serializable> {
@SelectProvider(type = MyBatisSqlProvider.class)
List<T> findById(ID id);
}
public class MyBatisSqlProvider<T,ID extends Serializable> implement ProviderMethodResolver {
public String findById(ProviderContext context,ID id) {
return new SQL().SELECT(this.column(context))
.FROM(this.table(context))
.WHERE(this.where(id))
.toString();
}
}