태그 보관물: sqlite3

(Swift3) swift3 관점에서의 sqlite3 API 분석 – C-API 사용법 심화

Swift에서 sqlite3 사용하기에 대한 글을 몇 편 작성했었는데, 이 글에서는 C API를 사용하는데 있어서 관련되는 Swift 타입들과, 실제 C/C++ API가 어떤식으로 변환되어 Swift 영역으로 들어오는지에 대해 좀 더 자세하고 깊이 들여다보도록 하겠다.

sqlite3_open

먼저 데이터베이스 파일을 열고 커넥션을 만드는 부분부터 시작하자. 모든 작업의 시작점이 될 sqlite3_open() 함수의 원형1은 다음과 같다. 파일경로를 받아서 연결을 생성한 후 연결 핸들러를 같이 전달받은 ppDB에 넣어준다. 이 때 ppDB의 타입은 sqlite3인데, 이는 C 구조체로되어 있다.2

int sqlite3_open(
    const char *filename,
     sqlite3 **ppDB
);

//...
typedef struct sqlite3 sqlite3;

ppDB의 타입 자체는 Swift가 알 수 없는 타입으로 들어온다. (아무래도 헤더파일이 아닌 구현부쪽에 정의된 타입으로 보인다) 따라서 *ppDB 라는 포인터는 UnsafePointer<T> 타입이 아닌OpaquePointer로 반입된다. 따라서 sqlite3 ** 타입은 UnsafeMutablePointer<OpaquePointer!>가 될 것이고 따라서 sqlite3_open()의 Swift 타입은 아래와 같이 변형될 것이다.

func sqlite3_open(_ filename: String, _ ppdb: UnsafeMutablePointer<OpaquePointer!>!) -> Int32

따라서 실제 사용시에는 다음과 같은 모양이 된다. 아래 코드는 사용자의 문서 폴더에 data.db 파일을 생성하고 이를 연결한다. 이때 Swift3에서 변형된 API에 맞게, FileManager, NSURL 관련된 코드도 좀 더 다듬어졌다.

let path = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask)
            .last!.appendingPathComponent("data.db").path
var db: OpaquePointer? = nil // inout 으로 넘겨지기 때문에 var로 선언해야 한다.
if sqlite3_open(path, &db) == SQLITE_OK { /// 만약 함수 내에서 이렇게 쓴다면, guard 문을 쓰는 편이 낫겠지?
  print("db connected.")
} else {
  print("failed")
}

여담인데, 만약 sqilte3 구조체가 Swift로 들여올 수 있는 구조체라면, OpaquePointer 대신에 UnsafePointer 계열을 쓸 수 있다. 다음은 그렇다는 가정하에 어떻게 sqlite3_open()을 열 수 있는가 하는 예를 보여준다. 3

var db = UnsafeMutablePointer<sqlite3>.allocate(capacity: 1)
if sqlite3_open(path, &db) == SQLITE_OK {
  ...
}

dbUnsafeMutablePointer<sqlite3>으로 선언했고, 이는 C에서 sqlite* 포인터 타입에 해당된다. 이 때 넘겨지는 파라미터는 이중포인터 타입이므로 여기에 다시 &을 붙여서 이중포인터로 보낸다.

쿼리 컴파일 및 실행

문자열로 주어진 쿼리는 sqlite3 내부에서 컴파일되며, 컴파일 성공 시에 쿼리를 실행하게 된다. 쿼리를 컴파일한 결과는 sqlite3_stmt라는 구조체에 저장된다. 4

그리고 이 원형은 아래와 같다.

int sqlite3_prepare(
    sqlite3 *db, // 데이터베이스 연결 핸들러
    const char *zSql, // 쿼리
    int nBytes, // 쿼리의 최대 길이, 모르겠으면 그냥 -1
    sqlite3_stmt **ppStmt, // stmt 객체 포인터
    const char **pzTail // 쿼리의 남은 길이. 사용되지 않으므로 NULL 을 쓴다.
);

비슷하게 다음과 같이 번역된다.

