57.Query Service

Query 서비스는 쿼리문이나 객체의 입력만으로 DB 데이터 조작을 가능하게 하는 서비스이다. Query 서비스는 JDBC(Java Database Connectivity)를 이용한 데이터 액세스 수행 부분을 추상화함으로써 간편한 데이터 액세스 방법을 제공하고, JDBC 사용 시 발생할 수 있는 공통적인 에러를 줄여준다. Query 서비스는 내부적으로 DataSource 서비스를 이용하고 있으므로, DataSource 서비스와 같이 배포되어야 함에 유의하도록 한다.

Query Service 매뉴얼에서 제공하는 모든 테스트 코드는 HSQL DB를 기반으로 실행된다. (단, ※ CallableStatement, LOB의 경우는 Oracle 9i, 10g를 기반으로 함.)

57.1.Configuration

Query 서비스를 활용하기 위해서는 다음과 같은 속성들이 정의되어 있어야 한다. 다음에서 각 속성이 가지는 의미에 대해 알아보기로 하자.

PropertyDescriptionRequiredDefault Value
jdbcTemplate DataSource 서비스를 이용하여 해당하는 DB로부터 java.sql.Connection을 얻어 정의된 쿼리문을 실행시킬 수 있도록 한다. PagingJdbcTemplate의 Bean Id를 값으로 정의한다. YN/A
sqlRepository 테이블 매핑 정보 및 쿼리문을 정의한 매핑 XML 파일들을 처리하는 역할을 수행하는 SQLLoader의 Bean Id를 정의한다. YN/A
pagingSQLGenerator DB에 특화된 형태의 페이징 처리를 위한 SQL을 정의하지 않더라도, 해당 DB에 따라 페이징 처리를 위해 알맞은 SQL을 생성할 수 있도록 도와주는 PagingSQLGenerator의 Bean Id를 정의한다. NN/A
lobHandlerLOB 유형의 데이터를 다루어야 하는 경우 해당하는 DB에 적합한 LobHandler의 Bean Id를 정의한다.NN/A
velocityPropsFilename Dynamic SQL 문을 다루어야 하는 경우 Velocity에 의해 남겨지는 Log 파일의 경로를 변경하고자 할 때 정의한다. Velocity Log 파일의 경로는 다음과 같이 절대/상대 경로(file:...)나 클래스패스(classpath:...)를 이용하여 정의 가능하다.
  • file:./query/log/velocity.log

  • classpath:/anyframe/core/query/log/velocity.log

값을 정의하지 않았을 경우에는 Velocity Log는 남겨지지 않는다. Log를 남기고자 하는 경우에는 정의된 경로에 지정된 로그 파일이 생성되어 있어야 함에 유의하도록 한다.

NLog를 남기지 않음.

위에서 언급한 Query 서비스가 필요로 하는 설정 정보 중, jdbcTemplate, sqlRepository, pagingSQLGenerator, lobHandler에 대해 좀 더 자세히 짚어보기로 하자.

57.1.1.jdbcTemplate

Query 서비스에서는 DataSource 서비스를 이용하여 해당하는 DB에 접근하고, java.sql.Connection 객체를 얻어내어 정의된 쿼리문을 실행시키기 위해 anyframe.core.query.impl.jdbc 패키지 하위에 Spring의 JdbcTemplate을 확장한 PagingJdbcTemplate, PagingNamedParamJdbcTemplate, OraclePagingJdbcTemplate를 제공하고 있다.

  • PagingJdbcTemplate : 내부 ResultSetExtractor를 이용하여 조회 결과에 대한 매핑 처리 및 페이징 처리를 수행한다.

  • OraclePagingJdbcTemplate : DBMS가 Oracle일 경우, batch 처리에 의해 변경된 데이터의 전체 건수를 알아야 하는 경우에 사용할 수 있다. Oracle에서는 Insert, Update, Delete에 대한 batch 처리를 위해 2가지 방법을 제공한다. 그 중 하나가 JDBC 2.0 Spec.을 준수하여 처리하는 방법 이며, 다른 하나는 Oracle 특화된 batch 처리 방법 이다. Query 서비스는 이 중 첫번째 방법으로 batch 작업을 수행하는데, 정상 처리되었을 경우 수행 결과로 전달되는 int 배열 내에는 -2 값이 셋팅되어 있게 된다. JDBC 2.0 Spec.에 의하면 결과값 -2가 의미하는 바는 "정상 처리되었으나 변경된 데이터의 건수를 알 수 없음."이다. 따라서, batch 처리 후, 변경된 데이터의 전체 건수를 알기 위해서는 두번째 방법으로 batch 작업을 수행해야 하며 이를 위해서 제공된 구현체가 OraclePagingJdbcTemplate이다.

  • PagingNamedParamJdbcTemplate : 별도 설정없이 QueryServiceImpl 클래스에서 내부적으로 사용하는 클래스로 Named Parameter를 가진 Dynamic SQL 처리를 수행한다.

다음은 PagingJdbcTemplate을 위해 필요한 주요 설정 정보들이다. (이외 설정 정보에 대해서는 org.springframework.jdbc.core.JdbcTemplate의 설정 정보를 참고하도록 한다.)

PropertyDescriptionRequiredDefault Value
dataSource참조할 dataSource의 Bean Id를 정의한다.YN/A
exceptionTranslator ExceptionTranslator의 Bean Id를 정의한다. ExceptionTranslator는 DB 데이터 조작시 SQLException이 발생한 경우 별도 Exception 객체에 해당하는 SQL Error Code와 Error Message 정보를 셋팅하여 throw하도록 하는 역할을 수행하며, Query 서비스에서는 anyframe.core.query.impl.util.RawSQLExceptionTranslator를 제공하고 있다. 따라서, ExceptionTranslator를 별도 셋팅하면, 데이터 조작으로 인한 오류가 발생한 경우 Query 서비스를 통해 throw된 QueryServiceException으로부터 SQL Error Code와 Error Message 정보를 추출할 수 있게 된다. NN/A
maxFetchSize 다량의 데이터 전체 조회시 발생할 수 있는 성능 저하를 방지하기 위해, maxFetchSize를 활용할 수 있다. 조회된 결과 데이터의 건수가 정의된 maxFetchSize보다 큰 경우 Exception 처리된다. NN/A
nativeJdbcExtractor OraclePagingJdbcTemplate을 사용하는 경우에만 정의할 수 있다. 사용중인 Connection Pool에 맞게 Wrapping되어 있는 Connection 객체로부터 본래의 JDBC Connection 객체를 추출하는 역할을 수행하는 NativeJdbcExtractor의 Bean Id를 정의한다. NN/A

57.1.2.sqlRepository

Query 서비스에서는 테이블 매핑 정보 및 쿼리문을 정의한 매핑 XML 파일들을 처리하는 역할을 수행하기 위해 anyframe.core.query.impl.config.loader.SQLLoader를 제공하고 있다. 다음은 SQLLoader를 위해 필요한 주요 설정 정보들이다.

Tag NameAttrubute NameDescriptionRequiredDefault ValueChild Tag
config:configuration    filename
filename  테이블 매핑 정보와 사용할 쿼리문을 정의하고 있는 매핑 XML 파일명을 지정하는 요소로 복수 지정 가능하다. filename 요소에 대한 지정은 Spring Configuration 파일 경로 지정 방식과 동일하므로, 절대/상대적인 파일 경로 지정(file:...)과 클래스패스를 이용한 지정(classpath:...)이 가능하다. *를 활용한 Pattern Matching 역시 적용 가능하다. YN/A 
nullcheck  해당 DB Column의 값이 없어서 null value가 리턴되었을 때, 지정한 값으로 변환시켜준다. 현재, CHAR, VARCHAR, LONGVARCHAR 타입의 칼럼에 대해서만 지원된다. NN/A 
sqlloaddynamic매핑 XML 파일에 대한 동적 Reload 설정여부를 정의한다.NN 
 frequency Reload 주기를 세팅한다.(milliseconds 단위) 10미만 입력시 10으로 인식하며, 10이상 입력시 입력값으로 인식한다. N  
skiperror  매핑 XML 파일을 읽어들이면서, error가 발생한 경우 skip 여부를 셋팅한다. N  

57.1.3.pagingSQLGenerator

Query 서비스에서는 페이징 처리를 위해 DB에 특화된 형태의 SQL을 구성하지 않더라도, 해당 DB에 적합한 페이징 처리 SQL을 구성할 수 있도록 도와주는 역할을 수행하기 위해 다음과 같은 PagingSQLGenerator를 제공한다.

DB 종류PagingSQLGenerator Class
Oracleanyframe.core.query.impl.jdbc.generator.OraclePagingSQLGenerator
DB2anyframe.core.query.impl.jdbc.generator.DB2PagingSQLGenerator
HSQLDBanyframe.core.query.impl.jdbc.generator.HSQLPagingSQLGenerator
Altibaseanyframe.core.query.impl.jdbc.generator.AltibasePagingSQLGenerator

이 외, PagingSQLGenerator가 필요한 경우에는 anyframe.core.query.impl.jdbc.generator.AbstractPagingSQLGenerator를 확장하여 신규 PagingSQLGenerator를 생성하고, getPaginationSQL() 메소드를 구현해주면 된다. getPaginationSQL() 메소드에는 입력받은 SQL을 기반으로 페이징 처리를 위해 변경된 SQL을 전달하는 로직을 정의하면 된다. 다음은 OraclePagingSQLGenerator 클래스의 일부 내용이다.

public class OraclePagingSQLGenerator extends AbstractPagingSQLGenerator {
        public String getPaginationSQL(String originalSql, Object[] originalArgs,
            int[] originalArgTypes, int pageIndex, int pageSize) {
            // 정의된 기본 쿼리문을 ROWNUM을 이용한 형태로 변경하기 위해 앞,뒤로 문자열 추가  
            StringBuffer sql = new StringBuffer(
                " SELECT * FROM ( SELECT   INNER_TABLE.* , ROWNUM AS ROW_SEQ FROM ( \n");
                sql.append(originalSql);
                sql.append(" ) INNER_TABLE WHERE ROWNUM <= ? ) "   
                + " WHERE ROW_SEQ BETWEEN ? AND ?");

            // 쿼리문에 입력되어야 할 기본 입력 인자 외에 
            // 페이징 처리를 위한 pageIndex, pageSize 인자값 셋팅
            setQueryArgs(originalArgs, pageIndex, pageSize);
            // 쿼리문에 입력되어야 할 기본 입력 인자 외에 
            // 페이징 처리를 위한 pageIndex, pageSize 인자 타입 셋팅
            setQueryArgTypes(originalArgTypes);
            // 변경된 쿼리문 전달
            return sql.toString();
        }

        protected void setQueryArgs(Object[] originalArgs, int pageIndex,
            int pageSize) {
                Object[] args = new Object[originalArgs.length + 3];

                for (int i = 0; i < originalArgs.length; i++) {
                    args[i] = originalArgs[i];
                }

                args[originalArgs.length] = String.valueOf(new Long(pageIndex
                    * pageSize));
                args[originalArgs.length + 1] = String.valueOf(new Long((pageIndex - 1)
                    * pageSize + 1));
                args[originalArgs.length + 2] = String.valueOf(new Long(pageIndex
                    * pageSize));

                setArgs(args);
        }

        protected void setQueryArgTypes(int[] originalArgTypes) {
            int[] argTypes = new int[originalArgTypes.length + 3];

            for (int i = 0; i < originalArgTypes.length; i++) {
                argTypes[i] = originalArgTypes[i];
            }

            argTypes[originalArgTypes.length] = Types.VARCHAR;
            argTypes[originalArgTypes.length + 1] = Types.VARCHAR;
            argTypes[originalArgTypes.length + 2] = Types.VARCHAR;

            setArgTypes(argTypes);
        }
}      		

또한, 전체 조회 조건에 해당하는 데이터의 건수를 조회하기 위한 쿼리문은 상위 클래스인 AbstractPagingSQLGenerator 내에 정의되어 있으며 해당 DBMS가 count(*)을 지원하지 않는 경우에는 해당하는 PagingSQLGenerator에서 오버라이드해 주도록 한다.

PagingSQLGenerator가 정의되어 있지 않고 특정 페이지에 속한 목록을 조회할 경우 Query 서비스에서는 일단 해당되는 전체 목록을 모두 조회한다. 그리고 ScrollableResultSet의 Cursor를 이용하여 해당 페이지에 속한 데이터들을 추출하게 된다. 따라서, 해당 페이지에 속한 데이터만을 조회하는 PagingSQLGenerator에 비해 성능이 저하된다는 점에 유의하도록 한다.

57.1.4.lobHandler

Query 서비스에서는 Spring에서 제공하는 LobHandler를 사용하여 LOB 유형의 데이터를 다루도록 권장한다. 다음은 Spring에서 제공하는 LobHandler 목록이다.

  • Oracle(9i이상) : org.springframework.jdbc.support.lob.OracleLobHandler

  • the Others : org.springframework.jdbc.support.lob.DefaultLobHandler

단, Spring에서 제공하는 OracleLobHandler의 경우 Oracle 9i 이상에서만 사용 가능하므로 Oracle 8i 사용자를 위해 anyframe.core.query.impl.jdbc.lob.AnyframeOracle8iLobHandler를 추가로 제공하고 있다. OracleLobHandler나 AnyframeOracle8iLobHandler의 경우 다음과 같은 설정 정보가 필요하다.

PropertyDescriptionRequiredDefault Value
nativeJdbcExtractor 사용중인 Connection Pool에 맞게 Wrapping되어 있는 Connection 객체로부터 본래의 JDBC Connection 객체를 추출하는 역할을 수행하는 NativeJdbcExtractor의 Bean Id를 정의한다. (해당 lobHandler에서 nativeJdbcExtractor를 필요로 하는 경우에만 정의)

다음은 Spring에서 제공하는 주요 JdbcExtractor 클래스들이다.

  • Common DBCP : org.springframework.jdbc.support.nativejdbc. CommonsDbcpNativeJdbcExtractor

  • C3P0 : org.springframework.jdbc.support.nativejdbc. C3P0NativeJdbcExtractor

  • WebLogic : org.springframework.jdbc.support.nativejdbc. WebLogicNativeJdbcExtractor

  • WebSphere : org.springframework.jdbc.support.nativejdbc. WebSphereNativeJdbcExtractor

즉, 오픈소스 프로젝트인 Commons DBCP를 Connection Pool로 채택한 경우 CommonsDbcpNativeJdbcExtractor를 사용할 수 있다.

NN/A

57.1.5.Samples

다음은 위에서 언급한 Query 서비스 속성 정의를 포함하고 있는 context-query.xml의 일부이다.

<bean id="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
    <property name="sqlRepository" ref="sqlLoader"/>
    <property name="pagingSQLGenerator" ref="pagingSQLGenerator"/>
    <property name="lobHandler" ref="lobHandler"/>
    <!-- if you don't define velocityPropsFilename, 
        queryservice doesn't make a velocity log file. -->
    <property name="velocityPropsFilename" value="file:./testvelocity/velocity.log"/>
</bean>

<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>

<bean id="pagingSQLGenerator" 
    class="anyframe.core.query.impl.jdbc.generator.OraclePagingSQLGenerator"/>

