SQLite3 CRUD 처리 테스트 환경: xcode 12.5, swift 5

 

SQLite3 로 local db를 생성하고 TABLE을 CRUD 해보자 

회사에서 프로그래밍을 하다가 토큰을 로컬 디비에 저장해야 하는 상황이 생겼습니다. 그래서 로컬 디비로 realm을 쓸지 sqlite3를 쓸지 고민하다가, realm이 빠르긴 하지만 굳이 용량을 더 늘려가며 빠른 작업을 선택하기 보단 차라리 가볍게 가자는 생각에 sqlite3로 local db를 CRUD 하게 되었습니다. 

SQLIte3 를 통한 CRUD는 모두 성공적으로 테스트를 마쳤고, 그 결과는 아래와 같습니다. 

create 버튼을 누르면 table이 생성되고, insert를 누르면 데이터가 입력됩니다. update를 누르면 데이터를 수정하여 보여주고, delete를 누르면 특정 레코드만 지워버립니다. drop은 table 자체를 삭제시키는 버튼입니다.

 

- 데이터 생성 전.

기본화면

 

- 데이터 생성 후(insert)

 

- update 시

 

- delete 누르면

 

- drop 버튼 누를 시

기본화면

 

본격적인 코드 작업

본격적으로 코드 작업을 시작해보겠습니다. 

먼저 DBHelper 라는 클래스를 하나 만들어줍니다. 

그리고 아래 세 변수를 선언하고, init()과 deint()도 구현해줍니다.

databaseName은 여러분이 하고싶은 것을 하면 됩니다.

말 그대로 db의 이름입니다.

항상 ~~.sqlite 형식을 맞춰줘야 합니다.

import Foundation
import SQLite3

class DBHelper {
    static let shared = DBHelper()
    
    var db : OpaquePointer? //db를 가리키는 포인터
    // db 이름은 항상 "DB이름.sqlite" 형식으로 해줄 것.
    let databaseName = "mydb.sqlite"
    
    
    
    init() {
        self.db = createDB()
    }

    deinit {
        sqlite3_close(db)
    }
    ...

 

 

다음은 createDB 함수입니다.

db를 생성하고, 성공적으로 생성되면 생성한 Db 포인터를 반환합니다.

OpaquePointer는 sqlite 전용 포인터입니다.

(한 인도 강사가 오파큐포인터라고 불렀던 게 생각납니다 ㅋㅋ)

 private func createDB() -> OpaquePointer? {
        var db: OpaquePointer? = nil
        do {
            let dbPath: String = try FileManager.default.url(
            for: .documentDirectory,
            in: .userDomainMask,
            appropriateFor: nil,
            create: false).appendingPathComponent(databaseName).path
            
            if sqlite3_open(dbPath, &db) == SQLITE_OK {
                print("Successfully created DB. Path: \(dbPath)")
                return db
            }
        } catch {
            print("Error while creating Database -\(error.localizedDescription)")
        }
        return nil
    }

 

여기까지 잘 따라했다면 이제 맨 처음에 공개한 사진의 버튼이 수행할 함수들을 하나씩 구현해봅시다.

 

테이블 만들기(createTable)

query는 myTable이라는 이름의 테이블이 db상에 존재하지 않으면

id(Integer, primary key, autoincrement), my_name(text not null), my_age(int) 

필드로 구성된 myTable을 생성하라는 뜻입니다.

autoincrement란 레코드가 생성될 때마다 하나씩 자동으로 올려주는 값을 말합니다. 반드시 integer 타입이어야 합니다.

func createTable(){
        // 아래 query의 뜻.
        // mytable이라는 table을 생성한다. 필드는
        // id(int, auto-increment primary key)
        // my_name(String not null)
        // my_age(Int)
        // 로 구성한다.
        // auto-increment 속성은 INTEGER에만 가능하다.
        let query = """
           CREATE TABLE IF NOT EXISTS myTable(
           id INTEGER PRIMARY KEY AUTOINCREMENT,
           my_name TEXT NOT NULL,
           my_age INT
           );
           """
        var statement: OpaquePointer? = nil
        
        if sqlite3_prepare_v2(self.db, query, -1, &statement, nil) == SQLITE_OK {
            if sqlite3_step(statement) == SQLITE_DONE {
                print("Creating table has been succesfully done. db: \(String(describing: self.db))")
                
            }
            else {
                let errorMessage = String(cString: sqlite3_errmsg(db))
                print("\nsqlte3_step failure while creating table: \(errorMessage)")
            }
        }
        else {
            let errorMessage = String(cString: sqlite3_errmsg(self.db))
            print("\nsqlite3_prepare failure while creating table: \(errorMessage)")
        }
        
        sqlite3_finalize(statement) // 메모리에서 sqlite3 할당 해제.
    }

prepare는 쿼리를 실행할 준비를 하는 단계이고, step은 쿼리를 실행하는 단계라고 이해하시면 되겠습니다.

 

데이터 입력하기(insertData)

id, my_name, my_age에 데이터를 넣어봅시다.

id는 굳이 안 넣어줄 것이지만 insertQuery에는 또 적어주긴 해야 합니다.

sqlite3_bind_text의 두 번째 인자는 values(?, ?, ?) 에서 몇 번째 ?에 넣을거냐를 지정합니다.

세 번째 인자는 들어갈 값이 되겠습니다.

 

    
    func insertData(name: String, age: Int) {
        // id 는 Auto increment 속성을 갖고 있기에 빼줌.
        let insertQuery = "insert into myTable (id, my_name, my_age) values (?, ?, ?);"
        var statement: OpaquePointer? = nil
        
        if sqlite3_prepare_v2(self.db, insertQuery, -1, &statement, nil) == SQLITE_OK {
            sqlite3_bind_text(statement, 2, name, -1, nil)
            sqlite3_bind_int(statement, 3, Int32(age))
            
        }
        else {
            print("sqlite binding failure")
        }
        
        if sqlite3_step(statement) == SQLITE_DONE {
            print("sqlite insertion success")
        }
        else {
            print("sqlite step failure")
        }
    }

 

값 읽기

일단 값을 읽기에 앞서 우리가 모델 하나를 미리 선언해뒀다고 칩시다.

모델은 다음과 같이 선언해뒀습니다.

struct MyModel: Codable {
    var id: Int
    var myName: String
    var myAge: Int?
   
}

 

아래는 readData() 입니다. 

data를 읽어서 MyModel 배열을 내보내고 있는데, 시도해본 결과 [MyModel]이 아닌 [MyModel]? 에 값을 담으려고 시도하면 값이 안 담깁니다. 정확히는 값을 반환을 못합니다. 그러니 꼭 Nil-Safety가 적용되지 않는 변수를 선언하고, 반환합시다.

그리고 sqlite3_column_text 및 sqlite3_column_int는 아래에서 쓰이는 것과 같이 써야 합니다. 특히 int의 경우 Int32형을 반환하기에 우리는 Int 타입으로 형변환을 해줄 필요가 있습니다.

