Framework/Database2010. 4. 4. 01:01
Database Framework Scripting Layers is

a lightweight database abstraction layer suitable for high-load websites where you need the scalable advantages of connection pooling. DBFSlayer talks to clients via JSON over HTTP, meaning it's simple to monitor and can swiftly interoperate with any web framework you choose.

Features At A Glance
  • Simple HTTP interface
  • JSON-format messages
  • Multiple DB adapter (currently tested oracle 9 higher, mssql 2000 higher, mysql 5 higher)
  • Connection pooling
  • Multithreaded
  • Straight-forward configuration
  • Simple yet powerful access

Download
https://sourceforge.net/projects/dbfslayers/files/
dbfs_layers_fat.jar include following lib
     - jetty 6.1
     - servlet 2.5, jsp 2.5
     - struts 1.2.9
     - json-lib 2.3
     - bleujin framework_core_fat.jar(http://sourceforge.net/projects/clientcursordbf/)
     - Database JDBC client libraries(offical lib of oracle 9i ,mssql 2000 , mysql 5)


Install
   1. download dbfs_layer_web.zip(https://sourceforge.net/projects/dbfslayers/files/)
   2. extract dbfs_layer_web.zip
   5. download dbfs_layers_fat.jar at 2.dir (https://sourceforge.net/projects/dbfslayers/files/)
   6. configure webapps/simple/WEB-INF/default-config.xml
   6. execute java -jar dbfs_layer_fat.jar
   7. connect http://localhost:8080/simple/index.htm


Configuring Your Database
view webapps/simple/WEB-INF/default-config.xml


Example Usage

run : java -jar dbfs_layers_fat.jar
view : http://localhost:8080/simple/index.htm

<script  language="JavaScript">
    var dc = new Database(new Session('http://localhost:8080/simple/db.do', 'john'));

    var ins = dc.createUserCommand("insert into update_sample values(2, '222')") ;
    $('result').innerHTML = ins.execUpdate() + '<br/>\n' ;

    var cmd = dc.createUserCommand('select * from update_sample where a < :a') ;
    cmd.setPage(3, 1) ;   // 1 page per 3 unit
    cmd.setParam('a', '3') ;

    // var result = cmd.execQuery() ;   // excute. return JSON format
   
    $('result').innerHTML += cmd.execQuery()  + '<br/>\n';

    /*  execute procedure example
    var upt = dc.createUserProcedure('sample@selectEmpBy()') ;
    upt.setPage(3, 1) ;
    $('result').innerHTML = upt.execQuery() ;
   
    */
   
    /*  multiple query(same transaction) example
    var upt1 = dc.createUserCommand('select * from copy_tblc where no1 < :no1') ;
    upt1.setPage(3, 1) ;
    upt1.setParam('no1', '05') ;
   
    var upt2 = dc.createUserProcedure('sample@selectEmpBy()') ;
    upt2.setPage(3, 1) ;
   
    var upts = dc.createUserProcedures('multi query') ;
    upts.add(upt1).add(upt2) ;
   
    $('result').innerHTML = upts.execQuery() ;
    */
</script>



UTF8 Issues

The DBFSlayers JSON requires your data to be in UTF-8 format. This means your database should be in UTF-8 and it should return UTF-8 for queries.



Future Work
  • Batch MDL - batch insert, update, delete
  • Composite Query - select + mdl execute at same transaction
  • LOB Datatype support
  • JTA support at multiple DB
  • Security - add support for HTTPS / HTTP Auth as a basic access control mechanism (security is still primarily handled in the database).
  • Round-Robin Dispatching
  • Automatic Failover

  • Documentation - improvements to the documentation as suggested by the community.
  • Testing - better unit tests are in the works
  • Feeds/Get-if-modified-since Support - some cumulative stats mechanisms could support mechanisms to only download new log messages or if there is new log messages.
  • Language Bindings - the DBSlayer just speaks JSON + HTTP, so many languages should be able to talk to it. We welcome well-written client libraries for any language if you want to share yours.







'Framework > Database' 카테고리의 다른 글

DB Framework 2.0 Short Explain  (3) 2010.03.09
Framework Cursor (cursor in MySql)  (1) 2010.02.24
Framework - client cursor  (0) 2009.03.26
Framework - 커서의 선택 .. and  (0) 2009.03.24
프로시저 vs SQL  (0) 2009.03.23
Posted by bleujin
Framework/Database2010. 3. 9. 12:46

framework_src.zip의 com.bleujin.framework.db.sample에 기본 적인 예제가 있으며 특히 com.bleujin.framework.db.sample에 가장 기초적인 예제가 있다.

해당 예제들을 실행하기 위해서는 sample_mysql.sql(mssql.sql, oracle.sql 역시 모두 같은 역할을 한다.) 의 예제 테이블 등의 내용을 실행시킨다 . 모든 테스트를 실행하기 위해서는 com.bleujin.framework.db.sample.SampleAllTest.java 를 실행한다.


주요 클래스

DBManager

     - 가장 먼저 생성해야 할 클래스로 대부분 JDBC URL과 userID, password를 생성자로 받는다.
        DB별로 혹은
            MySQLDBManger, MySQLPoolDBManager
            MSSQLDBManger, MSSQLPoolDBManager
            OracleDBManger, OracleDBManager, OracleCacheDBManger, Oracle9iCacheDBManger
            H2EmbedDBMangaer, HSQLDBManager 등등

DB별, 접속형태별(Pool여부, Cache여부)로 여러개의 구현체를 가지고 있다.

ex)
DBManager dbm = new MySQLPoolDBManager("jdbc:mysql://novision/test", "bleu", "redf") ;



IDBController


      - DBManger를 생성자로 받는 IQueryable Impl의 Factory 역할을 맡는다.
      - 현재는 IDBController의 구현체로 DBController 한개만 있다.
      - initSelf() 메소드로 dbm의 Pool 초기화 등의 작업을 실행하고 destroySelf() 메소드로 DBManager Pool을 해제시킨다. initSelf()는 생성후 꼭 실행시켜 주어야 한다. 보통의 경우 initSelf()는 프로그램 시작시에 한번, destorySelf()는 프로그램 종료시에 한번 실행한다. Static 변수로 참조하여 사용하는 경우가 일반적이지만 꼭 그럴필요는 없다.

ex)
DBController dc = new DBController(dbm) ;
dc.initSelf() ;
....
dc.destroySelf() ;

     - DBManger와 IDBController를 생성시키고 initSelf()를 실행했다면 이제 쿼리를 실행시킬 준비는 모두 완료되었다.
        가장 간단한 쿼리 실행은
        Rows rows = dc.execQuery(String select) ;
        int result = dc.execUpdate(String mdl) ; 이다.
       



IQueryable
    - 인터페이스이고 execQuery()와 execUpdate()의 주 메소드 2개를 가지고 있다. 모든 실행할수 있는 쿼리는 IQueryable를 상속받는다. 가장 많이 알려진 구현체로 UserCommand, UserCommandBatch, UserProcedure, UserProcedureBatch, UserPorcedures 등이 있다.




IUserCommand

     - 굳이 비슷한 걸 찾자면 JDBC의 PreparedStatement의 역할과 비슷하며 사용방법도 비슷하다.

ex)
IUserCommand cmd = dc.createUserCommand("select * ... where a = ? and b = ?") ;
cmd.addParam(1).addParam("abc") ;
// cmd.addParam(0, 1).addParam(1, "abc") ; // 위구문과 의미가 같다. param index는 0부터 시작한다.

