iOS에서 SQLite 사용방법

관련하여 새롭게 작성된 글이 있으니, 이 글을 읽어주세요: [새로 작성된] iOS에서 SQLite3 사용방법

자주 쓰이지는 않지만, 현재로서는 미리 만들어 놓은 데이터를 검색하여 사용하는 가장 단순한(?) 방법이다. (코어데이터는 데이터 셋을 미리 만들어 사용하기가 까다롭다) 대신, 애플은 영구저장소를 활용하는 방법으로 코어데이터를 밀고 있기 때문에 SQL과 관련한 내용을 애플 개발자 문서에서 친절하게 소개하고 있는 자료는 좀 드물다. 대신 SQL의 C인터페이스를 설명하는 글은 인터넷에서 많이 있으므로 적절하게 찾아보면 된다.

SQLite3를 사용하기

SQLite를 사용하기 위해서는 SQLite3 프레임워크를 프로젝트에 포함시켜 줘야 한다. 프레임워크 중에는 sqlite3 이 있고 sqlite3.0 이 있는데 sqlite3을 선택해야 한다.

데이터 베이스를 액세스해서 자료를 읽어오거나 혹은 새로운 값을 쓰는 부분은 “Model”에 해당하므로, 별도의 클래스에서 이 처리를 담당하도록 하는 것이 좋다. 즉 해당 클래스는 데이터베이스 파일에 대한 인터페이스로 기능하면서 MVC에서는 Model을 담당하게 하는 것이다.

예제 – DBInterface.m

DBInterface 클래스는 SQLite3 DB 저장소를 액세스하는 클래스의 템플릿으로, DB 액세스에 대한 내용을 설명하면서 하나 하나 구성해 가는 과정을 살펴보도록 하겠다. 먼저 내부 인터페이스에서는 sqlite3 객체하나와 DB 파일의 경로를 담을 문자열 프로퍼티를 하나 선언한다.

#import "DBInterface.h"

#define aDB_FILENAME @"database.sqlite"

@interface DBInterface()
{
    sqlite3 *myDB;
}
@property (readonly, nonatomic) NSString *dbFilePath
@end

DB 파일의 경로 구하기 (및 DB 파일 복사)

DB파일의 경로를 구하고자 할 때 DB파일의 위치를 구하면 된다. 만약 사전과 같이 DB에 있는 내용을 읽어오기만 하면 된다면 번들에 포함될 DB 파일을 샌드박스의 사용자 문서 폴더로 복사해줄 필요가 없다. 하지만 DB 파일에 내용을 추가로 쓰는 기능을 수행해야 한다면 이 파일을 사용자 문서 폴더로 복사한다.

물론 이 내용은 init 메소드에서 처리해주어도 상관없다.

@systhesize dbFilePath = _dbFilePath;

-(NSString *)dbFilePath
{
    if(!_dbFilePath) {
        NSString *databaseSourcePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:aDB_FILENAME];
        NSString *docPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:aDB_FILENAME];
        NSFileManager *fileManager = [NSFileManager defaultManager];
        if (![fileManager fileExistsAtPath:docPath])
            [fileManager copyItemAtPath:databaseSourcePath toPath:docPath error:nil];
        _dbFilePath = docPath;
    }
    return _dbFilePath;
}

참고로 이 예제에서 사용할 테이블의 이름은 WORD 이고 이 테이블은 id, name, description 의 칼럼을 가지고 있다고 가정한다. id는 정수값, 나머지는 모두 텍스트 정보를 담는 테이블이다.

DB에서 검색하기

