iBATIS는 간단한 XML서술자를 사용해서 간단하게 자바빈즈를 SQL statement에 맵핑시킨다. 간단함(Simplicity)이란 다른 프레임워크와 객체관계맵핑툴에 비해 iBATIS의 가장 큰 장점이다. iBATIS Data Mapper를 사용하기 위해서 당신은 자바빈즈와 XML 그리고 SQL에 친숙할 필요가 있다. 여기엔 배워야 할것도 거의 없고 테이블을 조인하거나 복잡한 쿼리문을 수행하기 위해 필요한 복잡한 스키마도 없다. Data Mapper를 사용하면 당신은 실제 SQL문의 모든 기능을 가질수 있다. JDBC 로만 프로그래밍 할 때의 번거로움을 줄여주기 위해 재사용 모듈로 개발된 것인데 그 주요한 어려움을 ibatis 개발자 가이드에서 다음과 같이 정리하고 있다.
iBATIS의 역사와 사용
* SQL 문장과 프로그래밍 코드의 분리 * JDBC 라이브러리를 통해 매개변수를 전달하고 결과를 추출하는 일 * 데이터베이스 접근 클래스와 비즈니스 로직을 담은 클래스의 분리 * 자주 쓰이는 데이터를 변경되지 않는 동안에 임시 보관(Cache) * 트랜젝션과 쓰레드 관리
이러한 사항들에 대해서 한번쯤 생각해볼 필요가 있다.
# SQL 문장과 프로그래밍 코드의 분리 SQL문과 프로그램 코드는 꼭 분리해야 할까? 우선 프로그램이 작은 경우는 굳이 분리할 필요가 없어 보인다. 파일만 두개로 분리되기 때문에 관리에 불편함이 가중될 수 있다. 프로그램이 커져도 분리하는 것이 꼭 유리한 것만은 아니다. SQL을 사용하는 클래스가 1000개라면 분리했을 때 2000개가 되어서 더 복잡해질 수도 있다. 그럼에도 불구하고 ibatis에서는 SQL을 분리하는 이유는 무엇일까? Separation of Concern!! 할 일의 명확한 분리. 아마도 이것이라 짐작된다. 자바 코드에서 SQL을 없애서 순수 OO로 만들어놓기. 이것은 결벽증이나 지나친 원리집착이 아니라 체계가 다른 것을 나누어 보관하는 것이다. 논리적으로 정연한 흐름을 만들어 둘 수 있고, 객체지향 입장에서 데이터에 해당하는 도메인 모델(Domain model) 혹은 도메인 객체(Domain object)와 관계형 데이터베이스의 테이블(Table)을 느슨한 관계(loosely coupled)로 만들어서 변화에 대한 유연성을 확보할 수도 있다. 즉, 정규화를 위해서 혹은 데이터 접근의 효율성을 위해서 테이블 구조를 바꾸어도 프로그램 코드 안에 있는 SQL을 수정할 필요가 없어진다.
# JDBC 라이브러리를 통해 매개변수를 전달하고 결과를 추출하는 일 select 등를 할 때 조건에 해당하는 매개변수를 설정하거나, insert 문에서 값을 할당하기 위해 매개변수를 설정하는 일은 무척 번거로운 일이다. 단순한 일이면서도 오류가 잦은 부분이다. 때문에 프로젝트를 몇 차례 겪고 나면 자신만의 라이브러리를 만들거나 공통팀에서 유틸리티를 제공하는 일을 흔하게 볼 수 있다. Apache Commons의 DB Utils 나 Spring의 JDBC Template 등은 이러한 작업을 용이하게 하려고 등장했고, ibatis 역시 이러한 작업을 훨씬 부드럽게 해준다.
# 데이터베이스 접근 클래스와 비즈니스 로직을 담은 클래스의 분리 이른바 DAO(Data Access Object) 패턴이 이러한 일을 담당한다. ibatis는 DAO 계층 구현을 위한 유틸리티 성격이면서 동시에 best practice 역할도 수행한다.
# 자주 쓰이는 데이터를 변경되지 않는 동안에 임시 보관(Cache) ibatis 에선 XML 설정만으로 캐시를 할 수 있다.
# 트랜젝션과 쓰레드 관리 트랜젝션 처리 역시 용이하다.
iBATIS의 주요기능
Data Mapper (com.ibatis.sqlmap.*)
개념 iBATIS Data Mapper API는 프로그래머에게 자바빈즈 객체를 PreparedStatement파라미터와 ResultSets으로 쉽게 맵핑할 수 있도록 한다. Data Mapper의 기본적인 생각은 간단함(simple)이다. 이는 자바코드의 20%를 사용하여 JDBC기능의 80%를 제공하는 간단한 프레임워크라는 뜻이다.
작동원리 Data Mapper는 자바빈즈, Map구현, 원시래퍼타입(String, Integer…) 그리고 SQL문을 위한 XML문서를 맵핑하기 위한 XML서술자를 사용하는 매우 간단한 프레임워크를 제공한다.
다음은 생명주기에 대한 높은 레벨의 서술이다.
1) 파라미터(자바빈즈, Map 또는 원시래퍼)로써 객체를 제공한다. 파라미터 객체는 update문내에 입력값을 셋팅하기 위해 사용되거나 쿼리문의 where절을 셋팅하기 위해서 사용된다.
2) 맵핑된 statement을 실행한다. 이 단계는 마법이 일어나는곳이다. Data Mapper프레임워크는 PreparedStatement 인스턴스를 생성할것이고 제공된 파라미터객체를 사용해서 파라미터를 셋팅한다. 그리고 statement를 실행하고 ResultSet으로부터 결과 객체를 생성한다.
3) update의 경우에 영향을 미친 rows의 숫자를 반환한다. 조회문일경우에 한 개(single)의 객체 또는 컬렉션 객체를 반환한다. 파라미터처럼 결과 객체는 자바빈즈, Map 원시타입래퍼또는 XML이 될수 있다.
SQLite (1) 데이터베이스의 정의 : Android에서 데이터베이스로 접근하려면, SQLiteOpenHelper 클래스를 상속한 데이터베이스 헬퍼를 정의하고 이것을 이용한다. 이 헬퍼는 데이터베이스의 생성 및 업그레이드를 관리한다. SQLiteOpeHelper 클래스를 상혹한 클래느는 다음의 메소드를 오버라이드한다.
오버라이드하는 메소드
메소드의 호출
onCreate() 메소드 onUpgrade() 메소드
데이터베이스 생성 시 호출. 데이터베이스 업그레이드 시 호출.
(2) 데이터베이스 헬퍼 생성자 SQLiteOpenHepler 클래스
SQLiteOpenHelper(Context context, String fileName, SQLiteDatabase.CursorFactory factory, int version)
기능 : SQLiteOpenHelper 클래스의 생성자 인수 : context 컨텍스트 fileName 데이터베이스 파일명
factory 팩토리
version 버전
데이터베이스 이름에는 저장할 곳의 파일명을 지정합니다. /data/data/패키지명/databases/데이터베이스 파일명 ( test.db) 에 저장됩니다. 팩토리는 사용하지 않으면 null을 지정하고, 버전은 1을 지정.
(3) 데이터베이스의 생성 : onCreate() 메소드를 호출 SQLiteOpenHelper 클래스
void onCreate(SQLiteDatabase db)
기능 : 데이터베이스 생성 시 호출 인수 : db 데이터베이스 객체
onCreate() 메소드 내에서는 SQL명령으로 테이블을 생성하고 있습니다. test 라는 테이블명에서 id 와 info 라는 컬럼을 갖고 있는 테이블을 생성하는 SQL 명령은 아래와 같습니다. 컬럼의 형은 문자열이므로 컬럼명 뒤에 text라고 기술하고, id 는 다른 ID 와 중복되지 않는 기본키이므로 primary key 라고 추가합니다. create table id not exists text(id text primary key, info text)
SQL명령을 실행하려면 SQLtestDatabase 클래스의 execSQL()을 사용합니다. SQLiteDatabase 클래스
void execSQL(String Sql)
기능 : SQL 명령의 실행 인수 : sql SQL명령
(4) 데이터베이스의 업그레이드 : 데이터베이스는 버전업 시 데이터베이스 구조를 변환하지 않으면 업그레이드가 안 되는 경우가 있습니다. 필요로 하는 버전에 이상이 있을 경우 이 onUpdate() 메소드가 불리므로, 이 메소드 내에서 구버전 번호와 신버전 번호의 정보로부터 데이터베이스 구조의 변환을 실행합니다. SQLiteOpenHelper 클래스
void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
기능 : 데이터베이스를 업그레이드한 때 인수 : db 데이터베이스 객체 oldVersion 구버전 번호
newVersion 신버전 번호
다음으로, SQL명령으로 테이블을 삭제한 후 onCreate() 메소드를 호출하여 테이블을 생성합니다. test 라는 이름의 테이블을 삭제할 SQL명령은 다음과 같습니다. drop table if exists test
(5) 데이터베이스 객체의 취득 : 메인이 되는 SQLiteEx 클래스의 개체에서는 먼저 정의한 데이터베이스 헬퍼를 이용하는 SQLiteDatabase 형 데이터베이스객체를 구합니다. 프로그램 내에서는 이 객체를 사용해서 데이터베이스의 조작을 실행합니다. 데이터베이스 객체를 구하려면 DBHelper 객첼르 생성한 후 getWritableDatabase() 메소드를 호출합니다. SQLiteOpenHelper 클래스
SQLiteDatabase getWritableDatabase()
기능 : 데이터베이스 객체의 취득 인수 :데이터베이스 객체
(6) 데이터베이스의 쓰기 : 데이터베이스의 쓰기를 실행하려면 Content Values 클래스로 갱신하는 레코드 정보를 작성합니다. Content Values 객체를 생성한 후 put() 메소드로 컬럼명과 값을 세트로 추가합니다. ContentValues 클래스
void put(String colName, String value)
기능 : 레코드 정보에 컬럼명과 값 추가 인수 : colName 컬럼명 value 값
이 프로그램에서는 다음의 칼럼을 추가하고 있습니다.
컬러명
값
id info
0 텍스트 박스의 문자열
데이터 객체의 update() 메소드에 이 레코드 정보를 건네주는 것에 의해서, id[0]을 갖는 레코드의 info가 텍스트 박스의 문자로 덧쓰게 됩니다. SQLiteDatabase 클래스
int update(String tableName, ContentValues values, String where, String[] whereArgs)
기능 : 테이블 레코드의 갱신 인수 : tableName 테이블명 values 레코드 정보
where where 파라미터
whereArgs where 파라미터 반환값 : 갱신한 레코드 수
반환값 레코드 수가 0이 될 때는 id[0] 이 존재하지 않는다고 간주하고, insert() 메소드로 테이블에 레코드를 삽입한다. SQLiteDatabase 클래스
long insert(String tableName, String nullColumnHack, ContentValues values)
기능 : 테이블의 레코드 추가 인수 : tableName 테이블명 nullColumnHack NULL값
values 레코드 정보
반환값 : 레코드 ID
(7) 데이터베이스로부터 읽기 : 데이터베이스로부터 읽기를 실행하려면 데이터베이스 객체의 query() 메소드를 사용합니다. SQLiteDatabase 클래스
기능 : 데이터베이스로부터 읽기 인수 : tableName 테이블명 columns 컬럼명의 배열 selection selection parameter selectArgs selection parameter factor groupBy groupBy parameter having having parameter orderBy orderBy parameter limit limit parameter
반환값 : Cursor 객체
selection, groupBy, having, orderBy, limit 파라미터는 SQLite의 query 명령 파라미터입니다. 이번에는 id가 0 인것만을 취득하고 싶기 때문에 selection 파라미터에 "id=0" 을 지정하고 있습니다. 반환값은 Cursor 객체로 구해지는데, Cursor 객체는 내부적으로 다수의 레코드를 저장하고 있습니다. getCount() 메소드로 레코드 수, getColumnCount() 메소드로 컬럼 수를 구할 수 있습니다.
Cursor 클래스
int getCount()
기능 : 레코드 수 구하기
반환값 : 레코드 수
Cursor 클래스
int getColumnCount()
기능 : 컬럼 수 구하기
반환값 :컬럼 수
Cursor 객체는, 자장하는 레코드 안의 조작대상을 지시하는 정보(커서)를 갖습니다. 커서에서 선두의 레코드를 지시하는 때에는 moveToFirst() 메소드를 부릅니다. Cursor 클래스
boolean moveToFirst()
기능 : 커서에서 선두 레코드를 지시한다.
반환값 : 성공 또는 실패
그리고 나서, getString() 메소드 등을 부르는 것으로 레코드 내의 컬럼이 지시하는 값을 수하는 것이 가능합니다. Cursor 클래스
String getString(int colIdx)
기능 : String 형 값 구하기 인수 : colIdx 컬럼의 위치
반환값 : String 형의 값
double getDouble(int colIdx)
기능 : double 형 값 구하기
인수 : colIdx 컬럼의 위치
반환값 : double 형의 값
float getFloat(int colIdx)
기능 : float 형 값 구하기
인수 : colIdx 컬럼의 위치
반환값 : float 형의 값
int getInt(int colIdx)
기능 : int 형 값 구하기
인수 : colIdx 컬럼의 위치
반환값 : int 형의 값
long getLong(int colIdx)
기능 : long 형 값 구하기
인수 : colIdx 컬럼의 위치
반환값 : long 형의 값
short getShort(int colIdx)
기능 : short 형 값 구하기
인수 : colIdx 컬럼의 위치
반환값 : short 형의 값
여기에서는 컬럼1(0부터 시작하므로 2열 째) 값을 String 형으로 얻고 있습니다. 마지막으로, Cursor 객체의 close() 메소드를 호출하여 Cursor 를 종료합니다.
public class SQLiteEx extends Activity implements View.OnClickListener{ private final static String DB_NAME = "test.db"; // DB name private final static String DB_TABLE = "test"; // table name private final static int DB_VERSION = 1; // Version private EditText editText; private Button btnWrite; private Button btnRead; private SQLiteDatabase db;
// 초기화 @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); requestWindowFeature(Window.FEATURE_NO_TITLE); // 레이아웃의 생성 LinearLayout layout = new LinearLayout(this); layout.setBackgroundColor(Color.rgb(255, 255, 255)); layout.setOrientation(LinearLayout.VERTICAL); setContentView(layout); // 텍스트 박스의 생성 editText = new EditText(this); editText.setText("", EditText.BufferType.NORMAL); setLLParams(editText, 240, 50); layout.addView(editText); // 쓰기 버튼의 생성 btnWrite = new Button(this); btnWrite.setText("쓰기"); btnWrite.setOnClickListener(this); setLLParams(btnWrite); layout.addView(btnWrite); // 일기 버튼의 생성 btnRead = new Button(this); btnRead.setText("읽 기"); btnRead.setOnClickListener(this); setLLParams(btnRead); layout.addView(btnRead); // 데이터베이스 객체 구하기 (5) DBHelper dbHelper = new DBHelper(this); db = dbHelper.getWritableDatabase(); } // 버튼 클릭 이벤트 처리 public void onClick(View v){ if ( v == btnWrite) { try { String str = editText.getText().toString(); writeDB(str); } catch (Exception e) { showDialog(this, "Error", "Write Error"); } } else if ( v == btnRead) { try { String str = readDB(); editText.setText(str); } catch (Exception e) { showDialog(this, "Error", "Read Error"); } } }