// 혹은 아래와 같이 같은 변수를 여러개 사용하거나 편이성을 위해 named parameter를 사용할수 있다.
IUserCommand cmd = dc.createUserCommand("select * ... where a = :a and b = :b and c >= :a") ;
cmd.addParam("a", 1).addParam(b, "abc") ;

// 만약 특별한 형을 지정하고 싶다면. cmd.addParam("a", 1, Types.LONG) ; 와 같이 명시적으로 지정한다.


      - IUserCommand 실행
Rows rows = cmd.execQuery() ;  // select
int result = cmd.execUpdate() ;     // mdl

       - 실행계획 보기
//실행계획의 해당 DB의 기능을 이용하기 때문에 Format은 DB마다 다른데, MySQL은 표 형태로, MSSQL은 XML Graph형태로, Oracle은 문자열로 보여준다.
cmd.viewPlan(OutputSteam output) ;


       - Page 설정
// 쿼리를 실행하기 전에 Page를 설정하여 해당 Page의 결과값만을 가져올수 있다.
cmd.setPage(Page.create(10, 2)) ;     // 10개씩 했을때 2페이지 즉 11-20번째 row를 가져온다.
cmd.execQuery() ;

이전의 cursor 관련글에서도 확인하였듯이 가장 좋은 방법은 원하는 집함만을 억세스 해서 원하는 집합형태로 가져오는 쿼리를 사용하는 것이 Page 설정에 있어서의 가장 좋은 답이다. 그렇게 생각했기 때문에 예전의 버전에는 Page 관련 기능을 넣지 않았다. 다만 그것은 MySQL의 limit나 오라클의 rownum의 단순한 사용방법보다는 훨씬 더 고난이도이기 때문에 DB별로 자유자재로 할 수 있는 사람이 극히 드물고 또한 매우 번거롭다. 또한 그 효과를 볼수 있는 곳이 제한적이기 때문에 번거로움을 무시하고 모든 곳에 그와 같은 쿼리 방식을 사용하는것은 불필요하다는 아니지만 비효율적이다라는 생각이 들었다.


