SQLite C API 소개 본문

자료

SQLite C API 소개

keydisk 2010. 9. 29. 16:17

원본 출처 : http://jeminency.egloos.com/3325961

꽤 간단한 내용이지만, SQLite는 한글 매뉴얼이 별로 없는 탓에 끄적여 봅니다.

그간 C API를 써 본 DB들을 보면, ODBC와 비슷한 형태를 갖고 있는 경우가 많습니다. 오라클은 C API보다는 Pro*C를 쓰니 예외로 하고요.. 오라클 C API가 어떻게 되어 있는 지도 모르겠네요 -_-;

대략 Open-Prepare-Bind-Execute-(Fetch)-Close의 과정을 거치는데 open-close는 당연히 DB를 열고 핸들러(DB 정보 구조체)를 초기화하는 것과 종료하는 것이니 어려울 것 없고요.

Prepare는 DB에게 자기가 실행할 SQL 문장을 던져주고 미리 실행 플랜을 준비하도록 합니다. binding 할 값이 없다면 prepare 하지 않고 곧바로 execute도 가능합니다만, 예를 들어 문장은 동일한데 값만 바뀌는 쿼리를 여러 번 실행할 경우에 다이렉트로 그 때마다 실행을 한다면, 매번 DB 엔진은 SQL 문장을 해석하고 어떻게 실행할 지를 결정하고, 값에 따라 실행하게 되겠죠. 값만 달라진다면, 문장을 해석하고 실행할 지 결정하는 부분은 사실 한 번만 해도 되는데 말이죠.

그리고 bind는 실행하기 전 쿼리에 값을 할당하는 것입니다. 바인딩할 부분은 보통 '?'으로 표현하는데 'SELECT * FROM test WHERE id = ?' 이런 식으로 쿼리 문장을 표현할 수 있습니다. 이 문장 그대로 prepare를 딱 한 번만 한 다음 실행할 때는 '?'위치에 integer 변수를 binding 하여 여러 가지로 id 값을 주어서 결과를 가져올 수 있습니다.

Execute는 말 그대로 문장을 실행하는 것인데 DB에서는 select 쿼리가 실행 되었을 경우 result set 공간에 결과값을 저장해 놓습니다.
Fetch는 그런 result set의 값을 가져오는 과정입니다. insert-update-delete를 실행했을 경우는 당연히 필요없는 과정이죠.

상당히 대충 설명했는데 -_- 직접 예를 보겠습니다.
testtbl의 스키마는 'CREATE TABLE testtbl(id int, value text);' 입니다.

예제 프로그램은 id를 1부터 3까지 돌리며 해당되는 레코드를 긁어오는 것입니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sqlite3.h>