<bean id="nativeJdbcExtractor" 
    class="org.springframework.jdbc.support.nativejdbc.CommonsDbcpNativeJdbcExtractor" 
    lazy-init="true"/>

<bean id="lobHandler" class="org.springframework.jdbc.support.lob.OracleLobHandler"
    lazy-init="true">
    <property name="nativeJdbcExtractor" ref="nativeJdbcExtractor"/>
</bean>     

또한, sqlLoader의 속성은 다음 context-query-sqlloader.xml 파일에서와 같이 정의할 수 있다.

<bean id="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
  <config:configuration>
    <!-- xml files in folder -->
    <!--filename>file:./testmappings/**/testcase-*.xml</filename-->
    <!-- xml files in classpath -->
    <filename>
      classpath:/query/mapping-query-general.xml
    </filename>
        중략...
    <nullcheck type="VARCHAR" default-value="" />
    <sqlload dynamic="true" frequency="5" />
    <skiperror>true</skiperror>
  </config:configuration>
</bean>     

57.1.6.TestCase

다음은 Query 서비스를 사용하여 해당하는 DB에 샘플 데이터를 INSERT, SELECT, UPDATE, DELETE하는 테스트 코드의 일부이다.

57.1.6.1.INSERT

다음은 INSERT 예제이다.

public void insertQuery() throws Exception{
    IQueryService queryService = (IQueryService) context.getBean("queryService");
    //create() : XML mapping파일에 정의되어 있는 SQL query를 이용하여 INSERT를 실행한다.
    int rs = queryService.create("create", new Object[] { "1234567890123", "AAAAA" , "seoul"});
    if ( rs == -1 ){
       throw new Exception("Insert query failed");
    }
}            	

57.1.6.2.SELECT

다음은 SELECT 예제이다.

public void selectQuery() throws Exception{
    IQueryService queryService = (IQueryService) context.getBean("queryService");
    //find() : XML mapping파일에 정의되어 있는 SQL query를 이용하여 SELECT를 실행한다.

    //일반적인 경우(table과 class를 mapping하지 않은 경우)
    ArrayList rsquery  = (ArrayList) queryService
        .find("selectGeneral", new Object[] { "%12345%" });
    Map hsRsquery = new HashMap();
    for( int i = 0 ; i < rsquery.size() ; i ++ ){
        hsRsquery = (Map) rsquery.get(i);
        String name = (String) hsRsquery.get("name");
    }
    
    /*매핑 XML에 해당 클래스와 매핑되는 테이블이 존재하지 않을 경우,
    * 쿼리 수행 결과에 대해 하나의 Row별로 칼럼명,
    * 해당값을 쌍으로 org.apache.commons.collections.map.ListOrderedMap에 put하고 
    * ListOrderedMap들을 ArrayList에 담은 형태로 결과값을 리턴하게 된다.
    */

    //Table - Class mapping을 사용한 경우
    Collection rsqueryNotUsingResultMapping = queryService
        .find("selectNotUsingResultMapping", new Object[] { "%12345%" });
    Iterator rsqueryItr = rsqueryNotUsingResultMapping.iterator();
    while (rsqueryItr.hasNext()) {
        Customer customer = (Customer) rsqueryItr.next();
        String name =  customer.getNm();
    }

    //result-mapping을 사용한 경우
    Collection rsqueryUsingResultMapping = queryService
        .find("selectUsingResultMapping", new Object[] { "%12345%" });
    Iterator rsqueryItr_01 = rsqueryUsingResultMapping.iterator();
    while (rsqueryItr_01.hasNext()) {
        CompositionCustomer compositionCustomer 
            = (CompositionCustomer) rsqueryItr_01.next();
        String name =  compositionCustomer.getCompositionName();
    }

	System.out.println("rsquery.size() : " 
	    + rsquery.size());
	System.out.println("rsqueryNotUsingResultMapping.size() : " 
	    + rsqueryNotUsingResultMapping.size());
	System.out.println("rsqueryUsingResultMapping.size( : " 
	    + rsqueryUsingResultMapping.size();
}      			

57.1.6.3.UPDATE

다음은 UPDATE 예제이다.

public void updateQuery() throws Exception {
    IQueryService queryService = (IQueryService) context
        .getBean("queryService");
    //update() : XML mapping파일에 정의되어 있는 SQL query를 이용하여 UPDATE를 실행한다.
    int rs = queryService.update("update"
        , new Object[] { "9999999999999", "AAAAA" , "busan" , "1234567890123"});
    if ( rs == -1 ){
        throw new Exception("Update query failed");
    }
}               

57.1.6.4.DELETE

다음은 DELETE 예제이다.

public void deleteQuery() throws Exception {
    IQueryService queryService = (IQueryService) context
        .getBean("queryService");
    //remove() : XML mapping파일에 정의되어 있는 SQL query를 이용하여 DELETE를  실행한다.
    int rs = queryService.remove("delete", new Object[] { "9999999999999" });
    if ( rs == -1 ){
        throw new Exception("Delete query failed");
    }
}          						

57.2.Mapping XML Files

Query 서비스 초기화 시, Query 서비스는 속성 정의 파일에 정의되어 있는 매핑 xml 파일들을 로드한다. 그리고 사용자 요청 시 매핑 정보를 기반으로 query id를 이용해 실행하고자 하는 쿼리문을 찾아 실행한다. 매핑 XML 파일은 <queryservice>와 </queryservice>내에 크게 <table-mapping>과 <queries>로 구성된다. <queries>는 필수 요소이므로 빠뜨리지 않도록 주의해야 한다.

57.2.1.table-mapping 정의 방법

<table-mapping> 내에 <table>을 이용하여, 테이블과 특정 클래스간의 매핑 정보들을 정의할 수 있다. <table-mapping> 내에는 여러 개의 <table>을 정의할 수 있다.

Tag NameDescriptionChild Tag
table 테이블과 클래스간의 매핑 정보를 정의한다.

* attribute 설명

name: 해당 테이블명

class : 매핑 클래스명

field-mapping(필수), primary-key(필수)
field-mapping테이블의 칼럼과 이에 매핑되는 클래스의 attribute를 정의한다.dbms-column(필수), class-attribute(필수)
primary-key해당 테이블의 Primary Key를 정의한다.dbms-column(필수)
dbms-column해당 테이블의 칼럼명을 정의한다. 
class-attributedbms-column에서 정의한 칼럼과 매핑되는 해당 클래스의 attribute명을 정의한다.  

다음은 위에서 나열한 설정 정보들을 이용한 <table-mapping>의 설정 예제로, 테이블 TBL_CUSTOMER와 클래스 Customer간의 매핑 정보를 담고 있다.

<queryservice>
    <table-mapping>
       <table name="TBL_CUSTOMER"
       class="anyframe.example.domain.Customer">
          <field-mapping>
             <dbms-column>ssno</dbms-column>
             <class-attribute>ssno</class-attribute>
          </field-mapping>
          <field-mapping>
             <dbms-column>name</dbms-column>
             <class-attribute>nm</class-attribute>
          </field-mapping>
          <field-mapping>
             <dbms-column>address</dbms-column>
             <class-attribute>addr</class-attribute>
          </field-mapping>
          <primary-key>
             <dbms-column>ssno</dbms-column>
          </primary-key>
       </table>
    </table-mapping>
</queryservice>

57.2.2.queries 정의 방법

<queries> 내에 <query>를 이용하여, Query 서비스들을 통해 실행할 쿼리문들을 정의할 수 있다. <queries> 내에는 여러 개의 <query>를 정의할 수 있다.

Tag NameDescriptionChild Tag
query 쿼리문을 정의 한다.

* attribute설명

id : 해당하는 쿼리문을 식별하기 위한 식별자

isDynamic : 동적 쿼리인지 아닌지 식별 (Default=true)

isCamelCase : 조회 결과 매핑시 조회 칼럼명에 대해 CamelCase 적용할 것인지 정의 (Default=true)

mappingStyle : 조회 결과 매핑시 조회 칼럼명에 대해 어떤 유형으로 변경할 것인지 정의 (camel,lower, upper, none 중 택일, Default=camel)

camel - 조회 칼럼명에 대해 CamelCase 적용

lower - 조회 칼럼명에 대해 LowerCase 적용

upper - 조회 칼럼명에 대해 UpperCase 적용

none - 조회 칼럼명을 DBMS가 전달한 그대로 적용

*isCamelCase로는 다양한 스타일의 칼럼 매핑 지원에 제한이 있어 신규 속성을 추가함. 따라서 isCamelCase는 Deprecated 예정임 (Anyframe 3.2.1 이후) mappingStyle이 적용된 경우 isCamelCase는 무시되며, 이전 버전과의 호환을 위해 isCamelCase가 true인 경우 mappingStyle은 'camel', false인 경우 'lower'로 처리됨.

statement(필수), lobStatement(선택), param(선택), result(선택)
statement 실행할 쿼리문을 정의한다. Joined 쿼리에서는 동일한 조회 칼럼명이 있을 경우, Alias를 부여하도록 한다.  
param Query 서비스는 해당 쿼리문을 미리 컴파일하여 PreparedStatement 형태로 저장하여 처리하고 있다. 따라서 입력값 셋팅을 위해 PreparedStatement에 setXXX를 수행하려면, 입력 Parameter에 해당하는 SQL Type을 java.sql.Types에 정의된 값을 참조하여 param tag의 attribute인 type의 값으로 정의한다. param tag는 입력 Parameter의 개수와 순서에 맞게 추가한다. 입력 Parameter의 데이터를 가져오기 위해서는 다음과 같은 기준에 따라 데이터 타입을 정의해야만 한다
Java TypeDBMS Type
StringCHAR
StringVARCHAR
StringLONGVARCHAR
java.math.BigDecimalNUMERIC
java.math.BigDecimalDECIMAL
booleanBIT
byteTINYINT
shortSMALLINT
intINTEGER
longBIGINT
floatREAL
doubleFLOAT
doubleDOUBLE
byte[]BINARY
byte[]VARBINARY
byte[]LONGVARBINARY
java.sql.DateDATE
java.sql.TimeTIME
java.sql.TimestampTIMESTAMP

* attribute 설정

type : parameter의 DBMS type

binding : CallableStatement경우 'IN','OUT','INOUT' 중 선택

name : CallableStatement경우 Stored Procedure 내에 정의된 변수 이름 정의

생략 시 VARCHAR로 인식됨
result 해당 쿼리가 조회를 위한 SELECT문일 경우에 사용할 수 있으며, 쿼리 수행 결과를 매핑할 클래스명을 정의한다. <result>가 지정되지 않았을 경우 쿼리 수행 결과의 각 Row에 대해 칼럼명, 칼럼값을 쌍으로 Map에 put하고 각 Row별 Map들을 ArrayList에 담은 형태로 결과값을 리턴하게 된다. isCamelCase, mappingStyle 속성값에 따라 Map에 저장되는 키값이 달라짐에 유의하도록 한다. (예를 들어, mappingStyle의 속성값이 'camel'이고 조회 칼럼명이 USER_NAME인 경우 Map의 키값은 userName이 된다.)

* attribute 설명

length : 한 페이지에 보여질 데이터의 건수

class : 수행 결과를 저장할 클래스명

result-mapping(선택)
result-mapping 해당 쿼리가 조회를 위한 SELECT문일 경우에 사용할 수 있으며, 수행 결과를 저장할 클래스와 매핑되는 테이블이 정의되지 않았을 경우 또는 수행 결과를 저장할 클래스의 속성명이 조회 칼럼명에 mappingStyle을 적용한 이름과 일치하지 않는 경우에 한해 해당하는 칼럼에 대해 매핑되는 해당 클래스의 attribute 명을 정의한다.

* attribute 설명

column : 조회된 칼럼명

attribute : 정의한 칼럼에 매핑되는 클래스의 attribute명

 

57.3.Usecases

이 페이지를 통하여 다양한 Query Service 사용 방법에 대해 소개하고자 한다. 상세한 내용을 알고자 한다면, 아래 나열된 각각의 Use Case를 참고하도록 한다.

57.3.1.Result Mapping

다음은 Query Service를 통해 목록 조회를 수행한 이후 조회 결과를 특정 객체에 매핑하기 위한 방법들이다.

57.3.1.1.조회 결과 매핑이 별도로 정의되어 있지 않은 경우

조회 결과 매핑을 위해 <table-mapping/>, <result-mapping/>을 별도로 정의하지 않은 경우, 쿼리문 수행 결과는 각 결과 Row 별로 Map에 담아 ArrayList 형태로 리턴된다. 조회 결과값을 추출하기 위해서는 각 Map으로부터 get("칼럼명")을 통해 해당 칼럼의 값을 얻어낼 수 있다.

<queryservice>
    <queries>
       <query id="selectGeneral" isDynamic="false">
          <statement>
             SELECT * FROM TBL_CUSTOMER WHERE SSNO like ?
          </statement>
          <param type="VARCHAR" />
       </query>
    </queries>
</queryservice>  

57.3.1.2.<result-mapping> 없이 <table-mapping>을 이용할 경우

특정 클래스와 단일 테이블 사이의 매핑 정보를 정의할 때 사용하며, 테이블과 특정 클래스 간의 매핑 정보를 정의해 두면 특정 조회문의 조회 결과를 매핑할 때 별도의 <result-mapping>없이 해당 클래스명만 <result>에 정의해 두면 되므로 XML 정의가 보다 간단해질 수 있다. 또한 <table-mapping>을 이용하면 별도 쿼리문 정의없이 객체만으로도 단건 데이터 생성/수정/삭제/조회가 가능해진다. <result class=""anyframe.example.domain.Customer"/>와 같이 table mapping시 정의한 클래스를 이용하면 쿼리문 수행 결과는 해당 클래스의 setter 호출을 통해 저장되고, getter를 호출함으로써 결과값을 얻을 수 있게 된다.

<queryservice>
    <table-mapping>
        <table name="TBL_CUSTOMER"
            class="anyframe.example.domain.Customer">
            <field-mapping>
                <dbms-column>ssno</dbms-column>
                <class-attribute>ssno</class-attribute>
            </field-mapping>
            <field-mapping>
                <dbms-column>name</dbms-column>
                <class-attribute>nm</class-attribute>
            </field-mapping>
            <field-mapping>
                <dbms-column>address</dbms-column>
                <class-attribute>addr</class-attribute>
            </field-mapping>
            <primary-key>
                <dbms-column>ssno</dbms-column>
            </primary-key>
        </table>
    </table-mapping>    
    <queries>
       <query id="select" isDynamic="false">
          <statement>
             SELECT * FROM TBL_CUSTOMER WHERE SSNO like ?
          </statement>
          <param type="VARCHAR" />
          <result class="anyframe.example.domain.Customer"/>
       </query>
    </queries>
</queryservice>	

57.3.1.3.<table-mapping>,<result-mapping>없이 <result>만을 이용할 경우

매핑 대상 클래스의 속성명이 조회 칼럼명과 동일하거나 CamelCase된 형태이어서, <table-mapping>나 <result-mapping>를 통해 별도 매핑을 수행하지 않아도 되는 경우에 사용할 수 있다. 즉, isCamelCase, mappingStyle 속성값에 따라 조회된 칼럼명과 매핑되는 클래스의 속성명을 찾는다. (예를 들어, mappingStyle의 속성값이 'camel'이고 조회 칼럼명이 USER_NAME인 경우 매핑되는 속성명은 userName이 된다.))