Rows


   - 쿼리를 실행할수 있는 Interface인 IQueryable의 하위 구현체는 IUserCommand, IUserProcedure, IUserCommmandBatch, IUserProcedureBatch, UserProceudres, TimeoutQuery ... 등등 아주 많은데 그중의 모든 Select 문의 결과값으로 ResultSet의 인터페이스를 구현한 Rows를 반환한다. ResultSet Interface를 구현하였기 때문에 기존의 메소드를 거의 모두 지원하며 몇가지 추가 유틸리티 메소드를 제공한다.

  - 기존의 일반 JDBC에서 반환하는 ResultSet의 구현체와는 다르게 Rows는 Value Object 기반의 클라이언트 커서를 사용한다. 따라서 쿼리의 실행후 pstmt.close(), rs.close(), freeConnection()의 과정은 전부 내부에서 이루어지고 호출자에게 노출되지 않는다. 일단 IDBControler가 초기화된후 dc.getRows() 혹은 cmd.execQuery() 실행후 사용자는 Resource의 반환등에 전혀 신경쓸 필요 없으며 내부적으로 상황에 맞는 결과 캐쉬를 통해 실행속도를 보장한다. rows는 인터페이스 규약상 close()가 있지만 close()를 하지않아도 단순 ValueObject이므로 가비지 컬렉터에 의해 자동으로 정리가 된다.

   - Rows는 거의 순수하게 Value 객체이기 때문에 Framework에서 자동으로 반환한 Resource와 상관없이 Clob 억세스와 쿼리 재실행 등이 가능하며 Serialized XML 형태로 언제든 변환이 가능하다.


rows.getNextPage() ; // 다음 페이지의 rows를 가져온다.
rows.getPrePage() ;; // 이전 페이지의 rows를 가져온다.

   - Clob Access
     JDBC에는 Clob과 Blob의 표준 datetype이 있지만 DB마다 지원여부와 사용방법이 조금씩 다르다. 또한 그 사용방법이 대부분 번거롭게 때문에 varchar의 글자수 제한으로 어쩔수 없이 Clob을 사용해야 하는 경우 매우 불편하다. 그래서 Datatype이 Clob이더라도 DB에 따라 자동 형변환을 통해 rows.getString()을 통해 Access한다. (Blob은 getBinaryStream()) 만약에 Clob에 들어가는 데이타가 100M가 넘는다면 이 방법을 사용하는 것을 신중히 생각해야 할지도 모른다. 그러나 텍스트 1M를 넣는 경우도 매우 흔치 않을뿐더러 그경우에도 Blob으로 다루는 방법을 선택하는게 좋다.

    # insert 혹은 update시 parameter는 DB 벤더에 상관없이 setClob(String str), setBlob(InputStream input) 메소드를 사용하면 된다.




IUserCommandBatch

   - 특정 테이블에 10건의 insert를 해야 한다면 10개의 UserCommand를 실행시키는 것보다 10개의 Arrary를 parameter에 set한후 실행시키는 Batch를 사용하는게 좋다. 평균적으로 최소한 Batch는 건당 insert 속도가 최소 1ms 이하를 보장하며 이는 UserCommand의 기준 속도인 50ms보다 약 50배가 더 빠르다.

ex)
IUserCommandBatch cmd = dc.createUserCommandBatch("insert into update_sample values(?, ?)") ;
int max = 1000 ;
for (int i = 0; i < max; i++) {
        cmd.addBatchParam(0, i) ;
        cmd.addBatchParam(1, i + "th ..") ;
 }
int count = cmd.execUpdate() ;
와 같이 설정하거나..

int[] a = new int[max];
String[] b = new String[max] ;
for (int i = 0; i < max; i++) {
    a[i] = i ;      b[i] = i + "th .." ;
}
cmd.addParam(a).addParam(b) ;
int count = cmd.execUpdate() ;
등과 같이 직접 Array를 인자로 설정한다.


    - 하나의 Batch문에 에 가장 효율적인 건수는 JVM에서 관리되는 Array가 차지하는 메모리에도 밀접한 관련이 있긴 하지만 대략 10,000-100,000 사이가 가장 좋다. row의 평균사이즈를 150-250 byte로 했을때 대부분은 만건당 최대 10초이내의 효과를 보인다.




IUserProcedure

   - 가장 많은 오해를 받지만 가장 효율적이며 많이 사용되는 객체이다. IUserProcedure는 오라클이나 MSSQL의 Procedure와는 직접적인 관계는 없다. 프로그램에 직접 ANSI SQL를 사용했을 경우 이후 DB에 변동이 있다면 재앙수준의 변경을 요구한다. 특히 SQL이 String 변수로 관리되기 때문에 직접 모든 프로그램을 열어서 수정해야 한다. 컴파일 오류가 아닌 런타임 오류가 나기 때문에 수정에 대한 확신도 매우 어렵다.

    이러한 문제로 몇년전부터는 IBatis등에서 XML 형태로 Key Value형태로 관리한후 Key 형태로 Access 하는 방식이 좀더 많이 퍼지게 되었다. 비슷하게 UserProcedure는 앞에서 말한 DB 벤더에 디펜던트한 Procedure와는 직접적 상관없이 그냥 IDName을 가진 Message 객체에 불과하다. 그 Message를 어떻게 해석할 것인가는 DB Manager에 달려 있으며

현재 Framework에 포함되어 있는 OracleDBManger는 패키지와 프로시저, MSSQL은 Procededure로, MySQL은 Procedure 혹은 Function으로 H2DBManager는 특정 XML 파일의 Key-Value 형식의 SQL name으로 해석하고 있다. 즉 UserProcedure는 DB Maanger를 어떻게 구현하는가에 따라 해석방법이 정해지며 일단 같은 name을 가진다면 DB 벤더의 프로시저 혹은 패키지 지원여부와 상관없이 동일한 결과값을 보장해야 한다.


