개발/java

mybatis로 jpa 흉내내기

캐리캐리 2024. 9. 3. 13:18

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

    }
}

 

@Autowired 개인 OrderInfoMapper orderInfoMapper; 공개 목록<OrderInfo> getOrderInfoList(String odNo){ RequestModel 빌드 = OrderRequestModel.builder() .odNo(Condition.<String>builder().value(odNo).condition("=").build()) .build( ); List<OrderInfo> byId = orderInfoMapper.findById(build); } @Dao 공용 인터페이스 OrderInfoMapper는 MybatisMapper<OrderInfo, RequestModel>을 확장합니다. { } 공용 인터페이스 MybatisMapper<T, ID는 직렬화 가능>을 확장합니다. { @SelectProvider(type = MyBatisSqlProvider.class) List<T> findById(ID id); } 공용 클래스 MyBatisSqlProvider<T,ID 확장 직렬화 가능> 구현 ProviderMethodResolver { 공용 문자열 findById(ProviderContext 컨텍스트,ID id) { 반환 새 SQL().SELECT(this.column(context)) .FROM(this.table(context)) .WHERE(this.where(id)) .toString(); } }