<queryservice>
    <queries>
        <query id="getUser" isDynamic="false">
            <statement>
                SELECT USER_ID, USER_NAME, PASSWORD, SSN, SL_YN, 
                    BIRTH_DAY, AGE, CELL_PHONE, ADDR, EMAIL, EMAIL_YN, 
                    IMAGE_FILE, REG_DATE
                FROM USERS
                WHERE USER_ID = ?
            </statement>
            <param type="VARCHAR" />
            <result class="anyframe.example.domain.User"/>
        </query>
    </queries>
</queryservice>

* Query 서비스는 내부적으로 쿼리문 수행으로 얻어진 조회 결과를 매핑할 때 다음과 같은 순서로 매핑 기준을 찾는다.

  1. 정의된 <result-mapping> 정보가 있으면 이를 기반으로 매핑

  2. 정의된 <result> 클래스에 대한 <table-mapping> 정보가 있으면 이를 기반으로 매핑

  3. 정의된 <result> 클래스에 대한 정보가 있으면 이를 기반으로 매핑

  4. HashMap에 mappingStyle, isCamelCase 속성값을 기반으로 매핑

57.3.1.4.<result-mapping>을 이용할 경우

<result-mapping>은 <table-mapping>에 정의되지 않은 클래스이면서, 조회된 칼럼명과 매핑 클래스의 속성명이 연관성이 없어 별도 매핑이 필요한 경우 사용한다. <result-mapping>에 해당 column과 attribute를 일대일로 매핑한다. <result>에 정의된 클래스의 setter 호출을 통해 저장되고, getter 호출을 통해 결과값을 얻을 수 있다.

<queryservice>
    <queries>
        <query id="selectUsingResultMapping" isDynamic="false">
            <statement>SELECT * FROM TBL_CUSTOMER WHERE SSNO like ?</statement>
            <param type="VARCHAR"/>
            <result class="anyframe.example.domain.CompositionCustomer">
                <result-mapping column="NAME" attribute="nm"/>
                <result-mapping column="ADDRESS" attribute="addr"/>
            </result>
        </query>
    </queries>
</queryservice>	

Result Mapping시 유의사항

  • SELECT 문을 통해 조회된 칼럼 중 <result-mapping>을 통해 매핑하지 않은 경우에는 기본적으로 mappingStyle을 이용하여, 매핑 처리를 수행한다. 만약 정의된 Result 클래스 내에 mappingStyle에 맞는 속성명이 존재하지 않으면 매핑은 수행되지 않을 것이다.

  • Result 클래스가 내부에 다른 User Defined Object를 포함하고 있는 Composite Object인 경우에는 column/attribute 사이의 매핑 정보를 정의할 때, {} 내에 ,를 구분자로 하여 정의할 수 있다. 단, attribute 정의시 반드시 내부 Object의 변수명을 그대로 사용해야 한다.

    <query id="findWithCompositeResultMapping">
        <statement>
            select grp.GROUP_ID, grp.GROUP_NAME, cd.CODE_ID, cd.CODE_NAME, cd.CODE_DESC, cd.CODE_USE_YN
            from TB_CODE_GROUP grp, TB_CODE cd
            where grp.GROUP_ID = cd.GROUP_ID and cd.GROUP_ID = :groupId 
        </statement>
        <result class="anyframe.core.query.vo.LocalResultMappingVO">
            <result-mapping column="{ GROUP_ID, GROUP_NAME }" attribute="{ group.groupId, group.codeNm }" />
            <result-mapping attribute="codeID" column="CODE_ID" />
            <result-mapping attribute="codeDescription" column="CODE_DESC" />
        </result>
    </query>

    위에서 정의한 쿼리 정의에 의해 'findWithCompositeResultMapping'의 수행 결과는 LocalResultMappingVO 객체에 담겨져 전달될 것이다. 이 때, GROUP_ID, GROUP_NAME 칼럼값은 LocalResultMappingVO 내의 group이라는 이름의 변수에 해당하는 객체의 groupId, codeNm 에 각각 매핑된다. 따라서 전달된 LocalResultMappingVO로부터 GROUP_ID, GROUP_NAME 값을 추출하기 위해서는 LocalResultMappingVO.getGroup().getGroupId(), LocalResultMappingVO.getGroup().getCodeNm()을 호출하면 된다.

    다음은 CodeVO라는 객체를 포함하고 있는 LocalResultMappingVO라는 객체의 일부이다.

    public class LocalResultMappingVO implements Serializable {
        private String groupID;
        private String groupName;
        private String codeID;
        private String codeName;
        private String codeDescription;
        private String codeUseYn;
        private CodeVO group;
        
        // getter, setter...
    }

57.3.1.5.테스트 코드 Sample

다음은 앞서 제시한 매핑 XML 파일에 정의된 쿼리문을 실행하는 테스트 코드의 일부이다.

/**
 * Query 서비스를 통해 DB에 입력된 데이터를 조회하는 테스트 코드
 */
public void selectCustomer() throws Exception{
	IQueryService queryService = (IQueryService) context.getBean("queryService");

	//find() : 매핑 XML 파일에 정의되어 있는 query id를 이용하여 SELECT를 실행한다.

	//별도 Result Mapping을 정의하지 않은 경우
	ArrayList rsquery  = (ArrayList) queryService
	                                  .find("selectGeneral", new Object[] { "%12345%" });
	Map hsRsquery = new HashMap();
	for( int i = 0 ; i < rsquery.size() ; i ++ ){
		hsRsquery = (Map) rsquery.get(i);
		String name = (String) hsRsquery.get("name");
	}
	
	// table-mapping을 정의한 경우
	Collection rsqueryUsingTableMapping = queryService.find(
			"selectUsingTableMapping", new Object[] { "%12345%" });
	Iterator rsqueryItr = rsqueryUsingTableMapping.iterator();
	while (rsqueryItr.hasNext()) {
		Customer customer = (Customer) rsqueryItr.next();
		String name = customer.getNm();
	}

	// result-mapping을 정의한 경우
	Collection rsqueryUsingResultMapping = queryService.find("selectUsingResultMapping"
	    , new Object[] { "%12345%" });
	Iterator rsqueryItr_01 = rsqueryUsingResultMapping.iterator();
	while (rsqueryItr_01.hasNext()) {
		CompositionCustomer compositionCustomer 
		= (CompositionCustomer) rsqueryItr_01.next();
		String name =  compositionCustomer.getCompositionName();
	}
	
	// result class만 정의한 경우
	Collection rsqueryUsingOnlyResultClass = queryService.find(
			"selectUsingOnlyResultClass", new Object[] { "%12345%" });
	Iterator rsqueryItr_02 = rsqueryUsingOnlyResultClass.iterator();
	while (rsqueryItr_02.hasNext()) {
		CamelCasedCustomer camelCasedCustomer
		(CamelCasedCustomer) rsqueryItr_02.next();
		String name = camelCasedCustomer.getName();
	}	
}					

57.3.2.Embedded SQL

다음은 Embedded SQL을 사용하는 경우로, 매핑 XML 파일에 별도로 쿼리문을 정의해 두지 않고도 특정 쿼리문을 소스 코드 내에 직접 입력하여 실행할 수 있다.

57.3.2.1.속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml 파일의 일부이다.

<bean id="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
	<property name="jdbcTemplate" ref="jdbcTemplate"/>
	<property name="sqlRepository" ref="sqlLoader"/>
	중략...
</bean>
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean> 			

57.3.2.2.테스트 코드 Sample

다음은 매핑 XML 파일에 별도 쿼리문을 정의하지 않고 쿼리문을 직접 입력하여 실행하는 테스트 코드의 일부이다. 다음에서 볼 수 있듯이 Query 서비스는 매핑 XML 파일없이도 INSERT, SELECT, UPDATE, DELETE를 실행할 수 있도록 지원한다.

/**
 * 테스트 코드를 샐행하기 위한 data table 생성
 */
public void insertCustomer() throws Exception{
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	//createBySQL() : 매핑 XML 파일에 쿼리문이 정의되어 있지 않을 때 INSERT를 실행한다.
	int result = queryService.createBySQL(
			"insert into TBL_CUSTOMER values (?, ?, ?)",
			new String[] {"VARCHAR", "VARCHAR", "VARCHAR" },
			new Object[] {"1234567890123", "GilDongHong", "Seoul" });
	if ( result == -1 ){
		throw new Exception("Insert Query failed");
	}
}

/**
 * Query 서비스를 통해 DB에 입력된 데이터를 조회하는 테스트 코드
 */	
public void selectCustomer() throws Exception{
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	//findBySQL() :  매핑 XML 파일에 쿼리문이 정의되어 있지 않을 때 SELECT를 실행한다.
	Collection result = queryService.findBySQL(
			"select NAME, ADDRESS from TBL_CUSTOMER where SSNO like ?",
			new String[] { "VARCHAR" }, 
			new Object[] { "%4567890123" });
	Iterator resultItr = result.iterator();
	while( resultItr.hasNext() ){
		Map resultMap = (Map) resultItr.next();
		resultMap.get("name");
	}
	
	System.out.println("result.size() : " + result.size());
}

/**
 * Query 서비스를 통해 DB에 입력된 데이터를 수정하는 테스트 코드
 */	
public void updateCustomer() throws Exception{
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	//updateBySQL() : 매핑 XML 파일에 쿼리문이 정의되어 있지 않을 때 UPDATE를 실행한다.
	int result = queryService.updateBySQL(
			"update TBL_CUSTOMER set NAME=? where SSNO=?", 
			new String[] {"VARCHAR", "VARCHAR" }, 
			new Object[] { "Anonymous","1234567890123" });
			
    System.out.println("result : " + result);
}

/**
 * Query 서비스를 통해 DB에 입력된 데이터를 삭제하는 테스트 코드
 */
public void deleteCustomer() throws Exception{
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	//removeBySQL() : 매핑 XML 파일에 쿼리문이 정의되어 있지 않을 때 DELETE를 실행한다.
	int result = queryService.removeBySQL(
			"delete from TBL_CUSTOMER where SSNO=?", 
			new String[] { "VARCHAR" },
			new Object[] { "1234567890123" });
			
    System.out.println("result : " + result);
}     				

57.3.3.OR Mapping

다음은 OR mapping을 사용하는 예로, query 정의없이 object만을 사용해서 기본적인 INSERT, UPDATE, DELETE, SELECT를 실행한다.

57.3.3.1.속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml과 Query 서비스에서 읽어들일 매핑 XML 파일의 위치를 정의한 context-query-sqlloader.xml 파일의 일부이다.

<bean id="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
	<property name="jdbcTemplate" ref="jdbcTemplate"/>
	<property name="sqlRepository" ref="sqlLoader"/>
	중략...
</bean>
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
  <property name="dataSource" ref="dataSource" />
</bean> 			

<bean name="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
   	<config:configuration>
        <filename>classpath:/query/mapping-query-ormapping.xml</filename>
 		중략...
	</config:configuration>		
</bean>			

57.3.3.2.매핑 XML 파일 Sample

다음은 앞서 정의한 Query 서비스를 통해 로드된 mapping-query-ormapping.xml 파일의 일부로, 테이블 TBL_ORMapping와 ORMapping 클래스 사이의 매핑 정보를 포함하고 있다.

<queryservice>
    <table-mapping>
        <table name="TBL_ORMapping"
            class="anyframe.example.domain.ORMapping">
            <field-mapping>
                <dbms-column>id</dbms-column>
                <class-attribute>id</class-attribute>
            </field-mapping>
            <field-mapping>
                <dbms-column>name</dbms-column>
                <class-attribute>nm</class-attribute>
            </field-mapping>
            <field-mapping>
                <dbms-column>address</dbms-column>
                <class-attribute>addr</class-attribute>
            </field-mapping>
            <primary-key>
                <dbms-column>id</dbms-column>
            </primary-key>
        </table>
    </table-mapping>
</queryservice>

57.3.3.3.OR Mapping시 사용할 매핑 클래스 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 OR Mapping 정보를 기반으로 특정 테이블에 데이터를 INSERT, UPDATE, SELECT할 때 사용되는 클래스 ORMapping.java 내용의 일부이다.

public class ORMapping implements Serializable {

	public String id;
	public String nm;
	public String addr;
	
	public ORMapping() {
	}
	
	public ORMapping(String i, String n, String a) {
		id = i;
		nm = n;
		addr = a;
	}
	
	// getter, setter ...
}					

57.3.3.4.테스트 코드 Sample

다음은 매핑 XML 파일에 별도 쿼리문을 정의하지 않고 객체만을 이용하여 INSERT, SELECT, UPDATE, DELETE를 실행하는 테스트 코드의 일부이다. 매핑 클래스에 필요한 값을 셋팅하여 Query 서비스에 전달함으로써 해당하는 쿼리문을 실행시킬 수 있다.

/**
 * Query 서비스를 통해 DB에 신규 데이터를 입력하는 테스트 코드
 */	
public void insertORMapping() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	ORMapping ormapping = new ORMapping("1234567890123", "HonggilDong", "Ansan");

	// ORMapping 객체에 초기값을 셋팅하고 이 객체를 통해 INSERT를 실행한다.
	// 다음 코드에 의해 실행되는 쿼리문은
	// INSERT INTO TBL_ORMapping (address ,name ,id ) 
	// values ( '1234567890123', 'HonggilDong', 'Ansan' )	
	int result = queryService.create(ormapping);
    System.out.println("result : " + result);
}

/**
 * Query 서비스를 통해 DB에 입력된 데이터를 조회하는 테스트 코드
 */	
public void selectORMapping() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	ORMapping ormapping = new ORMapping();
	ormapping.setId("1234567890123");

	// primary-key로 지정한 칼럼에 매핑되는 attribute에 값을 입력하고 
	// find를 실행하면 primary-key를 조회 조건으로  하는 SELECT를 실행한다.
	// 다음 코드에 의해 실행되는 쿼리문은 
	// SELECT NAME, ADDRESS FROM TBL_ORMAPPING WHERE ID = '1234567890123'
	Collection result = queryService.find(ormapping);
	if ( result.size() != 1 ){
		throw new Exception("Select failed");
	}
}

/**
 * Query 서비스를 통해 DB에 입력된 데이터를 수정하는 테스트 코드
 */	
public void updateORMapping() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	ORMapping ormapping = new ORMapping("1234567890123", "HonggilDongn", "Seoul");

	// 다음 코드에 의해 실행되는 쿼리문은 
	// Update TBL_ORMapping set address = 'Seoul' ,name = 'HonggilDong'  where id = '1234567890123'	
	int result = queryService.update(ormapping);
	if ( result == -1 ){
		throw new Exception("Update failed");
	}
}

/**
 * Query 서비스를 통해 DB에 입력된 데이터를 삭제하는 테스트 코드
 */	
public void deleteORMapping() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");
		
	ORMapping ormapping = new ORMapping();
	ormapping.setId("1234567890123");
	
	// 다음 코드에 의해 실행되는 퀴리문은 
	// DELETE FROM TBL_ORMapping where id = 1234567890123	
	int result = queryService.remove(ormapping);
	if ( result == -1 ){
		throw new Exception("Update failed");
	}
}					

57.3.4.Dynamic Query