func sqlite3_prepare(_ db: OpaquePointer!, _ zSql: String, 
                  _ nByte: Int32, _ ppStmt: UnsafeMutablePointer<OpaquePointer>,
                    _ pzTail: UnsafePointer<String>) -> Int32

따라서 쿼리를 컴파일 하는 과정은 다음과 같이 사용한다.

/// usage of sqlite3_prepare

var stmt: OpaquePointer? = nil
do {
  defer{sqlite3_finalize(stmt)}
  let query = "CREATE TABLE IF NOT EXISTS test ( num INT );"
  if sqlite3_prepare(db, query, -1, &stmt, nil) == SQLITE_OK {
    /// ... 쿼리 실행...
  } else {
    print("There is an error in your query.")
  }
}

단일 쿼리를 실행하는 것은 sqlite3_step으로 이루어진다. 이는 SQLITE_ROWSQLITE_DONE을 리턴하는데, _ROW의 경우에는 SELECT 문에 의해서 가져온 결과가 있다는 의미이며, _DONE은 모든 쿼리 실행이 완료되었다는 의미이다.

/// usage of sqlite3_prepare

var stmt: OpaquePointer? = nil
do {
  defer{sqlite3_finalize(stmt)}
  let query = "CREATE TABLE IF NOT EXISTS test ( num INT );"
  if sqlite3_prepare(db, query, -1, &stmt, nil) == SQLITE_OK {
    if sqlite3_step(stmt) == SQLITE_DONE {
      print("table created")
    } else {
      print("failed to create table.")
    }
  } else {
    print("There is an error in your query.")
  }
}

insert 액션

insert, update, delete 등은 한 회당 한 번씩 실행한다. 보통 이 때는 쿼리 문자열 내에 값을 넣는 것보다 보안상으로는 쿼리를 컴파일한 후에 값을 바인딩하는 것이 안전하다고 한다. 이 때 사용하는 함수들이 sqlite3_bind_* 함수들이다. 이는 각각의 칼럼5에 넣을 값을 바인딩해준다. 쿼리 바인딩을 위해서는 컴파일 시에 sqlite3_prepare 말고 sqlite3_prepare_v2를 쓴다. 쿼리에서 바인딩될 값은 ? 문자로 표시한다.

대표적으로 sqlite3_bind_int, sqlite3_bind_text를 보도록 하자.

int sqlite3_bind_int(
  sqlite3_stmt*, // 컴파일된 구문객체
  int column, // 쿼리에서 나타나는 ?의 순서. 1부터 시작한다. 
  int vaule // 바인딩할 값
);

int sqlite3_bind_text(
  sqlite3_stmt*, // 컴파일된 구문 객체
  int, // 순서
  const char*, // 문자열 시작점
  int, // 문자열의 길이
  void(*)(void*) // 문자열이나 BLOB 객체를 처리하는 함수포인터
);

칼럼 순서가 1부터 시작한다는 부분에 주의해야 한다. 참고로 C의 경우 동적으로 할당한 메모리에 문자열을 복사하고 바인딩한 후에 문자열을 폐기할 수 있는 함수 포인터를 같이 넣어줄 수 있다. (이 때 폐기 함수는 바인딩에 실패해도 동작한다.) 바인딩 완료후 직접해도 되는 부분이라 굳이 쓸 필요는 없으며, 어차피 Swift는 문자열 데이터를 알아서 관리해주니까 그냥 nil 을 주면 된다.

이들의 Swift 컨버전은 다음과 같을 것이다. 6

func sqlite3_bind_int(_ stmt: OpaquePointer!, _ column : Int32, _ value: Int32) -> Int
func sqlite3_bind_text(_ stmt: OpaquePointer!, _ column: Int32,
                    _ value: String, _ length: Int32,
                    _ disposer: (UnsafePointer<Void>) -> Void)
)

이번에는 그러면 값을 삽입하는 과정을 살펴보자.