ex)
IUserProcedure upt = dc.createUserProcedure("Sample@selectBy(:a)") ;
upt.addParam("a", 1); 
Rows rows = upt.execQuery() ;

만약 DBManager가 MySQLPoolDBManager라면 위 구문을 Sample_selectBy라는 procedure라고 해석하며
해당 MySQL에는 아래와 같은 Procedure가 있다면 해당 프로시저를 실행한후 그 결과 Rows를 반환한다.

CREATE PROCEDURE sample_selectBy(v_a int)
BEGIN
    SELECT * FROM update_sample WHERE a > v_a ;
END//



만약 DBManger가 오라클이라면 아래와 같이 Sample Package의 selectBy function을 실행시킨후 결과셋을 반환한다.

CREATE OR REPLACE PACKAGE BODY Sample
    is
        function selectBy (v_a number) return Types.cursorType
        is
            rtn_cursor Types.cursorType ;
        begin
            open rtn_cursor For
            select * from update_sample where a > v_a ;
           
            return rtn_cursor ;
        end ;

         ................
       
End Sample ;


     - 이와 같이 DB가 Procedure를 지원하는가 혹은 지원하지 않는가와 상관없이 IUserProcedure는 쓰이며 그 해석방법은 DBManager에게 달려있다. IUserCommand보다 IUserProcedure의 사용을 더 권장하는 이유는 DB 벤더의 장점을 최대한 활용할 수 있고 개발과 유지보수에 있어서의 장점때문이다. 물론 기타 성능이나 보안등의 자잘한 이유는 제껴두고서라도 말이다.


IUserProcedureBatch

    - IUserCommandBatch 처럼 UserProcedure의 Batch 버전이다. 다만 Proceudre 구조상 Batch라서 해서 UserProcedure와 비교해 커다란 성능차이는 없다. 다만 하나의 Procedure에 여러개의 SQL문을 담을 수 있다는 걸 생각하면 복잡한 배치처리 작업에나 쓸만하다.



UserProcedures
    - s가 뒤에 하나 붙어 있다. UserProcedure는 자신이 IQueryable의 구현체이면서 IQueryable의 구현체를 담을수 있는 composite 형태로 되어 있다. UserProcedures에 담긴 모든 MDL문은 자동으로 하나의 Transaction으로 처리가 된다. 그리고 물론 각각의 개별적인 실행보다는 조금 더 빠르다.

ex)
 IUserCommand cmd1 = dc.createUserCommand("insert into update_sample values(?, ?)") ;
 cmd1.addParam(1).addParam("abc") ;
 IUserCommand cmd2 = dc.createUserCommand("delete from update_sample where a = ?") ;
 cmd2.addParam(1);
       
 UserProcedures upts = dc.createUserProcedures("Multi MDL") ;
 upts.add(cmd1).add(cmd2) ;

 int result = upts.execUpdate() ;
 assertEquals(2, result) ;
    
    - 만약 UserProcedures에 여러개의 Select Query를 집어넣고 execQuery()를 실행하면 어떻게 될까? 이 경우에 여러개의 결과값을 반환하게 되는데 Rows.getNextRows()를 통해 다음 결과셋을 얻을수 있다. UserProcedures에 담긴 IQueryable 객체는 add된 순서대로 실행된다.

ex)        
  IUserCommand cmd1 = dc.createUserCommand("select * from copy_sample") ;
  cmd1.setPage(Page.create(10, 1)) ;
  IUserCommand cmd2 = dc.createUserCommand("select 3 from dept_sample") ;
       
  UserProcedures upts = dc.createUserProcedures("Multi Query") ;
  upts.add(cmd1).add(cmd2) ;
       
  Rows first = upts.execQuery() ;  // first query result ;
  assertEquals(true, first.getRowCount() == 10) ;
       
  Rows second = first.getNextRows() ; // second query result ;
  assertEquals(3, second.firstRow().getInt(1)) ;




XAUserProcedure

    - 앞의 UserProcedures는 하나의 DB에 대한 Transaction 보장이며 이기종 DB 즉 멀티 DB에 대한 멀티 MDL문의 분산 트랜잭션은 XAUserProcedure를 통해 할수 있다.


    MSSQL의 접속관리자인 mdc와 오라클의 접속관리자인 odc가 아래와 같이 이미 생성되어 있다고 하자.
// prepare
MSSQLDBManager mManager = new MSSQLDBManager("jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=test", "bleu", "redf");
DBController mdc = new DBController(mManager) ;
mdc.initSelf() ;

OracleDBManager oManager = new OracleDBManager("jdbc:oracle:thin:@novision:1521:al", "al", "redf") ;
DBController odc = new DBController(oManager) ;
odc.initSelf() ;
       

// tx1 : MSSQL용 쿼리 집합을 만든다.        
TxTransaction tx1 = new MSSQL2000TxTransaction(mManager, "mquery") ;
tx1.add((Queryable)mdc.createUserCommand("insert into copy_tblc values('111', 111)")) ;
       
