iOS의 SQLite-FMDB 사용

Programming/Objective-C 2012. 4. 27. 10:29

iOS에서 데이터를 저장하는 방식 중에는 크게 번들 plist 파일에 저장하는 방법과 SQLite로 db방식으로 저장하는 방법이 있다. plist는 파일 형식으로 저장하는 방법, SQLite는 db 형식으로 저장하는 방법으로 이해할 수 있는데 SQLite를 사용하기 위해서는 db를 쓰고 읽는 기능을 하기 위해서는 적지 않은 코드를 작성해야하는 번거로움이 있다. 이를 해결하고자 FMDB라는 Wrapper library를 GitHub에서 배포하고 있다. 정리 차원에서 일본 블로거 포스트 내용을 번역하여 정리해 본다.


1. FMDB 란?

FMDB는 SQLite를 iOS의 Objective-C에서 사용하기 편하도록 Wrapper Library를 GitHub에서 공개하고 있다. 

https://github.com/ccgus/fmdb


인터페이스는 JDBC 또는 ADO.NET에 가깝다. 따라서, 이것을 사용한 적이 있으면 원활하게 이해할 수 있을 것이다.



2. FMDB 준비

먼저 FMDB를 이용하고 싶은 프로젝트에 SQLite 라이브러리를 추가한다. 

1. Xcode 왼쪽 창의 탐색창에서 프로젝트를 선택 

2. 오른쪽 창에 PROJECT와 TARGET이 표시되며 TARGET을 선택

3. 오른쪽 창, 상단 탭에서 Build Phases 선택

4. 빌드에 대한 설정 항목이 표시되는데 그 중에서 Link Binary With Libraries를 선택

5. Link Binary With Libraries 왼쪽 아래에 +를 클릭

6. Choose framework and libraries add :라는 메시지가 표시되고

7. 검색을 통해서 프레임워크와 라이브러리 목록에서 libsqlite3.0.dylib 선택 

8. 대화 상자 오른쪽 아래 Add 버튼을 클릭


그 다음으로 FMDB 소스를 프로젝트에 추가한다.

1. FMDB 프로젝트 페이지에서 ZIP 파일을 내려 받는다.

2. 압축을 풀고 src 폴더에서 fmdb.m 파일을 제외하고 프로젝트에 import

3. 



3. 데이터베이스 만들기 및 open / close

데이터베이스를 제공하는 경우 미리 만든 데이터베이스 파일을 프로젝트 리소스에 통합하고 그 것을 복사하거나 SQLite를 생성해야한다. iOS 애플리케이션 작업 영역에 app.db라는 데이터베이스 파일을 생성하는 경우는 다음과 같이 처리한다.


NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *dir = [paths objectAtIndex : 0]; FMDatabase *db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"app.db"]]; NSLog(@"%@", db); [db open]; [db close];

databaseWithPath 메소드에 파일의 경로를 지정하여 파일이 기존에 존재하면 참조하고 없을 경우에는 새로 만들고 연결된 FMDatabase 인스턴스를 반환한다.


데이터베이스 작업을 시작하면 이 인스턴스에 대한 open 메소드를 호출한다. 종료는 close 메소드, close를 호출하면 데이터베이스를 닫고 변경된 내용을 파일에 저장한다. 


4. CREATE

테이블 생성은 다음과 같다.


NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *dir = [paths objectAtIndex : 0];
FMDatabase *db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"app.db"]];
NSString *sql = @"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);";
[db open];
[db executeUpdate:sql];
[db close];


FMDatabase - executeUpdate 메소드에 CREATE 문장을 지정하여 작업을 수행한다. SQLite는 IF NOT EXISTS에 대응하고, 이 것을 붙이면 테이블이 존재하지 않을 때만 생성 해주기 때문에 편리하다.


5. INSERT

행을 추가하는 것은 다음과 같다.


NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *dir = [paths objectAtIndex : 0];
FMDatabase *db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"app.db"]];
NSString *sql = @"INSERT INTO users (name) VALUES (?)";

[db open];
[db executeUpdate:sql, @"이름" ];
[db close];


executeUpdate는 Prepared Statement에 대응하고 있다. SQL 문장에서 ?를 작성한 매개변수 부분에 가변 인자를 할당한다.



6. DELETE

행 삭제는 다음과 같다.


NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *dir = [paths objectAtIndex : 0];
FMDatabase *db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"app.db"]];

NSString *sql = @"DELETE FROM users WHERE id =?";
[db open];
[db executeUpdate:sql, [NSNumber numberWithInteger:14]];
[db close];


7. SELECT

데이터 선택은 다음과 같다.


NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *dir = [paths objectAtIndex : 0];
FMDatabase *db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"app.db"]];

NSString *sql = @"SELECT id, name FROM users;";
[db open];

FMResultSet *results = [db executeQuery:sql];
NSMutableArray *users = [[NSMutableArray alloc] initWithCapacity:0];

while ([results next])
{
      User *user = [[User alloc] init];
      user.userId = [results intForColumnIndex:0];
      user.name = [results stringForColumnIndex:2];
      [users addObject:user];
}

[db close];


FMDatabase - executeQuery 처리 결과를 FMResultSet 인스턴스로 반환한다.

FMResultSet - next를 호출하여 가져온 행을 순차적으로 불러온다. 행이 있는 경우 YES, 끝이면 NO를 반환하므로 이 메소드를 호출하는 while 조건문을 통해서 모든 행이 열거된다.



8. 트랜잭션