Query 서비스는 text 치환, named parameter 형태 등을 통해 운영 시 입력된 조건 값에 따라 동적으로 변경되는 쿼리문 정의를 지원한다. 이를 위해서는 다음과 같은 syntax를 사용할 수 있다.

  • :ParameterName : 특정 쿼리문에 입력되어야 할 변수를 지정할 때 사용한다.

  • {{치환 문자열 키}} : 치환 문자열 키에 해당하는 값으로 치환되어야 하는 부분에 정의한다.

  • #if ~ (#elseif) ~ #end : 조건 분기가 필요한 부분에 정의한다.

  • # foreach ~ #end : Loop가 필요한 부분에 정의한다.

  • $velocityCount : foreach 구문내의 Loop index를 체크하고자 하는 부분에 정의한다.

57.3.4.1.속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml 과 Query 서비스에서 읽어들일 매핑 XML 파일의 위치를 정의한 context-query-sqlloader.xml 파일의 일부이다.

<bean id="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
	<property name="jdbcTemplate" ref="jdbcTemplate"/>
	<property name="sqlRepository" ref="sqlLoader"/>
	중략...
</bean>
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>          

<bean name="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
   	<config:configuration>
        <filename>classpath:/query/mapping-query-dynamic.xml</filename>
 		중략...
	</config:configuration>		
</bean>			

57.3.4.2.매핑 XML 파일 Sample

다음은 앞서 정의한 Query 서비스를 통해 로드된 mapping-query-dynaminc.xml의 일부로, 여러 유형의 dynamic 쿼리문을 포함하고 있다.

<queries>	
    <query id="getLogonIdByNamedParam" isDynamic="true">
	    <statement>
            SELECT LOGON_ID 
            FROM TBL_USER 
            WHERE LOGON_ID BETWEEN :lowId AND :highId
         </statement>
         <param name="lowId" type="VARCHAR"/>
         <param name="highId" type="VARCHAR"/>
	</query>
    <query id="getEmployeesByTextReplacement" isDynamic="true">
        <statement>
            SELECT LOGON_ID 
            FROM {{schema}} 
            ORDER BY {{sortColumn}}
        </statement>
    </query>
    <query id="getLogonIdByIf" isDynamic="true">
        <statement>
            SELECT LOGON_ID 
            FROM TBL_USER 
            #if ($id && !$id.equals(""))
            WHERE LOGON_ID like '%' || :logonId || '%'
            #end 
            ORDER BY {{sortColumn}}
        </statement>
        <param name="logonId"	type="VARCHAR"/>
    </query> 
    <query id="getLogonIdByForeach" isDynamic="true">
        <statement>
            SELECT LOGON_ID, NAME 
            FROM TBL_USER 
            WHERE LOGON_ID IN (
                #foreach ($logonId in $logonIdList) 
                    #if ($velocityCount	== 1 ) 
                        '$logonId' 
                    #else 
                        , '$logonId' 
                    #end
                #end 
            ) 
            ORDER BY NAME
        </statement> 
    </query> 	
</queries>		

57.3.4.3.테스트 코드 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 parameter 값에 따라 동적으로 변경 가능한 쿼리문들을 실행하는 테스트 코드의 일부이다.

/**
 * Named Parameter를 이용해서 query id가 'getLogonIdByNamedParam'인 Dynamic 쿼리문을 실행한다.
 * getLogonIdByNamedParam : key가 lowId, highId인 Named Parameter의 값을 key=value 형태로 Query 
 * 서비스에 전달하면 Query 서비스는 해당 value를 PreparedStatement에 셋팅하고 해당 쿼리문을 실행한다.
 * 이 메소드에서는 "lowId = a", "highId = z"라는 paramter를  Object[] 형태로 Query 서비스에 전달하고 있으며, 
 * 이 때 실행되는 쿼리문은 다음과 같다.
 * SELECT LOGON_ID FROM TBL_USER WHERE LOGON_ID BETWEEN 'a' AND 'z'
 */
public void dynamicQueryUsingNamedParameter() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	Object[] iVal = new Object[2];
	iVal[0] = new Object[]{"lowId", "a"};
	iVal[1] = new Object[]{"highId", "z"};
	
	ArrayList rtList 
		= (ArrayList)(queryService.find("getLogonIdByNamedParam", iVal));
    System.out.println("rtList.size() : " + rtList.size());
}

/**
 * {{치환문자열키}}를 이용해서  query id가 'getEmployeesByTextReplacement'인 Dynamic 쿼리문을 실행한다.
 * getEmployeesByTextReplacement : key가 schema, sortColumn인 치환문자열의 값을 key=value 형태로 Query 
 * 서비스에 전달하면 Query 서비스는 해당 value를 문자열로 그대로 치환해서 사용한다. 
 * 이 메소드에서는 "schema=TBL_USER", "sortColumn=NAME"라는 parameter를 Object[] 형태로 Query 서비스에 전달하고 
 * 있으며, 이 때 실행되는 쿼리문은 다음과 같다.
 * SELECT * FROM TBL_USER ORDER BY NAME
 */
public void dynamicQueryUsingTextreplace() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	Object[] iVal = new Object[2];
	iVal[0] = new Object[]{"schema", "TBL_USER"};
	iVal[1] = new Object[]{"sortColumn", "NAME"};
	
	ArrayList rtList 
		= (ArrayList)(queryService.find("getEmployeesByTextReplacement", iVal));
    System.out.println("rtList.size() : " + rtList.size());
}

/**
 * 조건분기를 위한 #if를 이용해서  query id가 'getLogonIdByIf'인 Dynamic 쿼리문을 실행한다.
 * getLogonIdByIf : key가 id인 parameter의 값이 Null이 아니고, 빈 문자열도 아니라면, #if문 내에 
 * 포함된 WHERE절이 실행될 쿼리문에 포함된다. (#if문이 끝나는 지점에는 반드시 #end를 정의해주어야 함에 
 * 유의하자.)
 * 또한, WHERE절 내에서는  '%' || :logonId || '%'와 같은 형태의 문장을 사용하고 있는데 이것은 Named 
 * Parameter의 값에 앞뒤로 %를 붙인 형태의 문자열을 만들어 내기 위함이다. 
 * (이 때, '%', ||, :logonId 사이에는 빈 칸을 두어야 WHERE절이 정상적으로 동작한다. )
 * 이 메소드에서는 "id=test", "sortColumn=NAME" 라는 parameter를 Object[] 형태로 Query 서비스에 전달하고 있으며
 * 실행되는 쿼리문은 다음과 같다.
 * SELECT LOGON_ID 
 * FROM TBL_USER 
 * WHERE LOGON_ID like '%test%' 
 * ORDER BY NAME
 */
public void dynamicQueryUsingCondition() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	Object[] iVal = new Object[3];
	iVal[0] = new Object[]{"id", "yes"};
	iVal[1] = new Object[]{"logonId", "test"};
	iVal[2] = new Object[]{"sortColumn", "NAME"};
	
	ArrayList rtList 
		= (ArrayList)(queryService.find("getLogonIdByIf", iVal));
    System.out.println("rtList.size() : " + rtList.size());
}

/**
 * Loop를 위한 #foreach를 이용해서  query id가 'getLogonIdByForeach'인 Dynamic 쿼리문을 실행한다.
 * getLogonIdByForeach : key가 logonIdList인  parameter의 값은 List 형태이며, List에서 순서대로 
 * 추출된 값들은 내부적으로 logonId라는 변수에 셋팅된다. logonId는 Query 서비스에 전달되어야 하는 입력 
 * parameter가 아니다.
 * velocityCount가 1인 경우 즉, 첫번째 Loop일 경우 logonIdList의 첫번째 값이 그대로 추가되고 그 이후부터는 
 * ,를 붙인 값이 추가되게 된다. (#foreach, #if문이 끝나는 지점에는 반드시 #end를 정의해주어야 함에 유의하자.)
 * 이 메소드에서는 logonIdList의 값이 "admin", "test"라는 두개의 문자열로 구성된 ArrayList를 Object[] 
 * 형태로 Query 서비스에 전달하고 있으며 실행되는 쿼리문은 다음과 같다.
 * SELECT LOGON_ID, NAME 
 * FROM TBL_USER 
 * WHERE LOGON_ID IN ('admin', 'test') 
 * ORDER BY NAME
 */
public void dynamicQueryUsingLoop() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");

	List logonIdList = new ArrayList();
	logonIdList.add("admin");
	logonIdList.add("test");

	Object[] iVal = new Object[]{"logonIdList", logonIdList};

	ArrayList rtList 
		= (ArrayList) (queryService.find("getLogonIdByForeach", new Object[]{ iVal })); 

	if (rtList.size() != 1) {
		throw new Exception("Dynamic Query Using Condition failed");
	}
} 					

Dynamic Query 정의시 유의사항

Query Service를 이용하여 목록 조회 결과를 특정 객체에 매핑하기 위해 쿼리문 실행시마다 매핑 정보 셋팅을 위한 로직이 반복 수행된다. Query Service의 성능을 개선하기 위해 Anyframe 4.0.0 이후부터 이 로직을 변경하여 한 번 처리한 Result Mapping 정보를 SQLLoader에서 queryId를 기준으로 관리하도록 하였다. 따라서 조회되는 칼럼 정보가 쿼리문 실행시마다 변경될 수 있는 SELECT, FROM 절이 가변적으로 변경되는 쿼리문인 경우에는 Result Mapping 정보가 쿼리문 실행시마다 변경될 수 있으므로 별도 Mapper 클래스를 통해 처리될 수 있도록 해야 한다. Anyframe에서 anyframe.core.query.impl.jdbc.mapper 패키지 하위에 제공되는 Mapper 클래스는 다음과 같은 역할을 수행한다.

표 57.1. Mapper Class

Class NameDescription
DefaultCallbackResultSetMapper[기본 적용] 조회 결과를 Map 형태로 전달받는 경우 Query Service에서 기본적으로 사용하는 Mapper
DefaultReflectionResultSetMapper[기본 적용] 조회 결과를 특정 VO 형태로 전달받는 경우 Query Service에서 기본적으로 사용하는 Mapper
CallbackResultSetMapper 조회 결과를 Map 형태로 전달받는 경우 쿼리문 실행 시점마다 칼럼 매핑을 처리하는 Mapper. SELECT 절이 동적으로 구성되어 있어 쿼리문 실행시마다 조회 결과가 달라지는 쿼리문인 경우 활용 가능
ReflectionResultSetMapper 조회 결과를 특정 VO 형태로 전달받는 경우 쿼리문 실행 시점마다 칼럼 매핑을 처리하는 Mapper. SELECT 절이 동적으로 구성되어 있어 쿼리문 실행시마다 조회 결과가 달라지는 쿼리문인 경우 활용 가능
MappingStyleColumnMapRowMapper StoredProcedure 수행시 CamelCase, Lower, Upper와 같은 Result Mapping Style을 적용하기 위해 Query 서비스에서 기본적으로 사용하는 Mapper

다음은 조건에 따라 FROM 절이 변경되는 쿼리문의 예이다. 앞서 언급한 바와 같이 QueryService에서는 기본적으로 queryId를 기준으로 한번 실행된 쿼리문에 대해 Result Mapping 정보를 저장해두고 있기 때문에 다음과 같이 SELECT 또는 FROM 절이 변경되는 쿼리문의 경우 처음 실행된 쿼리문과 이후 실행되는 쿼리문이 달라질 수 있게 된다. 따라서 현재 실행된 쿼리문의 결과와 queryId를 기준으로 저장된 Result Mapping 정보가 맞지 않아 원치 않은 결과가 도출될 수 있을 것이다. 이와 같은 경우에는 다음과 같이 <result mapper="..."/>를 이용하여 해당 쿼리문을 별도 Mapper를 통해 처리될 수 있도록 정의해 주어야 함을 기억하도록 하자.

'findUsers' 쿼리문의 경우 쿼리 수행 결과를 Map으로 전달받기 위해 CallbackResultSetMapper를 mapper로 정의하고 있음을 알 수 있다.

<query id="findUsers" isDynamic="true">
    <statement>
        SELECT LOGON_ID FROM {{schema}} ORDER BY {{sortColumn}}
    </statement>
    <result mapper="anyframe.core.query.impl.jdbc.mapper.CallbackResultSetMapper"/>
</query>

만일 'findUsers' 쿼리문의 수행 결과를 User Defined Object로 전달받기 위해서는 앞서 언급한 ReflectionResultSetMapper를 mapper로 정의하고, 전달받고자 하는 클래스를 명시해주면 된다.

<query id="findUsers" isDynamic="true">
    <statement>
        SELECT LOGON_ID FROM {{schema}} ORDER BY {{sortColumn}}
    </statement>
    <result class="anyframe.core.query.vo.UsersVO"  
        mapper="anyframe.core.query.impl.jdbc.mapper.ReflectionResultSetMapper"/>
</query>

위에서 제시한 Mapper 외에 특정 쿼리문에 적합한 Custom ResultSetMapper 클래스를 별도 구현할 수 있으며 이것은 Result Mapper 정의 방법을 참고하도록 한다.

57.3.5.Pagination

Query 서비스를 통해 조회 유형의 쿼리문 실행시 전체 결과를 조회하지 않고 해당 페이지에 속한 데이터만 조회할 수 있으므로, 효율적이고 성능에 유리하다. Query 서비스는 페이징 처리를 위해 별도의 로직이나 특정 DB에 종속적인 쿼리문을 요구하지 않는다. 다만 조회하고자 하는 페이지의 번호와 한 페이지에 보여지는 데이터의 개수만 입력 parameter로 전달하면 된다.

57.3.5.1.속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml 과 Query 서비스에서 읽어들일 매핑 XML 파일의 위치를 정의한 context-query-sqlloader.xml파일의 일부이다. (* 조회 유형의 쿼리에 대해 페이징 처리를 수행하기 위해서는 해당 DB에 맞는 PagingSQLGenerator를 추가 셋팅해주어야 함에 유의하도록 한다.)

Pagination시 유의 사항

QueryService 4.1.0 이후부터 QueryService 속성 정의시에는 반드시 DBMS에 적합한 PagingSQLGenerator를 셋팅해 주어야 한다. 적절한 PagingSQLGenerator가 존재하지 않는 경우에는 QueryService에서 제공하는 anyframe.core.query.impl.jdbc.generator.DefaultPagingSQLGenerator를 사용할 수 있으나, DefaultPagingSQLGenerator를 이용하여 findXXX() 메소드를 실행하면 QueryService 내부적으로 조건에 해당하는 모든 데이터를 fetch한 이후 ResultSet Cursor의 위치를 이동시키는 방식으로 특정 페이지에 속한 데이터를 걸러낸다. 이 때 ResultSet Cursor를 움직이는 로직에서 상당한 시간이 소요되어 다량의 데이터 조회시 성능에 심각한 영향을 끼칠 수 있다. 따라서, DefaultPagingSQLGenerator 사용은 권장하지 않는다.

<bean id="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
	<property name="jdbcTemplate" ref="jdbcTemplate"/>
	<property name="sqlRepository" ref="sqlLoader"/>
	<property name="pagingSQLGenerator" ref="pagingSQLGenerator"/>
	중략...
</bean>
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>  
<bean id="pagingSQLGenerator" class="anyframe.core.query.impl.jdbc.generator.OraclePagingSQLGenerator"/>  