이 테이블에서 특정한 name을 검색하는 함수를 작성해보자. SELECT 구문을 사용하는 메소드가 될 것이다. 이를 위해서는 다음의 단계를 따른다.

  1. DB를 연다.
  2. 쿼리문을 작성한다.
  3. SQL 포인터를 준비한다.
  4. 검색 결과에 따라 각 ROW에 대해 루프를 돌린다.
  5. 루프 내에서 한 레코드에 대해 각 필드값을 추출해 와 이를 사전 객체에 담고, 반환을 위한 배열에 추가한다.
  6. SQL 포인터를 종료한다. (finalize)
  7. DB 연결을 닫는다.

    -(NSArray *)serachWithKeyword:(NSString *)keyword
    {
    NSMutableArray *result = [NSMutableArray array];
    sqlite3_stmt *stmt;
    NSString *queryString = [NSString stringWithFormat:@”SELECT * FROM WORD WHERE NAME LIKE “%@%%””,keyword];
    const char *dbPath = [self.dbFilePath UTF8String];

    if (sqlite3_open(dbPath, &myDB)==SQLITE_OK) {
        const char *sql = [queryString UTF8String];
        if (sqlite3_prepare_v2(myDB, sql, -1, &stmt, NULL)==SQLITE_OK) {
            while(sqlite3_step(stmt)==SQLITE_ROW) {
                NSMutableDictionary *anItem = [NSMutableDictionary dictionary];
                NSNumber *indexNumber = [NSNumber numberWithInt:sqlite3_column_int(stmt,0)];
                NSString *name = [NSString stringWithUTF8String:(const char*)sqlite3_column_text(stmt,1)];
                NSString *description = [NSString stringWithUTF8String:(const char *)sqlite3_column_text(stmt,2)];
                [anItem setValue:indexNumber forKey:@"indexNumber"];
                [anItem setValue:name forKey:@"name"];
                [anItem setValue:description forKey:@"description"];
                [result addObject:anItem];
            }
        }
        sqlite3_finalize(stmt);
    }
    sqlite_close(myDB)
    
        return (NSArray *)result;
    

    }

비록 코드는 더럽게 길었지만, 몇 가지 sqlite3의 C 인터페이스에 정의된 함수들과 위에서 설명한 절차만 기억한다면 크게 어렵지 않게 구현할 수 있는 내용이라 하겠다.

업데이트 하기

특정 id의 데이터를 업데이트하는 과정은 select와는 조금 다른데, 쿼리 문자열에 데이터값들을 바인드하는 과정이 추가된다. (물론 그냥 쿼리문 자체에 넣어버리는 방법도 있는데, 만약 원격 DB를 사용하는 경우라면 보안을 위해 바인드하는 것을 추천)

-(void)updateRecordWithName:(NSString *)name description:(NSString *)description atIndex:(int)indexNumber
{
    sqlite3_stmt *stmt;
    NSString *queryString = @"UPDATE WORD SET NAME=?, DESCRIPTION=? WHERE ID=?";
    const char *dbPath = [self.dbFilePath UTF8String];
    if(sqlite3_open(dbPath,&myDB)==SQLITE_OK) {
        const char *sql = [queryString UTF8String];
        if(sqlite3_prepare_v2(myDB, sql, -1, &stmt, NULL)==SQLITE_OK) {
            sqlite3_bind_text(stmt,1,[name UTF8String],-1,NULL);
            sqlite3_bind_text(stmt,2,[description UTF8String],-1,NULL];
            sqlite3_bind_int(stmt,3,indexNumber);
            if(!sqlite3_step(stmt)==SQLITE_DONE){
                NSLog(@"fail to update");
            }
        }
        sqlite3_finalize(stmt);
    }
    sqlite3_close(myDB);
}

실제 레코드의 업데이트가 일어나는 부분은 sqlite3_step() 함수가 호출되는 시점이고, 이 때 성공 여부를 판별하는 값이 SQLITE_DONE 이라는 점만 감안한다면 충분히 insert / delete 의 경우에 대해서도 메소드를 작성하는 것이 그리 어렵지 않을 것이라 생각된다.

참고로 SQLite는 경량 데이터베이스이며 상당히 속도도 빠른 편이지만 DB 자체의 크기가 매우 커지는 경우 그 속도가 현저하게 떨어질 수 있다. 따라서 DB를 액세스 하는 경우에는 호출해주는 쪽에서  GCD 등을 사용해서 UI의 블락 현상을 방지해줄 필요가 있다.