SQLite는 암묵적으로 트랜잭션 제어를 한다. 평상시에는 문제가 없지만 대량의 INSERT를 실행하면 그때마다 BEGIN ~ COMMIT이 실행되기 때문에 속도가 크게 저하된다. 이러한 작업을 수행하려면 대상 구간을 명시적으로 트랜잭션 처리하는 것이 좋다.


FMDB가 명시적으로 트랜잭션 FMDatabase - beginTransaction 메서드에서 시작되고 commit으로 종료된다. 오류 등으로 처리 전의 상태로 되돌리려면 rollback 메소드를 실행한다.


NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *dir = [paths objectAtIndex : 0];
FMDatabase *db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"app.db"]];

NSString *sql = @"SELECT id, name FROM users;";
[db open];

FMResultSet *results = [db executeQuery:sql];
NSMutableArray *users = [NSMutableArray array];

while ([results next])
{
      User *user = [[User alloc] init];
      user.userId = [results intForColumnIndex:0];
      user.name = [results stringForColumnIndex:2];
      NSLog(@"%@", user.name);
      [users addObject:user];
}

[db close];


// Transaction
sql = @"INSERT INTO users (name) VALUES (?)";

[db open];
[db beginTransaction];

BOOL isSucceeded = YES;
for( User* user in users )
{
      NSLog(@"name:%@", user.name);
      if( ![db executeUpdate:sql, user.name] )
      {
isSucceeded = NO;
break;
      }
}

if( isSucceeded )
{
      [db commit];
}
else
{
      [db rollback];
}

[db close];

명시적 트랜잭션을 실행하는 동안에는 데이터베이스 전체가 잠긴다. 따라서 구간 내에서 새로운 데이터베이스를 open하면 응답 없음으로 간주한다.



9. 형식

FMDB를 이용하여 데이터를 취득, 설정하는 형식에 대한 정리.


데이터를 취득하기 위해서는 FMDatabase - executeQuery의 결과로 반환된 FMResultSet 인스턴스 메소드를 사용한다. 메소드 이름은 typeForColumn 또는 typeForColumnIndex 다. 각 컬럼 이름과 위치 인덱스 값을 얻을 수 있다.


예를 들어, stringForColumn에 "name"을 지정했다면 name 문자열(TEXT)의 컬럼을 가져온다. intForColumnIndex:4를 지정하면 SELECT 문을 검색할 4 번째 정수(INTEGER)를 얻을 수 있다. 대표적인 데이터 형식 대응은 아래와 같다.


SQLite

Objective-C 

검색 메소드 

 TEXT

NSString 

stringForColumn, stringForColumnIndex 

INTEGER 

int 

intForColumn, intForColumnIndex 

BOOL 

BOOL 

boolForColumn, boolForColumnIndex 

REAL 

double 

doubleForColumn, doubleForColumnIndex 

DATETIME (INTEGER, REAL, TEXT)

NSDate 

dateForColumn, dateForColumnIndex 

BLOB 

NSData 

dataForColumn, dataForColumnIndex 


FMDatabase - executeQuery로 지정하는 매개변수는 런타임에 유형검사를 한다. TEXT면 NSString, INTEGER면 NSNumber로 대응하며 잘 못된 형식을 지정하면 런타임 에러를 발생시킨다.


데이터 취득의 경우,  FMResultSet이 Objective-C의 변환을 담당하지만 설정시에 변환은 직접 해야한다. 변환 대응은 아래와 같다.


 SQLite

Objective-C 

변환 방법 

 TEXT

NSString 

변환 불필요 

INTEGER 

NSInteger 

NSNumber - numberWithInt 

BOOL 

BOOL 

NSNumber - numberWithBool 

REAL 

double 

NSNumber - numberWithDouble 

DATETIME (INTEGER, REAL, TEXT) 

NSDate 

변환 불필요 

BLOB 

NSData 

변환 불필요. 


SQLite의 날짜 형식은 다음과 같이 정의되어 있다. (Datatypes In SQLite Version 3 에서 인용).


TEXT as ISO8601 strings (“YYYY-MM-DD HH:MM:SS.SSS”).

REAL as Julian day numbers, the number of days since noon in Greenwich on November 24, 4714 B.C. according to the proleptic Gregorian calendar.

INTEGER as Unix Time, the number of seconds since 1970-01-01 00:00:00 UTC.


DATETIME은 고유의 형식이 있는 것은 아니고 TEXT, REAL, INTEGER를 사용한다. SQLite 클라이언트는 TEXT 밖에 허용하지 않으며 System.Data.SQLite과 같이 연결 문자열 DATETIME 형식의 해석 방법을 선택할 수 있다. 

FMDB의 경우 내부적으로 Unix Timestamp에서 운용하고 있으므로 형식을 정의할 때 INTEGER로 하면 좋다. 



10. Lita(데이터베이스 관리 프로그램)

FMDB를 이용한 데이터베이스 작업을 했을 때 그 결과가 어떻게 반영되었는지 확인하고 싶을 때가 있다. 또한 애플리케이션에서 사용하는 SQL 문을 검토하기 위해 실제 데이터베이스에 대해 SQL을 실행하고 싶은 경우도 많다. 이럴 때 사용하면 유용한 AIR 애플리케이션이 바로 Lita 다.


Lita – SQLite Administration Tool


iPhone 시뮬레이터를 통해 SQLite 데이터베이스를 생성한 경우, Mac의 다음 위치에 파일이 생성된다.

/Users/사용자이름/Library/Application Support/iPhone Simulator/5.0/Applications/애플리케이션 ID/Documents


만들어진 파일을 Lita에서 열고 애플리케이션이 데이터베이스 작업을 행한 후, Lita에서 갱신하면 저장된 데이터의 변경을 쉽게 할 수 있다. 





    

설정

트랙백

댓글