// tx2 : oracle용 쿼리 집합을 만든다.
TxTransaction tx2 = new Oracle9iTxTransaction(oManager, "oquery") ;
tx2.add((Queryable)odc.createUserCommand("insert into copy_tblc values('111', 111)")) ;
       

// add tx : 두개의 쿼리 집합을 XAUserProcedure에 넣는다.
TxTransaction[] txs = new TxTransaction[]{tx1, tx2} ;
XAUserProcedure upts = new XAUserProcedure(txs) ;

// 실행한다.
upts.execUpdate() ;


// clear : 더이상 사용되지 않을 DB 접속정보라면 정리한다.
mdc.destroySelf() ;
odc.destroySelf() ;


      - 보통의 XaTransaction 보다 훨씬 더 간편하게 실행할 수 있다. 결국 인터페이스의 간결화로 호출자는 자세한 XaTransaction 과정에 대해 알 필요가 없다.

MSSQL에서 XaTransaction을 사용하기 위해서는
     * Be sure that you have copied your sqljdbc.dll file from C:\Program Files\Microsoft SQL Server 2000 Driver for JDBC\SQLServer JTA\ to your SQL Server's "binn" directory (most likely C:\Program Files\Microsoft SQL Server\MSSQL\Binn).
     * Then open your instjdbc.sql script from C:\Program Files\Microsoft SQL Server 2000 Driver for JDBC\SQLServerJTA\ within Query Analyzer and run the script. This will install the extended stored procedures into SQL Server so that it can reference the sqljdbc.dll file.

     의 과정을 실행해야 하며 MS DTC가 기동중이어야 한다. MySQL은 얼마전까지 XaTransaction을 지원하지 않았고 현재도 InnoDB storage engine에서만 지원하는 걸로 알고 있다.



CombinedQuery

     - CombinedQuery는 UserProcedure와 조금 비슷한데 단지 UserProcedures와는 달리 MDL문과 Query문을 동시에 실행시킬수 있다는 점이 다르다.


ex)
IUserCommand ins = dc.createUserCommand("insert into update_sample values(?,?)") ;
ins.addParam(1).addParam("abc") ;
       
IUserCommand sel = dc.createUserCommand("select * from update_sample") ;
       
CombinedUserProcedures upts = dc.createCombinedUserProcedures("combined") ;
upts.add(ins, "ins", IQueryable.UPDATE_COMMAND).add(sel, "sel", IQueryable.QUERY_COMMAND) ;

// 타입이 다른 ins문과 sel문을 동시에 실행시킨다.       
upts.execUpdate() ;
       
Map result = upts.getResultMap() ; // for access sel'result
       
int rowcount = (Integer)result.get("ins") ;
Rows rows = (Rows)result.get("sel") ;
       
assertEquals(1, rowcount) ;
assertEquals("abc", rows.firstRow().getString("b")) ;

    - 역시 하나의 Transaction으로 처리된다. 주로 마이그레이션 작업이나 session scope temporary table을 가지고 작업할때 쓰인다. 혹은 Session Level Configuration 명령문과 select문을 혼합할때 쓰이기도 한다. 즉 특정경우에 Set Ansi_null Off 시키고자 할때 session 명령문은 Update로 Select문은 Query로 실행시킨다. 앞어 거듭 말했듯 Rows는 클라이언트 커서이기 때문에 한건을 insert 하고 해당 건을 select하고 해당 건을 지우는 3개의 명령문을 combinedProcedure에 넣고 실행시켜도 제대로 된 Rows 결과물을 볼 수 있다.




ExtraServant


    - ExtraServant는 IQueryable의 구현 클래스가 아니며 이름 그대로 IDBController의 하인 역할를 수행한다. 예컨데 모든 쿼리의 실행시간을 System.out으로 확인하고 싶다고 하자.

ex)
dc.addServant(new StdOutServant(StdOutServant.All)) ;

를 실행시켜 주면 해당 dc로부터 생성된 모든 IQueryable 객체는 실행후 StdOutServant를 통해 쿼리 이름과 실행시간을 System.out에 출력한다. 그와 동시에 300ms 이상의 모든 쿼리는 따로 기록해 두고 싶다고 한다면.

ex)
dc.addServant(new TraceOfLateProcServant(300)) ;
를 추가시키면 된다.

dc는 1개 이상의 ExtraServant를 가지며 모든 쿼리는 해당 쿼리 작업을 완료후 Chain된 ExtranServant를 통해 임의의 추가 행동을 지정할 수 있다. ExtraServant는 별도의 Thread로 관리되며 ExtraServant의 동작시간과 행동은 기존의 작업에 영향을 주지 않는다.

별도의 IDBController를 새로 만들어서 Connection만 공유한채 Servant 정보는 달리 유지시킬수 있다.

ex)
DBController newDc = new DBController("newDC", dc.getDBManager()) ;
newDc.addServant(new StdOutServant(StdOutServant.All)) ; // 모든 IQueryable를 화면에 프린트하는 Servant를 추가한다.
newDc.addServant(new StdOutServant(StdOutServant.All)) ; // 한개 더 추가한다.
newDc.initSelf() ;
       
