ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • mybatis로 jpa 흉내내기
    개발/java 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(); } }
     

    '개발 > java' 카테고리의 다른 글

    CompletableFuture  (6) 2024.09.05
    java stream Util 만들기  (2) 2024.09.04
    싱글턴 문제점과 해결책  (0) 2024.09.02
    api retry하기  (0) 2024.08.28
    Exception 분리  (0) 2024.08.19

    댓글

Designed by Tistory.