<bean name="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
    <config:configuration>
        <filename>classpath:/query/mapping-query-pagination.xml</filename>
                중략...
    </config:configuration>		
</bean>  		

57.3.5.2.매핑 XML 파일 Sample

다음은 앞서 정의한 Query 서비스를 통해 로드된 mapping-query-pagination.xml의 일부로, 테이블 매핑 정보와 쿼리문을 포함하고 있다.

<query id="selectUsingPagination">
    <statement>
        SELECT NAME FROM TBL_CUSTOMER WHERE SSNO like ?
    </statement>
    <param type="VARCHAR" />
    <result class="anyframe.example.domain.Customer"/>
</query>		

57.3.5.3.테스트 코드 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 SELECT 쿼리문을 실행하는 테스트 코드의 일부로, 페이징 처리된 수행 결과를 얻어 내기 위한 테스트 로직을 포함하고 있다.

/**
 * SELECT 수행 결과를 특정 페이지별로 조회하는 테스트 코드
 * 이 메소드에서는 TBL_CUSTOMER 테이블에 여러 데이터가 입력되었을 때, 한 페이지에 표현하고자 하는 데이터의 개수(5)와 
 * 조회 페이지 페이지 번호(3)를 입력하여  특정 페이지에 속한 데이터만 조회해 본다. 
 */
public void selectCustomer() throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("queryService");
	
    /** findWithRowCount() : 매핑 XML 파일에 정의되어 있는 query id를 이용하여 SELECT를 실행한다.
     *                       findWithRowCount()는 한번의 호출로 해당 SELECT로 얻을 수 있는 전체 
     *                       데이터의 개수와 특정 페이지에 해당하는 결과값들을 얻어 올 수 있다.
     */
    HashMap resultMap = queryService.findWithRowCount("selectUsingPagination",
        new Object[] { "%1234%" }, 3, 5);
			
    Customer customer = new Customer();
    Map rsMap = new HashMap();

    // 특정 페이지에 속한 결과값들 추출
    List resultList = (List) resultMap.get(IQueryService.LIST);
    for(int i = 0 ; i < resultList.size() ; i ++){
        customer = (Customer)resultList.get(i);
        customer.getNm();
    }  
	
    // 해당 SELECT로 얻어질 수 있는 전체 데이터의 개수 추출
    int totalSize = ((Long) resultMap.get(IQueryService.COUNT)).intValue();
    if (resultList.size() != 5 || totalSize != 15) {
        throw new Exception("Select query failed");
    }
}					

다음은 Query 서비스에서 제공하는 페이징 처리 기능을 수행하는 findXXX() 메소드 사용시 알아두어야 할 사항들이다. public Collection find...(..., int pageIndex, int pageSize);

  1. 페이지 번호가 1보다 작으면 결과 데이터가 없다. (Anyframe 3.2.0 이후)

  2. 페이지 번호가 1보다 작으면 첫번째 페이지가 조회된다. (Anyframe 3.2.0 이전 )

  3. 한 페이지에 보여줘야 하는 데이터의 개수는 0보다 커야 한다.

  4. 페이지 번호와 한 페이지에 보여줘야 하는 데이터의 개수를 사용하여 계산한 값이 전체 결과 데이터의 개수보다 크면 결과 데이터가 없다.(Anyframe 3.2.0 이후)

  5. 페이지 번호와 한 페이지에 보여줘야 하는 데이터의 개수를 사용하여 계산한 값이 전체 결과 데이터의 개수보다 크면 마지막 페이지가 조회된다. (Anyframe 3.2.0 이전)

  6. 한 페이지에 보여줘야 하는 데이터의 개수가 생략될 경우 매핑 XML 파일의 <result>에 별도로 정의해 주어야 한다.

    <query id="selectUsingPagination" isDynamic="false">
        <statement>
            SELECT NAME FROM TBL_CUSTOMER WHERE SSNO like ?
        </statement>
        <param type="VARCHAR" />
        <result length="10" class="anyframe.example.domain.Customer"/>
    </query>					

57.3.6.Batch Update

Query 서비스는 JDBC 2.0 batch updates를 사용해서 한번의 호출로 여러 건의 데이터를 INSERT, UPDATE, DELETE할 수 있게 한다.

57.3.6.1.속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml과 Query 서비스에서 읽어들일 매핑 XML 파일의 위치를 정의한 context-query-sqlloader.xml 파일의 일부이다.

<bean id="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
	<property name="jdbcTemplate" ref="jdbcTemplate"/>
	<property name="sqlRepository" ref="sqlLoader"/>
	중략...
</bean>
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>  		

<bean name="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
    <config:configuration>
         <filename>classpath:/query/mapping-query-batch.xml</filename>
 		 중략...
    </config:configuration>		
</bean>			

57.3.6.2.매핑 XML 파일 Sample

다음은 앞서 정의한 Query 서비스를 통해 로드된 mapping-query-batch.xml 파일의 일부로, 테이블 매핑 정보와 Batch로 처리할 쿼리문을 포함하고 있다.

<queryservice>
    <table-mapping>
        <table name="TBL_CUSTOMER"
            class="anyframe.example.domain.Customer">
            <field-mapping>
                <dbms-column>ssno</dbms-column>
                <class-attribute>ssno</class-attribute>
            </field-mapping>
            <field-mapping>
                <dbms-column>name</dbms-column>
                <class-attribute>nm</class-attribute>
            </field-mapping>
            <field-mapping>
                <dbms-column>address</dbms-column>
                <class-attribute>addr</class-attribute>
            </field-mapping>
            <primary-key>
                <dbms-column>ssno</dbms-column>
            </primary-key>
        </table>
    </table-mapping>
    <queries>
        <query id="insertbatch">
            <statement>
                INSERT INTO TBL_CUSTOMER ( ssno, name, address ) VALUES (?,?,?)
            </statement>
            <param type="VARCHAR" />
            <param type="VARCHAR" />
            <param type="VARCHAR" />
        </query>
    </queries>
</queryservice>

57.3.6.3.테스트 코드 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 INSERT 쿼리문을 Batch Update를 이용해 실행하는 테스트 코드의 일부이다. 입력 parameter를 LIST 형태로 담아서 Query 서비스에 전달하면, Query 서비스는 한 번의 실행으로 전달된 List 개수 만큼의 쿼리문을 실행한다.

/**
 * 매핑 XML 파일에 정의된 쿼리문을 batch update를 이용해 실행.
 */
public void insertCustomer() throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("queryService");

    //ArrayList에 입력할 데이터를 저장한다.
    ArrayList args = new ArrayList();
    Object[] arg = new Object[3];
    arg[0] = "1234567890123";
    arg[1] = "KimMinsu";
    arg[2] = "Ansan";
    args.add(arg);
    arg = new Object[3];
    arg[0] = "1234567890124";
    arg[1] = "LeeSungwook";
    arg[2] = "Seoul";
    args.add(arg);
    arg = new Object[3];
    arg[0] = "1234567890125";
    arg[1] = "ParkHeejin";
    arg[2] = "Seoul";
    args.add(arg);

    //ArrayList에 3개의 Object가 포함되어 있으므로 query id가 insertbatch인
    //쿼리문이 3번 실행된다.
    int[] results = queryService.batchUpdate("insertbatch", args);
    for (int i = 0; i < results.length; i++) {
        if (results[i] == -1) {
            throw new Exception("Batch Insert falied");
        }
    }
}

/**
 * 매핑 XML 파일에 쿼리문이 정의되어 있지 않을 때, 실행할 쿼리문을 직접 입력하여
 * batch update를 실행 
 */
public void insertCustomerBySQLQuery() throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("queryService");
    
    //실행 할 쿼리문을 정의
    String sql = "INSERT INTO TBL_CUSTOMER ( ssno, name, address ) VALUES (?,?,?)";

    //입력할 parameter의 SQL Type을 정의
    String[] types = new String[3];
    types[0] = "VARCHAR";
    types[1] = "VARCHAR";
    types[2] = "VARCHAR";

    //ArrayList에 입력할 데이터를 저장한다.
    ArrayList args = new ArrayList();
    Object[] arg = new Object[3];
    arg[0] = "1234567890126";
    arg[1] = "HongGildong";
    arg[2] = "Suwon";
    args.add(arg);
    arg = new Object[3];
    arg[0] = "1234567890127";
    arg[1] = "LeeSoonsin";
    arg[2] = "Seongnam";
    args.add(arg);
    arg = new Object[3];
    arg[0] = "1234567890128";
    arg[1] = "ChoiMinsu";
    arg[2] = "Seoul";
    args.add(arg);

    //ArrayList에 3개의 Object가 포함되어 있으므로 해당 쿼리문이 3번 실행된다.	
    int[] results = queryService.batchUpdateBySQL(sql, types, args);

    for (int i = 0; i < results.length; i++) {
        if (results[i] == -1) {
            throw new Exception("BatchInsertBySQL falied");
        }
    }
}

/**
 * OR Mapping을 이용해서 batch update를 실행
 */
public void insertCustomerByObject() throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("queryService");

    ArrayList args = new ArrayList();
    Customer customer = new Customer();

    customer.setSsno("1234567890129");
    customer.setNm("Smith");
    customer.setAddr("LA");
    args.add(customer);

    customer = new Customer();
    customer.setSsno("1234567890130");
    customer.setNm("Brown");
    customer.setAddr("Newyork");
    args.add(customer);

    customer = new Customer();
    customer.setSsno("1234567890131");
    customer.setNm("Eugene");
    customer.setAddr("Boston");
    args.add(customer);

    // 실행되는 쿼리문은 기본적인 INSERT문인
    // insert into TBL_CUSTOMER (ssno ,address ,name ) values ( ?, ?, ? )이다.
    int[] results = queryService.batchCreate(args);

    for (int i = 0; i < results.length; i++) {
        if (results[i] == -1) {
            throw new Exception("BatchInsertByObject falied");
        }
    }
}					

57.3.7.Callable Statement

CallableStatment는 표준 DDL, DML이 아닌 DB의 Stored Procedure, Function 등을 호출할 때 사용한다. Stored Procedure는 쿼리문을 하나의 파일 형태로 만들거나 DB에 저장해두고 함수처럼 호출해서 사용하는 것으로, 성능, 코드의 독립성, 보안성 등의 다양한 이점을 제공한다.

57.3.7.1.속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml 과 Query 서비스에서 읽어들일 매핑 XML 파일의 위치를 정의한 context-query-sqlloader.xml 파일의 일부이다.

<bean name="oracle_queryservice" class="anyframe.core.query.impl.QueryServiceImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>		
    <property name="sqlRepository" ref="sqlLoader"/>		
	중략...
</bean>	
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>  		

<bean name="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
    <config:configuration>
        <filename>classpath:/query/mapping-query-callablestatement.xml</filename>
                중략...
    </config:configuration>		
</bean>			

57.3.7.2.매핑 XML 파일 Sample

다음은 앞서 정의한 Query 서비스를 통해 로드된 mapping-query-callablestatement.xml 파일의 일부로, DB에 기 생성되어 있는 test_function이라는 Function을 실행시키기 위한 쿼리문을 포함하고 있다.

<queryservice>
    <queries>	
        <query id="callFunction">
            <statement>{? = call test_function(?)}</statement>
            <param type="NUMERIC" binding="OUT" name="outVal" />
            <param type="NUMERIC" binding="IN" name="inVal" />
        </query>
    </queries>    
</queryservice>	

57.3.7.3.테스트 코드 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 Function을 호출하기 위한 쿼리문을 실행하는 테스트 코드의 일부이다. Query 서비스를 통해 DB Function을 실행시키기 위해서는 사용하는 DB에 해당 Function이 미리 생성되어 있어야 한다.

/**
 * 매핑 XML 파일에 정의되어 있는 query id를 이용해 Function을 호출한다.
 */
public void callableStatementFunction() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("queryService");
	
	HashMap inVal = new HashMap();
	inVal.put("inVal", new Integer(10));
	
	Map results = queryService.execute("callFunction", inVal);
	BigDecimal rtVal = (BigDecimal) results.get("outVal");
	
	if (rtVal.intValue() != 1 ){
		throw new Exception("testCallableStatementProcedure failed");
	}
}

/**
 * 매핑 XML 파일에 정의되어 있지 않은 경우에도 특정 Function을 호출할 수 있다.
 * Map executeBySQL(String sql, String[] types, String[] names, String[] bindings, Map values) 
 */
public void callableStatementBySQL() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("oracle_queryservice");
	
	String sql = "{? = call test_function(?)}";
	String[] types = { "NUMERIC", "NUMERIC" };
	String[] bindings = { "OUT", "IN" };
	String[] names = { "outVal", "inVal" };
	
	HashMap inVal = new HashMap();
	inVal.put("inVal", new Integer(10));
	
	Map results = queryService.executeBySQL(sql, types, names, bindings, inVal);
	BigDecimal rtVal = (BigDecimal) results.get("outVal");

	if (rtVal.intValue() != 1 ){
		throw new Exception("testCallableStatementBySQL failed");
	}
}	

/**
 * 매핑 XML 파일에 정의되어 있지 않은 경우에도 특정 Function을 호출할 수 있다.
 * 이 메소드에서는 페이징 처리된 조회 결과를 얻고자 한다.
 * Map executeBySQL(String sql, String[] types, String[] names, 
 * 					String[] bindings, Map values, int pageIndex, int pageSize) 
 */
public void callableStatementBySQLWithPaging() throws Exception {
	IQueryService queryService = (IQueryService) context.getBean("oracle_queryservice");
	
	String sql = "{? = call test_function(?)}";
	String[] types = { "NUMERIC", "NUMERIC" };
	String[] bindings = { "OUT", "IN" };
	String[] names = { "outVal", "inVal" };
	
	HashMap inVal = new HashMap();
	inVal.put("inVal", new Integer(10));
	
	Map results = queryService.executeBySQL(sql, types, names, bindings, inVal, 1, 1);
	BigDecimal rtVal = (BigDecimal) results.get("outVal");

	if (rtVal.intValue() != 1 ){
		throw new Exception("testCallableStatementBySQLWithPaging failed");
	}
}					

57.3.8.CLOB, BLOB

기본적으로 LOB 유형의 데이터와 다른 유형의 데이터를 다루는 방식은 다르지 않다. 다만, LOB 유형의 데이터 조작이 필요한 경우에는 Query Service 속성 정의시 Lob Handler를 추가로 정의해 주어야 함을 기억하도록 하자. LobHandler 속성 정의에 대한 자세한 내용은 [lobHandler] 속성을 참고하도록 한다. 여기에서는 Oracle 9i 이상, Oracle 8i로 구분하여 LOB 유형의 데이터를 다루는 방법에 대해 살펴볼 것이다.

57.3.8.1.Oracle 9i 이상일 경우

속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml 과 Query 서비스에서 읽어들일 매핑 XML 파일의 위치를 정의한 context-query-sqlloader.xml 파일의 일부이다.

<bean name="oracle_queryservice" class="anyframe.core.query.impl.QueryServiceImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>		
    <property name="sqlRepository" ref="sqlLoader"/>		
    <property name="lobHandler" ref="lobHandler"/>
	중략...
</bean>	
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>   
<bean id="nativeJdbcExtractor" 
    class="org.springframework.jdbc.support.nativejdbc.CommonsDbcpNativeJdbcExtractor" 
    lazy-init="true"/>
