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://magicofdream.tistory.com/20
최근댓글