콘텐츠로 건너뛰기
Home » Swift에서 SQLite3 사용하기

Swift에서 SQLite3 사용하기

SQLite3는 C/C++API를 제공하고 있으며, 사용하기도 그리 어렵지 않다. Swift에서 SQLite3를 사용하기 위한 가장 간단한 방법은 Obejctive-C로 DB를 액세스하는 함수나 클래스를 작성하고, Xcode 프로젝트에서 이 클래스를 추가해 Swift에서 사용하는 방법이다. 특히 코코아 클래스들과 Swift 타입들 간에는 바로 브릿징되면서 자동으로 변환되는 것들이 있기 때문에 사용하기에 편리한 점은 있지만, Objective-C에 익숙하지 않거나, 혹은 그냥 아무 이유없이 C API와의 연동을 해보고 싶은 경우가 있을 수 있기에 방법을 소개한다.
SQLite3의 C API와 연동하는 방법에 대해서는 별도의 포스팅으로 내용을 분리하였다.
이 글에서는 C API를 직접 사용하면서 쿼리를 실행할 수 있는 클래스를 하나 작성해보도록 하겠다. 먼저 클래스 내에서 별도로 사용할 enum 타입들을 정의한다. 하나는 에러들이고, 다른 하나는 칼럼의 타입을 구분하기 위한 값으로 쓴다.

class SQLite {
  enum SQLError: Error {
    case connectionError
    case queryError
    case otherError
  }
  enum ColumnType {
    case int
    case double
    case text
  }
...
}

데이터베이스를 연결하고, 쿼리를 저장할 두 개의 객체 포인터가 필요하다. 그리고 파일 경로의 경우에는 바꾸는 사람 마음이겠지만, 간단히 앱 라이브러리 디렉토리에 저장되도록 하겠다.

var db: OpaquePointer?
var stmt: OpaquePointer?
let path: String = {
  let fm = FileManager.default
  return fm.urls(for:.libraryDirectory, in:.userDomainMask).last!
           .appendingPathComponent("db.sqlite").path
}()

이 클래스는 초기화 될 때 자동으로 파일을 찾아서 열고 쿼리를 받을 준비를 하도록 한다. 또 제거될 때 DB를 닫도록한다.

init() throws {
  if sqlite3_open(path, &db) == SQLITE_OK {
    return 
   }
   throw SQLError.connectionError
}
deinit {
  sqlite3_finalize(stmt)
  sqlite3_close(db)
}

이전의 예들에서는 간단하게 고정된 스키마의 테이블을 쓰면서 정수값 하나를 읽고, 쓰고 하는 예를 보였는데 여기에서는 필요할 때마다 쿼리를 인스톨하고 (또 필요하면 바인딩하고) 쿼리를 실행하도록 해보자.

func install(query: String) throws {
  sqlite3_stmt(stmt); stmt = nil
  if sqlite3_prepare_v2(db, query, -1, &stmt, nil) != SQLITE_OK {
    throw SQLError.queryError
  }
}

쿼리의 내용에 맞게 바인딩을 할 수 있게 해준다. 바인딩되는 칼럼의 순서와 타입은 쓰는 사람이 정확하게 쓰는 수 밖에…

func bind(data:Any, withType type:ColumnType at col:Int32 = 1) {
  switch type {
  case .int:
    if let value = data as? Int {
      sqlite3_bind_int(stmt, col, Int(value))
     }
  case .double:
    if let value = data as? Double {
      sqlite3_bind_double(stmt, col, value)
    }
  case .text:
    if let value = data as? String {
      sqlite3_bind_text(stmt, col, value, -1, nil)
    }
}

다음은 쿼리를 실행하는 함수인데, 이 결과는 SQLITE_DONE이거나 SQLITE_ROW일 것이다. (그 외에는 뭔가 에러가 발생했다는 의미가 될테고) 따라서 이 두 값에 따라서 DONE이면 실행을 종료하고, ROW이면 읽어온 row가 하나 있으니 그것을 핸들러로 처리하도록 한다. 사실 x 번째 칼럼에서 y 타입의 값을 얻어오는 메소드를 만드는 것도 나쁘지 않을 것 같기는 하다.

func execute(rowHanlder:((OpaquePointer) -> Void)? = nil) throws {
  while true {
    switch sqlite3_step(stmt) {
    case SQLITE_DONE: return
    case SQLITE_ROW: rowHandler?(stmt!)
    default:
      throw SQLError.otherError
    }
  }
}

최종적으로 테스트 코드이다.

do {
    let db = try SQLite()
    // 테이블 생성
    try db.install(query:"CREATE TABLE IF NOT EXISTS test (num INTEGER)")
    try db.execute()
    // 데이터를 삽입해보자.
    for i in 10..<20 {
        try db.install(query:"INSERT INTO test VALUES (?)")
        db.bind(data: i, withType: .int)
        try db.execute()
    }
    /// 조회
    try db.install(query:"select * from test")
    try db.execute(){ stmt in
        let n = sqlite3_column_int(stmt, 0)
        print(n)
    }
} catch {
    print(error)
}

다음은 전체 코드를 하나로 묶은 Gist이다.


import Foundation
class SQLite {
enum SQLError: Error {
case connectionError
case queryError
case otherError
}
enum ColumnType {
case int
case double
case text
}
var db: OpaquePointer?
var stmt: OpaquePointer?
let path: String = {
let fm = FileManager.default
return fm.urls(for:.libraryDirectory, in:.userDomainMask).last!
.appendingPathComponent("data.db").path
}()
init() throws {
guard sqlite3_open(path, &db) == SQLITE_OK
else {
throw SQLError.connectionError
}
}
func install(query: String) throws {
sqlite3_finalize(stmt)
stmt = nil
if sqlite3_prepare_v2(db, query, -1, &stmt, nil) == SQLITE_OK {
return
}
throw SQLError.queryError
}
func bind(data:Any, withType type: ColumnType, at col: Int32 = 1) {
switch type {
case .int:
if let value = data as? Int {
sqlite3_bind_int(stmt, col, Int32(value))
}
case .double:
if let value = data as? Double {
sqlite3_bind_double(stmt, col, value)
}
case .text:
if let value = data as? String {
sqlite3_bind_text(stmt, col, value, -1, nil)
}
}
}
func execute(rowHandler:((OpaquePointer) -> Void)? = nil) throws {
while true {
switch sqlite3_step(stmt) {
case SQLITE_DONE:
return
case SQLITE_ROW:
rowHandler?(stmt!)
default:
throw SQLError.otherError
}
}
}
deinit {
sqlite3_finalize(stmt)
sqlite3_close(db)
}
}
do {
let db = try SQLite()
// 테이블 생성
try db.install(query:"CREATE TABLE IF NOT EXISTS test (num INTEGER)")
try db.execute()
for i in 10..<20 {
try db.install(query:"INSERT INTO test VALUES (?)")
db.bind(data: i, withType: .int)
try db.execute()
}
/// 조회
try db.install(query:"select * from test")
try db.execute(){ stmt in
let n = sqlite3_column_int(stmt, 0)
print(n)
}
} catch {
print(error)
}

view raw

SQLite3.swift

hosted with ❤ by GitHub