newDc.getRows("select 1 from copy_sample") ;
newDc.destroySelf() ;

위의 경우 newDC는 기존 dc.getDBManger의 Owner가 아니기 때문에 destroySelf()를 실행시켜도 해당 DBManger의 Pool은 파괴되지 않는다.

     - ExtraServant는 다양한 상황에 새롭게 상속받아 구현함으로써 다양한 역할을 수행할 수 있다. 이를테면 특정 사용자가 접속했을때 MSN으로 메시지를 보내주는 Servant를 구현할 수도 있고 특정 쿼리들이 실행되었을때 Log를 남기는 Servant를 구현할수도 있다.



   Handler

   - Rows는 여러종류의 Handler를 사용하여 다양한 타입으로 변신이 가능하다.


Rows rows = dc.getRows("select * from copy_sample order by no1", 10, 1) ;

첫번째 row를 Map 형태로 바꿀 수도 있고
ex)
Map results = (Map)rows.toHandle(new MapHandler()) ;
assertEquals(2, results.size()) ;   // column 수 확인

모든 row를 Map의 List 형태로 바꿀 수도 있고.
List<Map> results = (List<Map>)rows.toHandle(new MapListHandler()) ;
assertEquals(10, results.size()) ; // row수 확인

모든 row를 Bean List 형태로 바꿀 수도 있고
List<TestBean> results = (List<TestBean>)rows.toHandle(new BeanListHandler(TestBean.class)) ;
TestBean row = results.get(0);

assertEquals(1, row.getNo1()) ;
assertEquals("01", row.getNo2()) ;

assertEquals(2, results.get(1).getNo1()) ;
assertEquals("02", results.get(1).getNo2()) ;


특정 컬럼의 값만을 얻어올수도 있다.
Object value = rows.toHandle(new ScalarHandler(2)) ; // 첫번째 row의 2번째 컬럼의 값
assertEquals("1", value.toString()) ;




6년전에 해당 Framework를 처음 개발했을때 목적은 4가지이다.

1. 풀링을 구현하되 프레임워크 사용자가 실수로라도 Connectionless를 만들 수 있는 방법을 원천적으로 차단한다.
    -> Connectio은 외부에서 보이지 않으며 Client Cursor 사용으로 결과셋 전달후 Resource는 자동으로 바로 반환한다. 즉 사용자는 JDBC의 Resource에 대해 알 필요도 없으며 알 수도 없다.

2. 프로그램에 SQL문을 심지 않는다.
    -> UserProcedure의 적극적인 사용으로 Query문은 모두 중앙집중 관리 되며 이는 개별적인 DB 벤더의 장점은 최대한 활용함과 동시에 DB 벤더에 종속되지 않는다는 상이한 목표를 동시에 추구한다. IQueryable의 모든 구현체는 단순히 Message역할만을 수행하기 때문에 JDBC의 PreparedStatement와 달리 정리해줘야 할 Resource가 아니다.

3. LOB 핸들링을 쉽게 한다.
   -> 벤더마다 그리고 버전마다 Lob Handling에 있어서의 사용방법이 다른걸 간단한 인터페이스로 통합시고 사용자는 byte 단위의 Access Handling을 하지 않아도 되게 한다.

4. 사용방법은 쉬워야 함과 동시에 여러가시 발생할 수 있는 상황에 쉽게 적응 가능해야 한다. 동시에 불필요한 퍼포먼스 감소를 일으켜서는 안된다.



이후.. 한동안 추가 기능이 없다가 최근에

  - MySQL을 기존의 MSSQL, Oracle과 더블어 기본 테스트 항목및 지원DB로 함.
  - 인터페이스의 소폭 수정
  - viewPlan(Query의 실행계획보기)
  - Paging (쿼리의 Page 설정, nextPage, prePage Access)
  - NamedParameter

등등의 잡다한 기능을 넣었다. 가장 큰 이유는 Paging 때문인데 JDBC에 Client cursor에 Page 관련 메소드가 있는데 제대로 implement한  JDBC가 거의 없어서 그냥 만들었다. -ㅅ-. Page는 DB 마다 처리 방법이 다르지만 해당 DB에 가장 효율적인 방법을 사용하려고 했다.








사용자는 framework_core_fat.jar를 추가하고 해당 DB의 JDBC jar만 추가시켜서 사용하면 된다.

아래와 같은 Apache Common Jar를 사용한다.
<fatjar.jarsource file="lib\jericho-html-2.5.jar" relpath=""/>  // 얘는 parser framework에 쓰임
<fatjar.jarsource file="lib\jmock-core-1.2.0.jar" relpath=""/>
<fatjar.jarsource file="lib\junit.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-pool-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-beanutils-1.8.0.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-collections-3.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-dbcp-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-digester.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-fileupload-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-io-1.2.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-lang-2.3.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-logging-1.0.4.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-net-1.3.0.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-validator-1.1.4.jar" relpath=""/>



'Framework > Database' 카테고리의 다른 글