let query = "INSERT INTO test (num) VALUES (?);"
let n: Int32 = 52
var stmt: OpaquePointer? = nil
if sqlite3_prepare_v2(db, query, -1, &stmt, nil) == SQLITE_OK {
  if sqlite3_bind_int(stmt, 1, n) == SQLITE_OK {
    if sqlite3_step(stmt) == SQLITE_DONE {
      print("completed.")
    }
  } else {
    print("can't bind value")
  }
} else {
  print("can't compile query")
}
sqlite3_finalize(stmt)

값을 불러오는 과정은 다음과 같다. SELECT 쿼리와 같이 리턴 값이 있는 경우 sqlite3_step() 함수는 결과를 SQLITE_ROW로 표시한다. 그러면 결과로 불려온 Row가 있다는 뜻이며 해당 row의 각 칼럼 값을 얻어내기 위해서는 바인딩때와 유사하게 sqlite3_column_* 함수들을 쓴다. 다만 바인딩때와는 달리 구문객체와 칼럼 순서(역시 1부터 시작함)만 있으면 값을 얻어낼 수 있다.

let query = "SELECT num FROM test LIMIT ?"
let maxCount: Int32 = 10
var result = [Int32]()
if sqlite3_prepare_v2(db, query, -1, &stmt, nil) == SQLITE_OK {
  if sqlite3_bind_int(stmt, 1, maxCount) == SQLITE_OK {
    while sqlite3_step(stmt) == SQLITE_ROW {
      let q:Int32 = sqlite3_column_int(stmt, 1)
      result.append(q)
    }
  }
}
for i in result {
  print(i)
}


텍스트인 경우에는 const unsigned char* sqlite3_column_text() 함수를 쓰는데 보다 시피 리턴타입이 문자열 포인터 타입이고, 이는 Swift 에서 UnsafePointer<CChar> 가 된다. C문자열(const char*)로부터 문자열을 만들기 위해서는 String의 Foundation 확장이 필요하며 init(cString:), init?(cString:encoding:)을 쓸 수 있다.

var result = [String?]()
...
while sqlite3_step(stmt) == SQLITE_ROW {
  let strPtr = sqlite3_column_text(stamt, 1)
  let str = String(cString:strPtr, encoding:.utf8)
  result.append(str)
}
...
for s in result.flatMap{$0} {
  print(s)
}

이상으로 sqlite3의 C/C++ API를 Swift 관점에서 바라볼 때 어떤 식으로 반입되며 어떻게 상호작용하는 Swift 코드를 작성할 것인지에 대해 살펴보았다.


  1. https://www.sqlite.org/capi3ref.html#sqlite3_open 
  2. https://www.sqlite.org/capi3ref.html#sqlite3 
  3. 이중 포인터를 UnsafeMutablePointer<UnsafeMutablePointer<T>> 타입으로 만들어야 한단 말인가? 
  4. typedef struct sqlite3_stmt sqlite3_stmt https://www.sqlite.org/capi3ref.html#sqlite3_stmt 
  5. 1부터 시작한다. 
  6. 함수포인터의 경우, 일반적인 Swift 자유 함수나 클로저 혹은 nil을 넣을 수 있다. 참고 

[새로 작성된] iOS에서 SQLite3 사용방법

예전에 쓴 글이 있기는 하지만, 그냥 요리법처럼 쓴 글이기도 하거니와 소스코드에서 뭔가 글자가 빠지는 등(syntax highlighter를 안써야 겠지만 기존 글 고치기가 귀찮아…) 문제가 많아 내용을 보충해서 다시 작성.

애플은 SQLite3를 직접 인터페이스하는 것보다는 코어데이터를 사용하라고 권장하고 있고, (실제로 있다가 빠진 것인지는 알 수 없으나 그런 주장을 하는 사람들이 종종 있다) 애플 개발자 문서에서도 관련 내용을 내렸다고 한다. (하지만 이는 사실이 아닐 거라 생각한다. 왜냐면 iOS에서 SQLite3를 인터페이스 하는 부분은 전적으로 libsqlite3를 사용하는 것이고, 이에 대한 문서는 SQLite3 홈페이지에 가면 있기 때문이다) Continue reading “[새로 작성된] iOS에서 SQLite3 사용방법” »