<bean id="lobHandler" class="org.springframework.jdbc.support.lob.OracleLobHandler" lazy-init="true">
    <property name="nativeJdbcExtractor" ref="nativeJdbcExtractor"/>
</bean>          	

<bean name="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
    <config:configuration>
        <filename>classpath:/query/mapping-query-lob.xml</filename>
                중략...
    </config:configuration>		
</bean>				

매핑 XML 파일 Sample

다음은 앞서 정의한 Query 서비스를 통해 로드된 mapping-query-lob.xml 파일의 일부로, 테이블 매핑 정보와 LOB 유형의 데이터를 다루는 다양한 쿼리문들을 포함하고 있다.

<queries>  		
    <query id="insertLOB">
        <statement>insert into longVarchar values(?,?,?)</statement>
        <param type="INTEGER"/>
        <param type="BLOB"/>
        <param type="CLOB"/>
    </query>
    <query id="selectLOB">
        <statement>select myblob, myclob from longVarchar where count = ?</statement>
        <param type="INTEGER"/>
    </query>
    <query id="updateLOB">
        <statement>update longVarchar set myblob = ?, myclob = ? WHERE count = ?</statement>
        <param type="BLOB"/>
        <param type="CLOB"/>
        <param type="INTEGER"/>
    </query>
    <query id="deleteLOB">
        <statement>delete from longVarchar WHERE count = ?</statement>
        <param type="INTEGER"/>
    </query>
</queries>			

테스트 코드 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 LOB 유형의 데이터를 INSERT, SELECT, UPDATE, DELETE하는 테스트 코드의 일부이다.

/**
 * Query 서비스를 통해 DB에 신규 LOB 유형의 데이터를 입력하는 테스트 코드
 */
public void insertLOB() throws Exception{
    IQueryService queryService = (IQueryService) context.getBean("oracle_queryservice");
	
    String strVal1 = "0무궁화꽃이피었습니다";
    String strVal2 = "1무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다
                      무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다
                      무궁화꽃이피었습니다무궁화꽃이피었습니다\n";
    strVal2 += "2무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다
                무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다
                무궁화꽃이피었습니다무궁화꽃이피었습니다\n";
    // skip
    strVal2 += "30무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다
               무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다무궁화꽃이피었습니다
               무궁화꽃이피었습니다무궁화꽃이피었습니다\n";
	
    int result = queryService.create("insertLOB"
        , new Object[] { new Integer(5), strVal1.getBytes() , strVal2 });
    System.out.println("result : " + result);
}

/**
 * Query 서비스를 통해 DB에 입력된 LOB 유형의 데이터를 조회하는 테스트 코드
 */
public void selectLOB() throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("oracle_queryservice");
	
    Collection result = queryService.find("selectLOB", new Object[] { new Integer(5) });

    Iterator resultItr = result.iterator();
    while (resultItr.hasNext()) {
        Map binary = (Map) resultItr.next();
        String myCLOB = (String) binary.get("myclob");
        String myBLOB = new String((byte[]) binary.get("myblob"));
    }
	
    System.out.println("result : " + result);
}

/**
 * Query 서비스를 통해 DB에 입력된 LOB 유형의 데이터를 수정하는 테스트 코드
 */
public void updateLOB() throws Exception{
    IQueryService queryService = (IQueryService) context.getBean("oracle_queryservice");
	
    String strVal1 =  "0장미꽃이피었습니다";
    String strVal2 = "1장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다
                     장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다
                     장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다\n";
    strVal2 += "2장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다
               장미꽃이피었습니다 장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다
               장미꽃이피었습니다장미꽃이피었습니다\n";
    // skip
    strVal2 += "30장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다
               장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다장미꽃이피었습니다
               장미꽃이피었습니다장미꽃이피었습니다\n";
	
    int result = queryService.create("updateLOB"
        , new Object[] { strVal1.getBytes() , strVal2,  new Integer(5)});
}

/**
 * Query 서비스를 통해 DB에 입력된 LOB 유형의 데이터를 삭제하는 테스트 코드
 */
public void deleteLOB() throws Exception{
	IQueryService queryService = (IQueryService) context.getBean("oracle_queryservice");
	queryService.remove("deleteLOB", new Object[]{new Integer(5)});
}						

57.3.8.2.Oracle 8i일 경우

속성 정의 파일 Sample

Spring에서 제공하는 Oracle LobHandler는 9i 이상에서만 적용 가능하므로, Oracle 8i 기반에서 LOB 유형의 데이터를 다루기 위해서는 Query 서비스 속성 정의시에 Anyframe에서 제공하는 LobHandler를 셋팅해 주어야 한다. 다음은 Query 서비스를 정의한 context-query.xml 파일의 일부로, Oracle 8i용으로 제공된 AnyframeOracle8iLobHandler를 정의한 부분이다.

<bean id="lobHandler" class="anyframe.core.query.impl.jdbc.lob.AnyframeOracle8iLobHandler"
    lazy-init="true">
    <constructor-arg value="net.sf.log4jdbc.ResultSetSpy"/>
    <property name="nativeJdbcExtractor" ref="nativeJdbcExtractor"/>
</bean>				

DriverSpy를 써서 실행되는 쿼리문을 로그로 남기고 있을 경우에는 LobHandler의 <constructor-arg>를 정의하고, 아닐 경우에는 제거해도 된다.

DataSource 서비스 속성 정의 파일 Sample

반드시 defaultAutoCommit의 속성을 false로 변경한다.

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
    <property name="url" value="jdbc:oracle:thin:@localhost:1521:oracle"/>
    <property name="username" value="system"/>
    <property name="password" value="manager"/>
    <property name="defaultAutoCommit" value="false"/>     
</bean>				

TransactionManager 속성 정의 파일 Sample

DataSource 서비스의 defaultAutoCommit 속성을 false로 설정했으므로, TransactionManager 셋팅이 필요하다.

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 
    <property name="dataSource" ref="dataSource"/>
</bean>

<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRES_NEW"
            rollback-for="Exception" />
    </tx:attributes>
</tx:advice>    

<aop:config>
    <aop:pointcut id="executionMethods"
        expression="execution(* anyframe.core.query..impl.QueryServiceImpl.*(..))" />
    <aop:advisor advice-ref="txAdvice"
        pointcut-ref="executionMethods" />
</aop:config>		

매핑 XML 파일 Sample

신규 LOB 유형의 데이터를 INSERT하는 쿼리문의 경우에는 기존 쿼리문 정의와 다르게 lobStatement tag를 추가 정의해 주어야 한다.

<query id="insertBlobClobWithOra8i" isDynamic="false">
    <statement>insert into binary values(?,empty_blob(),empty_clob())</statement>       
    <param  type="INTEGER"/>	
    <lobStatement>
        <statement>select  myclob, myblob from binary where bin_id = ? for update</statement>
        <param type="INTEGER"/>             
    </lobStatement>                                                 
</query>					

57.3.9.Named Parameter 'vo' 활용

매핑 XML 파일에 정의되어 있는 쿼리문을 실행시키기 위해 필요한 입력 Parameter들을 Query 서비스에 전달할 때, 개별 Parameter들을 포함하고 있는 VO 유형의 객체로 전달 가능하다. 따라서 입력 Parameter 개수나 이름이 변경되더라도, Query 서비스 사용 로직의 변경없이 매핑 XML 내의 해당되는 쿼리문만 변경함으로써 변경 사항 적용이 가능해지므로 쿼리문 변경이 용이해지는 장점을 제공한다.

57.3.9.1.속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml 과 Query 서비스에서 읽어들일 매핑 XML 파일의 위치를 정의한 context-query-sqlloader.xml 파일의 일부이다.

<bean id="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
    <property name="sqlRepository" ref="sqlLoader"/>
        중략...
</bean>
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>				

<bean name="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
    <config:configuration>
        <filename>classpath:/query/mapping-query-namedparamvo.xml</filename>
                중략...
    </config:configuration>		
</bean>				

57.3.9.2.매핑 XML 파일 Sample

다음은 앞서 정의한 Query 서비스를 통해 mapping-query-namedparamvo.xml 로, Named Parameter로 전달된 'vo'를 활용한 다양한 쿼리문들을 포함하고 있다. Parameter명이 'vo'인 객체로부터 . 뒤에 붙은 이름에 대한 getter 메소드를 호출한 결과를 PreparedStatement에 셋팅한 후, 해당 쿼리문을 실행하게 된다.

<queryservice>
    <queries>
        <query id="insertCategory">
            <statement>
                INSERT INTO TBL_CATEGORY 
                    (CATEGORY_NO, CATEGORY_NAME, CATEGORY_DESC, USE_YN, REG_ID, 
                    REG_DATE, MODIFY_ID, MODIFY_DATE) 
                VALUES (:vo.categoryNo, :vo.categoryName, :vo.categoryDesc, 
                    :vo.useYn, :vo.regId, sysdate, :vo.regId, sysdate)
            </statement>	
        </query>	
        <query id="updateCategory">
            <statement>
                UPDATE TBL_CATEGORY  
                SET CATEGORY_NAME = <emphasis role="bold">:vo.categoryName,</emphasis> 
                    CATEGORY_DESC = :vo.categoryDesc, 
                    USE_YN =:vo.useYn, MODIFY_ID = :vo.modifyId,
                    MODIFY_DATE = sysdate
                WHERE CATEGORY_NO = :vo.categoryNo
            </statement>
        </query>	
        <query id="deleteCategory">
            <statement>
                delete from TBL_CATEGORY where CATEGORY_NO = :vo.categoryNo
            </statement>	
        </query>
        <query id="findCategoryList">
            <statement>
                SELECT CATEGORY_NO, CATEGORY_NAME, CATEGORY_DESC, USE_YN, REG_ID
                FROM TBL_CATEGORY
                WHERE CATEGORY_NO like :vo.categoryNo
            </statement>
            <result class="anyframe.example.domain.Category"></result>
        </query>
        <query id="findCategory">
            <statement>
                SELECT CATEGORY_NO, CATEGORY_NAME, CATEGORY_DESC, USE_YN, REG_ID
                FROM TBL_CATEGORY
                WHERE CATEGORY_NO = :vo.categoryNo
            </statement>
            <result class="anyframe.example.domain.Category"></result>
        </query>					
    </queries>
</queryservice>		

57.3.9.3.테스트 코드 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 INSERT, SELECT, UPDATE, DELETE 쿼리문을 실행하는 테스트 코드의 일부이다. Query 서비스에 입력 Parameter 전달시 'vo'라는 Parameter명에 Category 또는 SearchVO 객체를 매핑시키고 있음을 알 수 있다.

/**
 * QueryService에 특정 쿼리의 입력 Parameter값을 전달할 때, named parameter 형태로 transfer
 * object를 전달한다. QueryService에서는 전달받은 객체의 getter 메소드를 호출하여 INSERT 쿼리문의
 * Parameter값을 셋팅하고 실행한다.
 */
public Category testInsertCategory() throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("queryService");

    Category category = makeCategory();
    
    Object[] args = new Object[] { "vo", category };
    int result = queryService.create("insertCategory",
        new Object[] { args });

    if (result != 1) {
        throw new Exception("Insert a new category failed");
    }

    return category;
}

/**
 * QueryService에 특정 쿼리의 입력 Parameter값을 전달할 때, named parameter 형태로 transfer
 * object를 전달한다. QueryService에서는 전달받은 객체의 getter 메소드를 호출하여 UPDATE 쿼리문의
 * Parameter값을 셋팅하고 실행한다.
 */
public void updateCategory(Category category) throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("queryService");
    category.setCategoryName("testUpdate");

    Object[] args = new Object[] { "vo", category };
    int result = queryService.update("updateCategory",
        new Object[] { args });

    if (result != 1) {
        throw new Exception("Update category failed");
    }
}

/**
 * QueryService에 특정 쿼리의 입력 Parameter값을 전달할 때, named parameter 형태로 transfer
 * object를 전달한다. QueryService에서는 전달받은 객체의 getter 메소드를 호출하여 DELETE 쿼리문의
 * Parameter값을 셋팅하고 실행한다.
 */
public void deleteCategory(Category category) throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("queryService");

    Object[] args = new Object[] { "vo", category };
    int result = queryService.remove("deleteCategory",
        new Object[] { args });

    if (result != 1) {
        throw new Exception("Delete category failed");
    }

    args = new Object[] { "vo", category };
    Collection rtCollection = queryService.find("findCategory",
        new Object[] { args });

    if (rtCollection.size() != 0) {
        throw new Exception("Find categorylist failed");
    }
}

/**
 * QueryService에 특정 쿼리의 입력 Parameter값을 전달할 때, named parameter 형태로 transfer
 * object를 전달한다. QueryService에서는 전달받은 객체의 getter 메소드를 호출하여 SELECT 쿼리문의
 * Parameter값을 셋팅하고 실행한다.
 */
public void findCategory(String categoryNo, String categoryName)
		throws Exception {
    IQueryService queryService = (IQueryService) context.getBean("queryService");

    Category searchVO = new Category();
    searchVO.setCategoryNo(categoryNo);

    Object[] args = new Object[] { "vo", searchVO };
    Collection rtCollection = queryService.find("findCategory",
        new Object[] { args });

    if (rtCollection.size() != 1) {
        throw new Exception("Find category failed");
    }

    Category category = (Category) rtCollection.iterator().next();
    if (!(categoryNo.equals(category.getCategoryNo()) && categoryName
        .equals(category.getCategoryName()))) {
        throw new Exception("Find category failed");
    }
}						

57.3.10.extends AbstractDAO

일반적으로 DAO(Data Access Object) 클래스에서는 Query 서비스를 활용하여 데이터 접근 로직을 처리한다. Query 서비스에서는 이러한 DAO 클래스를 간편하게 개발할 수 있도록 하기 위해 anyfram.core.query.AbstractDAO 클래스를 제공하고 있으며 다음과 같은 특징을 지닌다.

  • 입력된 문자열에 정해진 Prefix, Postfix를 조합하여 query id를 만든다.

    INSERT문의 경우 'createXXX', UPDATE문인 경우 'updateXXX', DELETE문인 경우 'removeXXX', 단건 조회 SELECT문인 경우 'findXXXByPk', 다건 조회 SELECT문인 경우 'findXXXList'가 query id가 된다. 따라서 Naming Rule을 정하고 이에 맞게 query id를 정의하면, 공통되는 기본 문자열만을 사용하여 원하는 쿼리문을 실행할 수 있게 된다.

  • query id 조합을 위한 Prefix, Postfix는 수정이 가능하다.

    createId(Default='create'), updateId(Default='update'), removeId(Default='remove'), findPrefix(Default='find'), findListPostfix(Default='List'), findByPkPostfix(Default='ByPk') 속성에 대한 setter 메소드를 제공하므로 수정 적용이 가능하다.

  • 입력 Parameter를 VO, Map, List, Object[] 형태에 담아 처리할 수 있다.

    입력된 Parameter는 Named Parameter로 처리되기 때문에 VO의 Map, VO의 ArrayList 등에 대한 처리도 가능해진다. 따라서 DAO 코드가 단순해질 수 있게 된다.

  • 단건 데이터 조회 결과 처리를 위해 필요한 공통 로직을 수행한다.

    Query 서비스의 find(), findBySQL() 메소드를 호출하면 SELECT문 실행 결과를 Collection 객체에 담아 전달한다. 따라서 조회 결과가 단건인 경우 Collection으로부터 단건 데이터를 꺼내서 전달하는 로직이 추가되어야 한다.

    if (collection == null || collection.size() == 0)
        return null;
    return collection.iterator().next();

    AbstractDAO에서 제공하는 findByPk(...) 메소드를 호출하면 위에서 언급한 추가 로직을 중복 구현하지 않고 단건 데이터를 얻을 수 있게 된다.

  • UI 개발 편의를 위해서 다건 데이터 조회 결과를 별도의 Page 객체에 저장하는 로직을 수행한다.

    Query 서비스의 findWithRowCount() 메소드를 호출하면, SELECT문 실행 결과를 Map 객체에 담아 전달하며, 개발자는 UI 개발 편의를 위해 Map 객체로부터 필요한 데이터를 추출하여 별도의 Page 객체에 담기 위한 로직을 추가해야 한다.

    List resultList = (List)queryMap.get(IQueryService.LIST);
    int totalSize = ((Long) queryMap.get(IQueryService.COUNT)).intValue();
    return new Page(resultList, (new Integer(pageIndex)).intValue(),
        totalSize, pageUnit, pageSize);

    AbstractDAO에서 제공하는 findListWithPaging() 메소드를 호출하면 위에서 언급한 추가 로직을 중복 구현하지 않고도 anyframe.common.Page 형태의 객체를 얻을 수 있게 된다.