DBFSlayers  (1) 2010.04.04
Framework Cursor (cursor in MySql)  (1) 2010.02.24
Framework - client cursor  (0) 2009.03.26
Framework - 커서의 선택 .. and  (0) 2009.03.24
프로시저 vs SQL  (0) 2009.03.23
Posted by bleujin
Framework/Database2010. 2. 24. 09:55


이전글에서 커서의 일반 지식과 오라클과 MSSQL의 커서 방식에 대해 얘기 했으니 3번째로 MySQL의 커서 방식을 이야기해보자. 사실 개인적으로 MySQL은 상대적으로 자주 사용하는 DB가 아니다. MySQL은 "거의" 무료지만 다른 2개의 디비에 비해 기능과 지원이 부족하고 다른 '완전한' 무료에 비해 그리 많이 좋지도 않는 어정쩡한 위치에 있기 때문이다.

거의 1년만이므로..
다시 기본조건을 상기해보자.

JDBC로 stmt.execQuery("select * from millonRow_tblc") ; 를 실행했을때..


Large ResultSet의 Handling에 관한 문제이다.


우리가 먼저 시작해야 할 점은 사용자는 과연 모든 결과가 필요할까? 라는 점이다.

물론 100만개의 Row가 모두 필요한 프로그램일수도 있다. 하지만 그 1%의 확률로 모든 Row가 필요한 프로그램이라고 하더라도 그 중에 다시 99% 이상은 DB에서 연산이 가능하다. DB는 다른 어떤 통계 프로그램보다 많은 연산자와 빠른 속력을 지원해주기 때문에 정말 화면에 모든 Rows를 뿌려야 하는 0.01%의 확률의 프로그램이 아니라면 연산을 DB에서 하는게 좋다.

어쨌거나 지금 그 0.01%의 확률의 프로그램을 만들어야 한다고 가정하자.
즉 당신은 이유야 어쨌거나 "select * from millonRow_tblc" 모든 결과 셋이 필요하다.