#define EXIT_WITH_ERROR(func, dbh) \
do { \
    if (func != SQLITE_OK) \
    { \
        fprintf(stderr, "line %d: %s \n: %s\n", __LINE__ \
            , #func ,sqlite3_errmsg(dbh)); \
        exit(sqlite3_errcode(dbh)); \
    } \
} while(0)

int main()
{
    sqlite3 *dbh;
    sqlite3_stmt *stmt;
    int i, j;
    const char *tail;
    char *errmsg;

    char *sql = "select * from testtbl where id=?";

    /* "testdb"라는 DB 파일을 연다 */
    EXIT_WITH_ERROR(sqlite3_open("testdb", &dbh), dbh);

    /* sql 문장에 대해 질의 플랜을 준비한다 */
    EXIT_WITH_ERROR(sqlite3_prepare(dbh, sql, strlen(sql), &stmt, NULL, dbh);

    for (i=1; i<=3; i++)
    {
        /* '?' 위치에 i 값을 바인딩한다 */
        EXIT_WITH_ERROR(sqlite3_bind_int(stmt, 1, i) , dbh);

        /* sql 쿼리 실행 */
        while (sqlite3_step(stmt) != SQLITE_DONE)
        {
            /* 실행된 결과값을 받아와 출력 */
            printf("%d : %s\n" , sqlite3_column_int(stmt, 0)
                , sqlite3_column_text(stmt, 1));
        }

        sqlite3_reset(stmt);
    }
    sqlite3_finalize(stmt);
}


EXIT_WITH_ERROR는 에러 체크를 위해 제가 작성한 매크로입니다. 실제 sqlite3_ 로 시작하는 함수들만 보시면 됩니다.

처음에 DB 오픈을 하고 sql 문장에 대해 prepare를 수행하죠?
prepare의 마지막 NULL 인자는 원래 char *가 들어가는 곳입니다. sql 문장이 하나가 아니라 여러 개일 경우는 첫번째 것만 prepare하고 마지막 인자의 포인터 변수에 두번째 문장이 시작하는 위치를 가리키도록 되어 있습니다.
순차적으로 수행되어야 할 sql 문이라면 변수를 따로 안 두고 실행 단위별로 sql 문을 선언하여 쓸 수 있다는 장점이 있습니다(자세한 설명은 sqlite3_prepare()의 매뉴얼 페이지를..).

그 다음에는 for 문을 돌며 바인딩-실행-결과 페치를 반복합니다.
바인딩은 for 문의 변수인 i를 그대로 이용합니다. 바인딩 함수의 두번째 인자는 binding variable의 순서입니다. 첫번째니까 '1'이죠. 여러 개 있으면 앞에서부터 1,2,3,..이 됩니다. 숫자이므로 sqlite3_bind_int를 썼고, 그 외에 _null, _text 등 다양한 바인딩 함수가 있습니다.

그리고 sqlite3_step()을 써서 실행을 하는데 특이한 점은 이 함수가 실행과 페치를 동시에 수행한다는 점입니다.

예를 들어 MySQL이라면, mysql_query()로 실행하고, mysql_store_result()로 결과 셋을 가져 온 후, mysql_fetch_row()을 반복 실행하여 한 건씩 긁어오는 과정을 거칩니다.

반면 SQLite는 sqlite3_step을 실행하면 실행하고 결과 셋의 첫번째 레코드를 가져오는 과정까지 거칩니다.
그 다음은 각 컬럼을 sqlite3_column_* 함수들을 써서 가져오면 됩니다(컬럼 번호는 0부터 시작합니다).
그리고, 두번째 이후로 sqlite3_step()을 실행하면 sql을 실행하지는 않고 결과 셋의 다음 레코드들을 가져오게 되는 것이죠. 만약 가져올 결과 레코드가 없으면 SQLITE_DONE을 리턴합니다.
즉, 만약 결과가 실행할 때마다 한 건씩만 있다면 굳이 while 문을 돌릴 필요는 없습니다. 실행 결과는 다음과 같습니다.

1 : aaa
1 : ddd
2 : bbb
3 : ccc

만약 while 문을 쓰지 않았다면 결과에는 1:ddd가 나오지 않을 것입니다. id 컬럼이 unique 하다면 while문을 쓰지 않아도 되겠지만, 방어적인 프로그래밍을 위해서는 while을 사용하는 스타일이 나을 듯 합니다 ^^

그리고 결과셋을 다 긁어왔다면 sqlite3_reset을 실행해서 바인딩 된 변수들을 초기화 해 주어야 합니다.
그렇지 않을 경우 두번째 bind를 할 때에 에러가 납니다.

이상으로 간단히 SQLite의 C API에 대해 살펴 보았습니다, 간단하죠? ^^
개인적으로는 다른 DB에 비해 비교적 API 사용법이 쉽다는 느낌이 듭니다. DB 데몬이 없기 때문에 API를 더욱 간략하게 만들 수 있었는 지도 모르겠습니다.

하지만 C를 쓸 경우는 아무래도 다른 언어보다는 조금 번거롭죠.
pysqlite 같은 걸 쓸 경우는 훨씬 편합니다. 이후에는 C언어 이외의 SQLite API에 대해 한 번 보도록 하겠습니다.