57.3.10.1.매핑 XML 파일 Sample

다음은 Query 서비스를 통해 로드된 mapping-query-abstractdao.xml 파일의 일부로, Named Parameter를 포함한 다양한 쿼리문들을 포함하고 있다.

<queryservice>
    <queries>
        <query id="createProduct">
            <statement>
                INSERT INTO PRODUCT 
                    ( PROD_NO, PROD_NAME, SELLER_ID, CATEGORY_NO, PROD_DETAIL, 
                    MANUFACTURE_DAY, AS_YN, SELL_QUANTITY, SELL_AMOUNT, IMAGE_FILE, 
                    REG_DATE ) 
                VALUES (:vo.prodNo, :vo.prodName, :vo.sellerId, :vo.category.categoryNo, 
                    :vo.prodDetail, :vo.manufactureDay, :vo.asYn, :vo.sellQuantity, 
                    :vo.sellAmount, :vo.imageFile, sysdate)
            </statement>
        </query>

        <query id="updateProduct">
            <statement>
                UPDATE PRODUCT 
                SET PROD_NAME = :vo.ProdName,
                    PROD_DETAIL = :vo.prodDetail,
                    MANUFACTURE_DAY = :vo.manufactureDay,
                    AS_YN = :vo.asYn,
                    SELL_QUANTITY = :vo.sellQuantity,
                    SELL_AMOUNT = :vo.sellAmount,
                    REG_DATE = sysdate
                WHERE PROD_NO = :vo.ProdNo
            </statement>
        </query>

        <query id="findProductByPk">
            <statement>
                SELECT	PROD_NO, PROD_NAME, SELLER_ID, CATEGORY_NO, PROD_DETAIL, 
                    MANUFACTURE_DAY, AS_YN, SELL_QUANTITY, SELL_AMOUNT, IMAGE_FILE, 
                    REG_DATE 
                FROM PRODUCT
                WHERE PROD_NO = :vo.ProdNo
            </statement>
            <result class="anyframe.example.domain.Product">
                <result-mapping column="{CATEGORY_NO}" attribute="{category.categoryNo}" />
            </result>
        </query>

        <query id="removeProduct">
            <statement>
                DELETE FROM PRODUCT 
                WHERE PROD_NO = :vo.prodNo
            </statement>
        </query>

        <query id="findProductList">
            <statement>
                SELECT product.PROD_NO, product.PROD_NAME, product.SELLER_ID, 
                    product.CATEGORY_NO, product.PROD_DETAIL, product.MANUFACTURE_DAY, 
                    product.AS_YN, product.SELL_QUANTITY, product.SELL_AMOUNT, 
                    product.IMAGE_FILE, product.REG_DATE 
                FROM PRODUCT product 
                WHERE 
                    product.PROD_NAME like :vo.prodName
                    AND product.AS_YN = :vo.asYn
                ORDER BY product.PROD_NO DESC
            </statement>
            <result class="anyframe.example.domain.Product">
                <result-mapping column="{CATEGORY_NO}" attribute="{category.categoryNo}" />
            </result>
        </query>
    </queries>
</queryservice>

57.3.10.2.DAO 클래스 코드 Sample

다음은 AbstractDAO를 상속받아 구현한 ProductDaoImpl.java 클래스의 일부이다. AbstractDAO에서 제공하는 메소드를 호출함으로써 Product 정보를 INSERT, UPDATE, DELETE, SELECT하는 역할을 수행하는ProductDaoImpl 로직이 보다 간단해졌음을 알 수 있을 것이다. AbstractDAO 클래스를 상속받은 경우 Query 서비스의 API를 직접 호출하려면 getQueryService() 메소드 호출을 통해 Query 서비스를 얻어내어 활용하면 된다.

public class ProductDaoImpl extends AbstractDAO {
	// query id가 'createProduct'인 INSERT문을 실행
	public int createProduct(Product vo) throws Exception {
		return create("Product", vo);
	}
	
	// query id가 'deleteProduct'인 DELETE문을 실행
	public int deleteProduct(Product vo) throws Exception {
		return remove("Product", vo);
	}
	
	// query id가 'findProductByPk'인 SELECT문을 실행하고 
	// 조회 결과인 Product 객체를 전달 
	public Product selectProduct(String prodNo) throws Exception {
		Product vo = new Product();
		vo.setProdNo(prodNo);
		Product resultVo = (Product) findByPk("Product", vo);
		return resultVo;
	}
	
	// query id가 'findProductList'인 SELECT문을 실행하고 
	// 조회 결과인 Page 객체를 전달
	public Page selectProductList(Product vo, int pageIndex, int pageSize,
			int pageUnit) throws Exception {
		return findListWithPaging("Product", vo, pageIndex,
				pageSize, pageUnit);
	}
	
	// query id가 'updateProduct'인 UPDATE문을 실행
	public int updateProduct(Product vo) throws Exception {
		return update("Product", vo);
	}
}					

57.3.10.3.DAO 클래스 속성 정의 파일 Sample

다음은 AbstractDAO 클래스를 상속받은 ProductDaoImpl 클래스의 속성을 정의한 context-service.xml 파일의 일부이다. AbstractDAO 클래스에서 내부적으로 Query 서비스를 사용하므로 Query 서비스의 Bean Id를 속성으로 셋팅해주도록 한다.

<bean id="productDAO" class="anyframe.example.query.sales.dao.impl.ProductDaoImpl">
	<property name="queryService" ref="queryService" />
	<property name="propertiesService" ref="propertiesService" />
</bean>				

57.3.10.4.DAO 클래스 테스트 코드 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 INSERT, SELECT, UPDATE, DELETE 쿼리문을 실행하는 테스트 코드의 일부이다.

/**
 * AbstractDAO를 통해 DB에 신규 데이터를 입력하는 테스트 코드
 */
public void insertProduct() throws Exception {
    Category category = categoryService.get("CATEGORY-00004");
    Product product = new Product();
    product.setProdName("example.sportsone");
    product.setCategory(category);
    product.setProdDetail("sports one detail");
    product.setSellerId("woos41");
    product.setAsYn("Y");
    product.setManufactureDay("20081225");
    product.setSellAmount(new Long(50));
    product.setSellQuantity(new Long(50));
    
	// 매핑 XML 파일에 정의되어 있는 query id를 이용하여 INSERT를 실행한다.
	productDao.create(product);
}

/**
 * AbstractDAO를 통해 DB에 입력된 데이터를 조회하는 테스트 코드
 */
public void selectProduct(String prodNo) throws Exception {
	// 매핑 XML 파일에 정의되어 있는 query id를 이용하여 SELECT를 실행한다.
	Product vo = productDao.get(prodNo);
}

/**
 * AbstractDAO를 통해 DB에 입력된 데이터를 페이징 처리하기 위한 테스트 코드
 */
public void selectProductList() throws Exception {
    ProductSearchVO searchVO = new ProductSearchVO();
    searchVO.setSearchCondition("0");
    searchVO.setSearchKeyword("example.sportsone");
    searchVO.setPageIndex(1);
    searchVO.setPageSize(2);
    
    // 매핑 XML 파일에 정의되어 있는 query id를 이용하여 SELECT를 실행하고,
    // 페이징 처리된 실행 결과를 얻는다.
    Page products = productDao.getPagingList(searchVO);
}

/**
 * AbstractDAO를 통해 DB에 입력된 데이터를 수정하는 테스트 코드
 */
public void updateProduct(Product product) throws Exception {
	// 매핑 XML 파일에 정의되어 있는 query id를 이용하여 UPDATE를 실행한다.
	product.setProdName("sportsone-update");
    product.setProdDetail("sports one detail-update");
    
	productDao.update(product);
}

/**
 * AbstractDAO를 통해 DB에 입력된 데이터를 삭제하는 테스트 코드
 */
public void deleteProduct(String prodNo) throws Exception {
	// 매핑 XML 파일에 정의되어 있는 query id를 이용하여 DELETE를 실행한다.
	productDao.remove(prodNo);
}					

57.3.11.implements IResultSetMapper

일반적으로 Query 서비스는 매핑 XML 파일에 정의되어 있는 쿼리문을 실행한 후, 그 실행 결과를 <result> 내에 정의된 객체나 HashMap에 담아 전달한다. 그런데 실행 결과를 <result> 내에 정의된 객체에 담기 위해서는 Java Reflection API 호출이 발생하므로 대량의 데이터를 처리할 때 성능 저하가 예상된다. 또한 실행 결과를 HashMap에 담는 경우에는 Java Reflection API 호출없이, 칼럼명을 키값으로 하여 칼럼값을 셋팅하나 특정 메소드 호출을 통해 주고 받는 데이터가 명확히 식별되지 않게 되어 어플리케이션 분석 및 변경시 불리할 수 있다. QueryService에서는 이러한 점을 보완하기 위해서 ResultSetMapper 실행을 지원한다. 즉, 쿼리문 실행 결과 처리를 Query 서비스에 위임하지 않고, 별도 구현된 ResultSetMapper를 통해 직접 처리할 수 있도록 지원한다.

57.3.11.1.속성 정의 파일 Sample

다음은 Query 서비스를 정의한 context-query.xml 과 Query 서비스에서 읽어들일 매핑 XML 파일의 위치를 정의한 context-query-sqlloader.xml 파일의 일부이다.

<bean id="queryService" class="anyframe.core.query.impl.QueryServiceImpl">
    <property name="jdbcTemplate" ref="jdbcTemplate"/>
    <property name="sqlRepository" ref="sqlLoader"/>
        중략...
</bean>
<bean id="jdbcTemplate" class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <property name="dataSource" ref="dataSource" />
</bean>			

<bean name="sqlLoader" class="anyframe.core.query.impl.config.loader.SQLLoader">
    <config:configuration>
        <filename>classpath:/query/mapping-query-resultsetmapper.xml</filename>
                중략...
    </config:configuration>		
</bean>			

57.3.11.2.매핑 XML 파일 Sample

다음은 앞서 정의한 Query 서비스를 통해 로드된 mapping-query-resultsetmapper.xml의 일부로, 쿼리문을 포함하고 있다. Query Id가 findCustomerWithResultSetMapper인 쿼리문 실행 결과는 사용자가 정의한 anyframe.example.query.CustomerMapper를 통해 처리되도록 정의하고 있음을 알 수 있다.

<queries>  		
    <query id="findCustomerWithResultSetMapper" isDynamic="false">
        <statement>select NAME, ADDRESS from TB_CUSTOMER where SSNO like ?</statement>
        <param type="VARCHAR"/>
        <result mapper="anyframe.example.query.CustomerMapper"/>
	</query>
</queries>		

<result>의 mapper는 신규 추가된 속성으로 http://www.anyframejava.org/dtd/anyframe-core-query-mapping-3.2.dtd에 정의되어 있다. 따라서, mapper를 정의하기 위해서 매핑 XML 파일 헤더에 DTD 위치를 다음과 같이 정의해 주어야 함에 유의하도록 한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE queryservice PUBLIC "-//ANYFRAME//DTD QUERYSERVICE//EN"
"http://www.anyframejava.org/dtd/anyframe-core-query-mapping-3.2.dtd">
<queryservice>
중략...
</queryservice>	

57.3.11.3.ResultSetMapper 코드 Sample

사용자는 다음과 같은 순서로 ResultSetMapper를 구현하면 된다.

  1. Query 서비스에서 제공하는 인터페이스 anyframe.core.query.IResultSetMapper를 implements한 클래스 정의

  2. ResultSet을 입력 인자로 하는 콜백 메소드 mapRow() 정의

  3. maprRow() 메소드 내에서 ResultSet을 이용하여 원하는 객체에 실행 결과 셋팅하고 실행 결과를 return

다음은 앞서 언급한 매핑 XML 파일에 정의된 CustomerMapper.java의 일부이다.

public class CustomerMapper implements IResultSetMapper {

	public Object mapRow(ResultSet resultSet) throws SQLException {
		Customer customer = new Customer();
		customer.setNm(resultSet.getString("name"));
		customer.setAddr(resultSet.getString("address"));
		return customer;
	}
}					

57.3.11.4.테스트 코드 Sample

다음은 앞서 언급한 매핑 XML 파일에 정의된 SELECT 쿼리문을 실행하는 테스트 코드의 일부이다.

/**
 * QueryService의 find() 메소드를 호출하여 매핑 XML에 정의된 쿼리문을 실행시키고, 매핑 XML에 정의된
 * IResultSestMapper 유형의 Mapper를 이용하여, 결과값이 매핑되는지 체크하기 위한 테스트 코드
 */
public void findWithCustomResultSetMapper() throws Exception {
	IQueryService queryService = (IQueryService) context
			.getBean("queryService");

	// execute query
	Collection rtList = queryService.find(
			"findCustomerWithResultSetMapper", new Object[] { "%123456%" });
	// assert a size of result
	if (rtList.size() != 3)
		throw new Exception("Fail to select with custom ResultSetMapper.");

	// assert in detail
	Iterator resultItr = rtList.iterator();
	while (resultItr.hasNext()) {
		Customer customer = (Customer) resultItr.next();
		if (!customer.getAddr().equals("Seoul"))
			throw new Exception("Fail to compare result in defail.");
	}
}					

57.4.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.query.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후, mvn compile exec:java -Dexec.mainClass=anyframe.example.query.Main이라는 명령어를 실행시켜 결과를 확인한다.

    • Eclipse 기반 실행

      Eclipse에서 압축 해제 프로젝트를 import한 후, src/main/java 폴더의 anyframe/example/query 하위의 Main.java를 선택하고 마우스 오른쪽 버튼 클릭하여 컨텍스트 메뉴에서 Run As > Java Application을 클릭한다. 그리고 실행 결과를 확인한다.

    표 57.2. Download List

    NameDownload
    hsqldb.zip Download
    anyframe.example.query.zip Download

57.5.Extensions