   func readData() -> [MyModel] {
        let query: String = "select * from myTable;"
        var statement: OpaquePointer? = nil
        // 아래는 [MyModel]? 이 되면 값이 안 들어간다.
        // Nil을 인식하지 못하는 것으로..
        var result: [MyModel] = []

        if sqlite3_prepare(self.db, query, -1, &statement, nil) != SQLITE_OK {
            let errorMessage = String(cString: sqlite3_errmsg(db)!)
            print("error while prepare: \(errorMessage)")
            return result
        }
        while sqlite3_step(statement) == SQLITE_ROW {
            
            let id = sqlite3_column_int(statement, 0) // 결과의 0번째 테이블 값
            let name = String(cString: sqlite3_column_text(statement, 1)) // 결과의 1번째 테이블 값.
            let age = sqlite3_column_int(statement, 2) // 결과의 2번째 테이블 값.
            
            result.append(MyModel(id: Int(id), myName: String(name), myAge: Int(age)))
        }
        sqlite3_finalize(statement)
        
        return result
    }

 

값 수정

값을 수정하기 전에 오류메시지가 계속 반복되니, 이를 해결하기 위한 함수 하나만 추가적으로 만들어줍시다. 

    private func onSQLErrorPrintErrorMessage(_ db: OpaquePointer?) {
        let errorMessage = String(cString: sqlite3_errmsg(db))
        print("Error preparing update: \(errorMessage)")
        return
    }

 

값을 수정하는 코드는 아래와 같습니다. id, name, age를 인자로 받아서 id 에 해당하는 레코드의 name과 age를 받아온 인자로 변경합니다. 이 때, name 처럼 Text 형태를 가진 것들은 쿼리 문에서 작은 따옴표로 감싸주고 있음에 유의하셔야 합니다.

func updateData(id: Int, name: String, age: Int) {
        var statement: OpaquePointer?
        // 등호 기호는 =이 아니라 ==이다.
        // string 부분은 작은 따옴표 두 개로 감싸줘야 한다.
        let queryString = "UPDATE myTable SET my_name = '\(name)', my_age = \(age) WHERE id == \(id)"
        
        // 쿼리 준비.
        if sqlite3_prepare(db, queryString, -1, &statement, nil) != SQLITE_OK {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        // 쿼리 실행.
        if sqlite3_step(statement) != SQLITE_DONE {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        print("Update has been successfully done")
    }

 

값 삭제

값을 삭제하는 건 값을 수정하는 것과 매우 비슷합니다.

id만 인자로 받아서 ID에 해당하는 레코드만 삭제해봅시다.

    func deleteTable(tableName: String) {
        let queryString = "DROP TABLE \(tableName)"
        var statement: OpaquePointer?
        
        if sqlite3_prepare(db, queryString, -1, &statement, nil) != SQLITE_OK {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        // 쿼리 실행.
        if sqlite3_step(statement) != SQLITE_DONE {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        print("drop table has been successfully done")
        
    }

 

테이블 전체 삭제(마지막)

마지막으로 table 전체를 삭제해봅시다.

어느 table을 삭제할 건지 지정해주기 위해 table의 이름을 인자로 받았습니다.

    func deleteTable(tableName: String) {
        let queryString = "DROP TABLE \(tableName)"
        var statement: OpaquePointer?
        
        if sqlite3_prepare(db, queryString, -1, &statement, nil) != SQLITE_OK {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        // 쿼리 실행.
        if sqlite3_step(statement) != SQLITE_DONE {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        print("drop table has been successfully done")
        
    }

 

실행해보기

main.storyboard를 아래와 같이 구성하고 테스트를 진행해보았습니다.

그 결과는 글의 맨 위에서 보여드린 사진들입니다.

 

이들을 각각 IBOutlet으로 viewController에 연결하고(ctrl + drag를 통해)

데이터가 변경될 때마다 테이블 뷰를 update 시켜주었습니다. 코드는 아래와 같습니다.

import UIKit
import SQLite3

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    var dataArray: [MyModel] = []

    let dbHelper = DBHelper.shared
    
    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.delegate = self
        self.tableView.dataSource = self
    }

    @IBAction func touchUpCreateButton(_ sender: Any) {
        dbHelper.createTable()
        self.dataArray = dbHelper.readData()
        self.tableView.reloadData()

    }
    
    @IBAction func touchUpInsertButton(_ sender: Any) {
        dbHelper.insertData(name: "첫 번째", age: 10)
        dbHelper.insertData(name: "두 번째", age: 20)
        dbHelper.insertData(name: "세 번째", age: 30)
        
        self.dataArray = dbHelper.readData()
        self.tableView.reloadData()
    }
    
    
    @IBAction func touchUpUpdateButton(_ sender: Any) {
        dbHelper.updateData(id: 1, name: "수정한 첫 번째", age: 99)
        
        self.dataArray = dbHelper.readData()
        self.tableView.reloadData()
    }
    
    @IBAction func touchUpDeleteButton(_ sender: Any) {
        dbHelper.deleteData(id: 1)
        
        self.dataArray = dbHelper.readData()
        self.tableView.reloadData()
        
    }
    
    @IBAction func touchUpDropButton(_ sender: Any) {
        dbHelper.deleteTable(tableName: "myTable")
        
        self.dataArray = dbHelper.readData()
        self.tableView.reloadData()
    }

}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        self.dataArray.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = self.tableView.dequeueReusableCell(withIdentifier: "tableViewCell") as? CustomTableViewCell
        else { fatalError("can't get cell") }
        
        cell.idLabel.text = String(dataArray[indexPath.row].id)
        cell.nameLabel.text = String(dataArray[indexPath.row].myName)
        if let age = dataArray[indexPath.row].myAge {
            cell.ageLabel.text = String(age)
        }
        
        return cell
    }
    
    
}

 

자, 이제 여러분도 따라해보고 테스트해보시길 바랍니다. 

참고 코드로 customTableViewCell.swift 파일과 DBHelper.swift 파일의 원본 코드도 아래에 첨부해두겠습니다.

 

참고 코드

customTableViewCell.swift

import UIKit

class CustomTableViewCell: UITableViewCell {
    @IBOutlet weak var idLabel: UILabel!
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var ageLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

 

DBHelper.swift

import Foundation
import SQLite3

struct MyModel: Codable {
    var id: Int
    var myName: String
    var myAge: Int?
    
    
}

class DBHelper {
    static let shared = DBHelper()
    
    var db : OpaquePointer? //db를 가리키는 포인터
    // db 이름은 항상 "DB이름.sqlite" 형식으로 해줄 것.
    let databaseName = "mydb.sqlite"
    
    
    
    init() {
        self.db = createDB()
    }

    deinit {
        sqlite3_close(db)
    }
    
    private func createDB() -> OpaquePointer? {
        var db: OpaquePointer? = nil
        do {
            let dbPath: String = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(databaseName).path
            
            if sqlite3_open(dbPath, &db) == SQLITE_OK {
                print("Successfully created DB. Path: \(dbPath)")
                return db
            }
        } catch {
            print("Error while creating Database -\(error.localizedDescription)")
        }
        return nil
    }
    
    func createTable(){
        // 아래 query의 뜻.
        // mytable이라는 table을 생성한다. 필드는
        // id(int, auto-increment primary key)
        // my_name(String not null)
        // my_age(Int)
        // 로 구성한다.
        // auto-increment 속성은 INTEGER에만 가능하다.
        let query = """
           CREATE TABLE IF NOT EXISTS myTable(
           id INTEGER PRIMARY KEY AUTOINCREMENT,
           my_name TEXT NOT NULL,
           my_age INT
           );
           """
        var statement: OpaquePointer? = nil
        
        if sqlite3_prepare_v2(self.db, query, -1, &statement, nil) == SQLITE_OK {
            if sqlite3_step(statement) == SQLITE_DONE {
                print("Creating table has been succesfully done. db: \(String(describing: self.db))")
                
            }
            else {
                let errorMessage = String(cString: sqlite3_errmsg(db))
                print("\nsqlte3_step failure while creating table: \(errorMessage)")
            }
        }
        else {
            let errorMessage = String(cString: sqlite3_errmsg(self.db))
            print("\nsqlite3_prepare failure while creating table: \(errorMessage)")
        }
        
        sqlite3_finalize(statement) // 메모리에서 sqlite3 할당 해제.
    }
    
    
    
    func insertData(name: String, age: Int) {
        // id 는 Auto increment 속성을 갖고 있기에 빼줌.
        let insertQuery = "insert into myTable (id, my_name, my_age) values (?, ?, ?);"
        var statement: OpaquePointer? = nil
        
        if sqlite3_prepare_v2(self.db, insertQuery, -1, &statement, nil) == SQLITE_OK {
            sqlite3_bind_text(statement, 2, name, -1, nil)
            sqlite3_bind_int(statement, 3, Int32(age))
            
        }
        else {
            print("sqlite binding failure")
        }
        
        if sqlite3_step(statement) == SQLITE_DONE {
            print("sqlite insertion success")
        }
        else {
            print("sqlite step failure")
        }
    }
    
    func readData() -> [MyModel] {
        let query: String = "select * from myTable;"
        var statement: OpaquePointer? = nil
        // 아래는 [MyModel]? 이 되면 값이 안 들어간다.
        // Nil을 인식하지 못하는 것으로..
        var result: [MyModel] = []

        if sqlite3_prepare(self.db, query, -1, &statement, nil) != SQLITE_OK {
            let errorMessage = String(cString: sqlite3_errmsg(db)!)
            print("error while prepare: \(errorMessage)")
            return result
        }
        while sqlite3_step(statement) == SQLITE_ROW {
            
            let id = sqlite3_column_int(statement, 0) // 결과의 0번째 테이블 값
            let name = String(cString: sqlite3_column_text(statement, 1)) // 결과의 1번째 테이블 값.
            let age = sqlite3_column_int(statement, 2) // 결과의 2번째 테이블 값.
            
            result.append(MyModel(id: Int(id), myName: String(name), myAge: Int(age)))
        }
        sqlite3_finalize(statement)
        
        return result
    }
    
    func updateData(id: Int, name: String, age: Int) {
        var statement: OpaquePointer?
        // 등호 기호는 =이 아니라 ==이다.
        // string 부분은 작은 따옴표 두 개로 감싸줘야 한다.
        let queryString = "UPDATE myTable SET my_name = '\(name)', my_age = \(age) WHERE id == \(id)"
        
        // 쿼리 준비.
        if sqlite3_prepare(db, queryString, -1, &statement, nil) != SQLITE_OK {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        // 쿼리 실행.
        if sqlite3_step(statement) != SQLITE_DONE {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        print("Update has been successfully done")
    }
    
    func deleteData(id: Int) {
        let queryString = "DELETE FROM myTable WHERE id == \(id)"
        var statement: OpaquePointer?
        
        if sqlite3_prepare(db, queryString, -1, &statement, nil) != SQLITE_OK {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        // 쿼리 실행.
        if sqlite3_step(statement) != SQLITE_DONE {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        print("delete has been successfully done")
    }
    
    func deleteTable(tableName: String) {
        let queryString = "DROP TABLE \(tableName)"
        var statement: OpaquePointer?
        
        if sqlite3_prepare(db, queryString, -1, &statement, nil) != SQLITE_OK {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        // 쿼리 실행.
        if sqlite3_step(statement) != SQLITE_DONE {
            onSQLErrorPrintErrorMessage(db)
            return
        }
        
        print("drop table has been successfully done")
        
    }
    
    private func onSQLErrorPrintErrorMessage(_ db: OpaquePointer?) {
        let errorMessage = String(cString: sqlite3_errmsg(db))
        print("Error preparing update: \(errorMessage)")
        return
    }
}

 

출처(Reference)

https://lazyowl.tistory.com/entry/Swift%EC%97%90%EC%84%9C-SQLITE3-Database-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

https://magicofdream.tistory.com/20

https://ios-development.tistory.com/85

https://hururuek-chapchap.tistory.com/39

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기
// custom