DB가 MySQL일때
pstmt = conn.prepareStatement("Select * ....", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
rs = pstmt.execQuery() ;
를 실행해보자.

아마도 특별히 Java의 Heap 메모리를 늘리지 않았다면 Out of Memory 예외를 보게 될것이다.
단 한행도 rs.next()를 하지 않았음에도 불구하고 말이다.

한행이 약 100byte라고 가정했을때 0.1k * 1000000 = 100M로
Select의 모든 결과셋의 크기가 기본 Java Heap Memory의 64M를 초과하기 때문이다.


MySQL에서 - 비록 공식문서에는 나와 있지 않지만
당신이 특별한 다른 옵션을 사용하지 않았을때 Client Cursor Location에 Static Cursor Type을 사용한다.

즉 JDBC에서 위 예제를 실행했을때
해당 쿼리의 모든 결과셋을 Client(여기서는 Application Server가 Client)에 전송하고 Client에서는 그 결과셋을 모두 받아서 Client 메모리안에 Table 형의 결과셋을 만들어 채우는 과정을 한다. 보통의 다른 Set 알고리즘과 마찬가지로 기본 Set이 거의 채워지면 해당 Set의 기존 Size를 1.5배 늘리고 이 용량이 다시 채워지면 다시 1.5배를 늘리는 동작을 반복한다.
다시 말해서 위의 경우 DB에 던지는 결과셋을 받아서 계속 Set의 용량을 늘리다가 어느순간 한계 Heap Size에 도달하게 되는 것이다.

따라서 정말 millonRow_tblc의 Table의 모든 Row가 필요하다면 Heap Size를 늘려야 한다. 그러나 근본적인 해결책은 아니다. 왜냐하면 Heap의 Size를 10배로 늘렸다고 해도 10명이 동시에 해당 프로그램을 실행하면 역시 Out of Memory Exception을 보게 된다. 백만 Row를 모두 받아서 Clinet에 Set을 구축하기까지는 꽤 오랜(아마도 컴퓨터 성능에 따라 1 - 10분 사이) 시간이 걸리기 때문에 10명이라는 수치는 매우 자주 일어나는 경우가 될것이다. 물론 사용자가 단 1명이더라도 결과를 보기위해서는 1-10분을 기다려야 한다.



공식적인 해결방법은 아니지만 MySQL에서는 일종의 꽁수같은 방법이 있다. 이것은 일종의 버그이므로 앞으로도 계속 지원되는 방법이라고는 보장할수 없다.

pstmt = conn.prepareStatement("Select * ....", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(Integer.MIN_VALUE) ;
psmt.execQuery() ;

위와 같이 ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY를 사용하고
FetchSize에 Integer.MIN_VALUE를 설정하면
MySQL은 Client에 더이상 Static Cursor를 사용하지 않는다. 위와 같이 작성할경우 MySQL은 FireHouse Cursor방식을 사용한다. FIreHouse Cursor는 앞에서 나왔던 MSSQL의 기본 cursor이다.

공식적인 cursor Type에서는 언급하지 않지만 인터넷 환경에서는 묵시적으로 자주 사용된다. FireHouse 커서 동작방식에 대해 말하기 전에 MYSQL의 JDBC는 Server Cursor Location을 지원하지 않는다. 물론 근본적으로 JDBC의 API에 Cursor Location을 설정할수 있는 메소드가 없기 때문이기도 하지만 그럼에도 MSSQL에서는 CursorType에 따라 묵시적으로 바뀌는 것과 달리 MySQL에서는 아예 Server Cursor Locatiion을 공식적으로 지원하지 않는다.

따라서 MSSQL의 FireHouse 커서와는 조금 다른 동작을 한다.

pstmt = conn.prepareStatement("Select * ....", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(Integer.MIN_VALUE) ;
rs = psmt.execQuery() ;
for( int i = 0 ; i < 10 ; i++) {
   out(rs.getString(1)) ;
}

와 같이 실행하였다면 반갑게도 바로 결과가 나올것이다. Connection Pool을 사용했다면 0.1초 이내에 말이다.

유레카~ 라고 소리지르기 전에..알아야 할 것이 있다.
FireHouse 커서방식은 Static Cursor Type과 달리 모든 결과셋을 Client에 구축하지 않고 Row Level 단위로 Access할수 있는 장점이 있긴 하지만 단점이 2가지가 있다.
첫번째는 rs는 더이상 previons()나 relative/absolute move를 지원하지 않는다. 이는 FireHouse의 동작방식을 생각해보면 당연한 것이다. FireHouse방식에서는 rs.next()를 호출하는 순간에 더 이상 이전 previous의 커서의 위치나 정보를 보관하지 않는다. next()를 호출할때마다 이전 record의 정보는 메모리에서 날림으로서 Out Of Memory를 피하는 방식이다. 그리고 당연히 rs.getRowCount() 같은 메소드도 사용할수 없다. 만약 RowCount를 알고 싶다면 while(rs.next()) i++ 와 같이 구할수 밖에 없다.

첫번째는 사실 그다지 큰 문제는 아니다. 보다 중요한 것은 두번째 단점이다.
두번째 단점은 MYSQL의 Client Location의 FireHouse Cursor는 멈추지 않는다는 것이다.
for( int i = 0 ; i < 10 ; i++) {
   out(rs.getString(1)) ;
}
와 같이 단지 10행만을 사용한다고 하더라도 MYSQL DB는 그걸 인지할수 없으며 여전히 남은 99만 9990개의 남은 row를 Client에 전송한다.

그래서
pstmt = conn.prepareStatement("Select * ....", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(Integer.MIN_VALUE) ;
rs = psmt.execQuery() ;
for( int i = 0 ; i < 10 ; i++) {
   out(rs.getString(1)) ;
}
까지 0.1초가 걸리더라도..

rs.close() ;
이 문장을 실행하는데 1분 - 10분 정도가 걸린다. FireHouse는 일종의 Data Stream이기 때문에 모든 결과셋을 받기 전까지는 rs를 close 할수 없다. 그렇다고 rs.close()를 하지 않으면 해당 Connection은 미아가 되어 얼마지나지 않아 Connection Pool Exception을 보게 된다.

가끔 특정 사이트를 보면 화면에 내용은 거의 다 뿌려졌는데 브라우저 하단의 Progress Bar는 여전히 진행중인 사이트를 본다면 반이상은 JSP로 위의 방식으로 코드를 작성했기 때문이다. response.flush()로 일단 화면에 보여지지만 JSP코드 하단의 rs.close()를 하는데 오래오래 걸리고 있기 때문이다.

MVC가 머고간에 일단 JSP에서 JDBC를 억세스 해서 close()를 하기전에 일단 Access한것만이라도 flush로 먼저 보낸다면 화면에는 빨리나온것처럼 보이기에 일단 급한 마음에 그렇게 한것이다.

Lazy Initialize 패턴같은 사용방식처럼 보일수도 있지만 이런다고 문제는 해결되지 않는다. 일단 Client에서 빨리 보여지느냐 늦게 보여지느냐에 상관없이 FireHouse Cursor Mode에서 Server인 DB는 여전히 남은 Row 들을 보내고 있다. 그 말은 귀중한 자원인 DB는 Network Channel에 사용하지 않을 Row를 쓰고 있고 여전히 Application Servers는 사용하지 않을 Row을 받고 있다는 얘기이다.

최근의 대부분의 하드는 100Mb read/sec 이상의 속도를 가진다. 그럼에도 대부분의 Network Channel은 고작해야 10Mb / sec이기 때문에 필요도 없는 Row를 Network로 보내느라 DB는 계속 바쁘다.

처음에 언급한바와 같이 상황에 따라 DB에 따라 심지어는 버전에 따라 이와 같은 문제를 해결하는 방식은 다르기에 이전의 Mssql의 방식을 사용할수는 없다.(다시 말하지만 MySQL의 JDBC에서는 Server Cursor Location이 없다.) 해결방법은 다음글에서.. =ㅅ=


'Framework > Database' 카테고리의 다른 글

DBFSlayers  (1) 2010.04.04
DB Framework 2.0 Short Explain  (3) 2010.03.09
Framework - client cursor  (0) 2009.03.26
Framework - 커서의 선택 .. and  (0) 2009.03.24
프로시저 vs SQL  (0) 2009.03.23
Posted by bleujin