프리젠테이션 레이어 개발 시 X-Internet 제품인 MiPlatform 또는 Gauce 등을 기반으로 할 경우 각 제품은 사용자 입력 사항을 제품 고유의 데이터 형태에 저장하여 전달한다. 따라서 Query 서비스를 이용하여 DB 데이터를 처리하기 위해서는 "제품 고유의 데이터 전달 형태 <-> Map 또는 VO 간의 변환"을 위한 추가 작업이 필요하며, 이로 인해 대량의 데이터를 다루는 경우 성능 저하가 발생할 가능성이 크다. Query 서비스에서는 기본 QueryService를 확장하여, 특정 X-Internet 제품에 최적화된 형태의 구현체를 제공함으로써 개발 편의성과 응답 속도 향상을 도모하고자 한다. 다음에서는 MiPlatform에 최적화된 MiPQueryService 사용 방법에 대해서 살펴보도록 한다.

57.5.1.MiPQueryService 활용

MiPQueryService는 MiPlatform 고유의 데이터 전달 형태로부터 사용자가 입력한 데이터를 추출하여 해당 DB에 반영하는 역할을 수행한다. 그러므로 투비소프트사의 X-Internet 제품인 MiPlatform 기반으로 프리젠테이션 레이어를 개발하는 경우, MiPQueryService를 통해 MiPlatform 고유의 데이터 전달 형태인 com.tobesoft.platform.data.Dataset, com.tobesoft.platform.data.VariableList를 그대로 이용할 수 있게 된다.

Pagination시 유의 사항

RiaQueryService 4.1.0 이후부터 PagingJdbcTemplate 속성 정의시에는 반드시 DBMS에 적합한 PagingSQLGenerator를 셋팅해 주어야 한다. 적절한 PagingSQLGenerator가 존재하지 않는 경우에는 QueryService에서 제공하는 anyframe.core.query.impl.jdbc.generator.DefaultPagingSQLGenerator를 사용할 수 있으나, DefaultPagingSQLGenerator를 이용하여 findXXX() 메소드를 실행하면 QueryService 내부적으로 조건에 해당하는 모든 데이터를 fetch한 이후 ResultSet Cursor의 위치를 이동시키는 방식으로 특정 페이지에 속한 데이터를 걸러낸다. 이 때 ResultSet Cursor를 움직이는 로직에서 상당한 시간이 소요되어 다량의 데이터 조회시 성능에 심각한 영향을 끼칠 수 있다. 따라서, DefaultPagingSQLGenerator 사용은 권장하지 않는다.

57.5.1.1.MiPQueryService 속성 정의 파일 Sample

다음은 MiPQueryService를 정의한 context-miplatform-query.xml 파일의 일부이다. MiPQueryService는 내부적으로 RiaQueryService를 통해 데이터 접근 처리를 수행하므로 RiaQueryService에 대한 참조 관계 설정이 필요하다.

<bean id="mipQueryService" 
    class="anyframe.core.query.ria.mip.impl.MiPQueryServiceImpl">
    <property name="riaQueryService" ref="riaQueryService"/>
</bean> 

<bean name="riaQueryService" class="anyframe.core.query.ria.impl.RiaQueryServiceImpl">
    <property name="jdbcTemplate" ref="pagingNamedParamJdbcTemplate"/>
    <property name="sqlRepository" ref="mipSqlLoader"/>
        중략...
</bean>

<bean id="pagingNamedParamJdbcTemplate" 
    class="anyframe.core.query.impl.jdbc.PagingNamedParamJdbcTemplate">
    <constructor-arg index="0" ref="pagingJdbcTemplate"/>
    <constructor-arg index="1" ref="dataSource"/>
</bean>

<bean id="pagingJdbcTemplate"
    class="anyframe.core.query.impl.jdbc.PagingJdbcTemplate">
    <constructor-arg ref="dataSource"/>
    <property name="exceptionTranslator" ref="exceptionTranslator"/>
    <property name="paginationSQLGetter" ref="pagingSQLGenerator"/>	
</bean>			

57.5.1.2.매핑 XML 파일 샘플

다음은 앞서 정의한 MipQueryService가 참조하는 RiaQueryService Bean을 통해 로드된 mapping-query-mip.xml 파일의 일부로, Named Parameter를 이용한 쿼리문들을 포함하고 있다.

<queries>
    <query id="createProduct" isDynamic="true" mappingStyle="upper">
        <statement>
            INSERT INTO PRODUCT ( 			
                PROD_NO, AS_YN, CATEGORY_NO, IMAGE_FILE, MANUFACTURE_DAY, 
                PROD_DETAIL, PROD_NAME, REG_DATE, SELL_AMOUNT, SELL_QUANTITY, SELLER_ID) 
            VALUES (:PROD_NO, :AS_YN, :CATEGORY_NO, :IMAGE_FILE, :MANUFACTURE_DAY, 
                :PROD_DETAIL, :PROD_NAME, sysdate, :SELL_AMOUNT, :SELL_QUANTITY, :SELLER_ID)
        </statement>
    </query>
		
    <query id="updateProduct" isDynamic="true" mappingStyle="upper">
        <statement>
            UPDATE PRODUCT
            SET 
                PROD_NAME = :PROD_NAME, PROD_DETAIL = :PROD_DETAIL,
                MANUFACTURE_DAY = :MANUFACTURE_DAY, AS_YN = :AS_YN, 
                IMAGE_FILE = :IMAGE_FILE, SELL_QUANTITY = :SELL_QUANTITY,   
                SELL_AMOUNT = :SELL_AMOUNT, CATEGORY_NO = :CATEGORY_NO
            WHERE
                PROD_NO = :PROD_NO 
        </statement>
    </query>
	
    <query id="removeProduct" isDynamic="true" mappingStyle="upper"> 
        <statement>
            DELETE FROM PRODUCT 
            WHERE 
                PROD_NO = :PROD_NO
        </statement>
    </query>
	
    <query id="findProductByPk" isDynamic="true" mappingStyle="upper">
        <statement>
            SELECT 
                PROD_NO, AS_YN, CATEGORY_NO, IMAGE_FILE, MANUFACTURE_DAY, 
                PROD_DETAIL, PROD_NAME, REG_DATE, SELL_AMOUNT, SELL_QUANTITY, 
                SELLER_ID
            FROM PRODUCT
            WHERE 
                PROD_NO = :PROD_NO
        </statement>							
    </query>

    <query id="findProductList" isDynamic="true" mappingStyle="upper">
        <statement>
            SELECT 
                PROD_NO, AS_YN, CATEGORY_NO, IMAGE_FILE, MANUFACTURE_DAY, 
                PROD_DETAIL, PROD_NAME, REG_DATE, SELL_AMOUNT, SELL_QUANTITY, 
                SELLER_ID 
            FROM PRODUCT
            WHERE 		
                PROD_NAME like '%' || :SEARCH_PROD_NAME || '%'
                AND	AS_YN = :SEARCH_AS_YN 
            ORDER BY PROD_NO
        </statement>		
    </query>
</quereis>		

57.5.1.3.테스트 코드 Sample

다음에서는 MiPQueryService를 이용하여 앞서 언급한 매핑 XML 파일에 정의된 INSERT, SELECT, UPDATE, DELETE 쿼리문을 실행하는 테스트 코드의 일부이다.

/**
 * MiPlatform용 Query 서비스를 통해 DB에 신규 데이터를 입력하는 테스트 코드
 */    
public void insertDataSet() throws Exception {
    IMiPQueryService mipQueryService = (IMiPQueryService) context.getBean("mipQueryService");

    Map queryMap = new HashMap();
    queryMap.put(IMiPQueryService.QUERY_INSERT, "createProduct");

    // INSERT, UPDATE, DELETE 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할 
    // Dataset을 전달. Dataset에는 다수의 데이터를 저장할 수 있음.
    int resultInsert =
        mipQueryService.update(queryMap, makeProduct());
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 입력된 데이터를 수정하는 테스트 코드
 */   
public void updateDataSet() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

    Map queryMap = new HashMap();
    queryMap.put(IMiPQueryService.QUERY_UPDATE, "updateProduct");
    
    // INSERT, UPDATE, DELETE 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할 
    // Dataset을 전달. Dataset에는 다수의 데이터를 저장할 수 있음.    
    int resultUpdate =
        mipQueryService.update(queryMap, makeProduct());
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 데이터를 입력/수정/삭제하는 테스트 코드
 */    
public void processAllDataSet() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

    Map queryMap = new HashMap();
    queryMap.put(IMiPQueryService.QUERY_UPDATE, "updateProduct");
    queryMap.put(IMiPQueryService.QUERY_INSERT, "createProduct");
    queryMap.put(IMiPQueryService.QUERY_DELETE, "removeProduct");
    
    // INSERT, UPDATE, DELETE 유형별로 사용할 query id를 정의한 Map과 DB에 반영해야 할
    // Dataset을 전달. Dataset에는 다수의 데이터를 저장할 수 있음.            
    int resultUpdate = mipQueryService.update(queryMap, makeProducts());
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 데이터를 입력/수정/삭제하는 테스트 코드
 * 이때, ActionCommand를 통해 전후처리 수행
 */    
public void processAllDataSetWithActionCommand() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

    Map queryMap = new HashMap();
    queryMap.put(IMiPQueryService.QUERY_UPDATE, "updateProduct");
    queryMap.put(IMiPQueryService.QUERY_INSERT, "createProduct");
    queryMap.put(IMiPQueryService.QUERY_DELETE, "removeProduct");
    
    // INSERT, UPDATE, DELETE 유형별로 사용할 query id를 정의한 Map, DB에 반영해야 할 
    // Dataset, DB에 접근하기 전과 후에 처리해야 할 작업을 정의한 ActionCommand를 전달
    // Dataset에는 다수의 데이터를 저장할 수 있음.     
    int resultUpdate =
         mipQueryService.update(queryMap, makeProducts(),
        new ProductActionCommand());
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 데이터를 조회하는 테스트 코드
 * 이때, 검색조건은 VariableList 형태임
 */     
public void findDataSetWithVariant() throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");

    // 실행할 query id와 VariableList 형태의 검색 조건을 전달
    Dataset resultDataSet =
        mipQueryService.search("findProductByPk", makeVariantList());
}

/**
 * MiPlatform용 Query 서비스를 통해 DB에 데이터를 조회하는 테스트 코드
 * 이때, 검색조건은 Dataset 형태임
 */    
public void findListDataSet(int expected) throws Exception {
    IMiPQueryService mipQueryService =
        (IMiPQueryService) context.getBean("mipQueryService");
    
    // 실행할 query id와 Dataset 형태의 검색 조건을 전달    
    Dataset resultDataSet =
        mipQueryService.search("findProductList",
        makeSearchCondition());
}					

위 소스 코드 중 testProcessAllDataSetWithActionCommand() 메소드에서는 ActionCommand를 이용하여 DB에 데이터를 입력하기 전에 특정 칼럼의 값을 변경하고 있다. 이와 같이 MiPQueryService는 anyframe.core.query.ria.mip.IMiPActionCommand를 구현한 별도 ActionCommand를 인자로 함께 전달하는 경우 입력 데이터를 DB에 반영하기 전/후에 대한 공통 처리를 수행할 수 있도록 지원한다. 예를 들어 입력받은 개별 Row를 DB에 신규 등록하기 전에 신규 식별자 값이 셋팅되어야 한다면, Loop을 돌면서 각 Row를 추출한 뒤 식별자를 셋팅하는 별도 로직없이 preInsert() 로직 내에 식별자 생성 구문이 추가된 ActionCommand 객체만 전달하면 되는 것이다.

57.5.2.RiaQueryService

앞서 언급한 바와 같이 MiPQueryService는 DB에 사용자 입력 사항을 반영하기 위해서는 RiaQueryService를 이용해야 하며, RiaQuerySerivce, MiPQueryService 각각의 역할은 다음과 같다.

  1. MiPQueryService는 입력 데이터를 ParameterSource에 담고, 조회 결과를 처리할 개별 RowCallbackHandler와 함께 실제 데이터 접근 로직을 수행하는 RiaQueryService 에게 전달

  2. RiaQueryService는 MiPQueryService 내에서 구현한 ParameterSource의 getValue() 메소드를 호출하며 입력값을 셋팅하고 해당 쿼리문 수행

  3. RiaQueryService는 MiPQueryService 내에서 구현한 RowCallbackHandler의 콜백 메소드를 호출함으로써 MiPlatform에서 사용할 수 있는 고유의 데이터 형태로 조회 결과값을 변환

따라서 RiaQueryService는 개발자가 직접 API를 호출하여 사용하는 서비스라기 보다는 MiPQueryService 내부적으로 참조하는 서비스이다.

57.5.2.1.속성 정의 파일 Sample

다음은 RiaQueryService를 정의한 context-query.xml 파일의 일부이다. RiaQueryService 기본 Query 서비스를 확장하고 있으므로 Query 서비스 속성 정의 방법과 동일하게 속성을 정의해주면 된다.

 <bean id="pagingNamedParamJdbcTemplate" 
    class="anyframe.core.query.impl.jdbc.PagingNamedParamJdbcTemplate">
    <constructor-arg index="0" ref="pagingJdbcTemplate"/>
    <constructor-arg index="1" ref="dataSource"/>
</bean>
   
<bean name="riaQueryService" class="anyframe.core.query.ria.impl.RiaQueryServiceImpl">
    <property name="jdbcTemplate" ref="pagingNamedParamJdbcTemplate"/>
    <property name="sqlRepository" ref="sqlLoader"/>
</bean>			

57.6.Resources

  • 다운로드

    다음에서 테스트 DB를 포함하고 있는 hsqldb.zip과 example 코드를 포함하고 있는 anyframe.example.miplatform.zip 파일을 다운받은 후, 압축을 해제한다. 그리고 hsqldb 폴더 내의 start.cmd (or start.sh) 파일을 실행시켜 테스트 DB를 시작시켜 놓는다.

    • Maven 기반 실행

      Command 창에서 압축 해제 폴더로 이동한 후 mvn jetty:run이라는 명령어를 실행시킨다. Jetty Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.miplatform을 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - m2eclipse, WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭하고 컨텍스트 메뉴에서 Maven > Enable Dependency Management를 선택하여 컴파일 에러를 해결한다. 그리고 해당 프로젝트에 대해 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server (Tomcat 기반)를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.miplatform을 입력하여 실행 결과를 확인한다.

    • Eclipse 기반 실행 - WTP 활용

      Eclipse에서 압축 해제 프로젝트를 import한 후, build.xml 파일을 실행하여 참조 라이브러리를 src/main/webapp 폴더의 WEB-INF/lib내로 복사시킨다. 해당 프로젝트를 선택하고 마우스 오른쪽 버튼을 클릭한 후, 컨텍스트 메뉴에서 Run As > Run on Server를 클릭한다. Tomcat Server가 정상적으로 시작되었으면 브라우저를 열고 주소창에 http://localhost:8080/anyframe.example.miplatform을 입력하여 실행 결과를 확인한다. (* build.xml 파일 실행을 위해서는 ${ANT_HOME}/lib 내에 maven-ant-task-2.0.10.jar 파일이 있어야 한다.)

    표 57.3. Download List

    NameDownload
    hsqldb.zipDownload
    anyframe.example.miplatform.zipDownload
    maven-ant-tasks-2.0.10.jarDownload