I've following sqlite code using C api within Swift having a read and a delete operation for review.
For those not familiar with Swift, defer
will execute after the end of the function.
Since I'm not closing the database connection and keeping it alive, I'm calling sqlite3_db_cacheflush(database)
to flush out the changes to the disk. Link
class Database {
internal var database: OpaquePointer! = nil
init(_ database: OpaquePointer) {
self.database = database
}
func deleteById(_ id: Int64) throws -> Int32? {
let sql = "DELETE FROM user WHERE id = \(id)"
defer {
sqlite3_db_cacheflush(database)
}
guard sqlite3_exec(database, sql, nil, nil, nil) == SQLITE_OK else {
throw NSError(domain: String(cString: sqlite3_errmsg(database)), code: 1, userInfo: nil)
}
return sqlite3_changes(database)
}
func findAllDepartments() throws -> [Department]? {
var statement: OpaquePointer? = nil
let sql = "SELECT id, name FROM department"
guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK else {
throw NSError(domain: String(cString: sqlite3_errmsg(database)), code: 1, userInfo: nil)
}
defer {
sqlite3_finalize(statement)
sqlite3_db_cacheflush(database)
}
var departments: [Department] = []
while sqlite3_step(statement) == SQLITE_ROW {
departments.append(Department(id: sqlite3_column_int64(statement, 0), name: String(cString: sqlite3_column_text(statement, 1))))
}
return departments
}
}
Usage of Database Class
do {
if sqlite3_open_v2(url.path, &database, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK { // Existing database (Serialized mode)
let db = Database(database)
try! db.findAllDepartments()
try! db.deleteById(4)
} else { // Error opening database
print(String(cString: sqlite3_errmsg(database)))
}
catch { let error as NSError {
print(error.description)
}
-
\$\begingroup\$ Have you considered making more of a wrapper around SQLite? So that you wouldn't really get the spilling over of opening a database where you're using the Database class? \$\endgroup\$MultiColourPixel– MultiColourPixel2020年12月22日 23:58:44 +00:00Commented Dec 22, 2020 at 23:58
1 Answer 1
You did not specify what precisely you were looking for, so a few general observations:
You do not need
sqlite3_db_cacheflush
. When you perform SQL updates, they are written to disk immediately (unless you are using transactions). You can do your inserts, updates, and deletes, leave the database open, and you’ll be fine.Use
Error
, notNSError
. I would advise againstNSError
(unless you needed Objective-C backward compatibility). In Swift, we would define our ownError
type:enum DatabaseError: Error { case sqlError(Int32, String) }
And then
guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK else { throw DatabaseError.sqlError(sqlite3_errcode(database), String(cString: sqlite3_errmsg(database))) }
Then, when catching errors, the codes and the error messages are more easily retrieved.
Failure of
sqlite3_open_v2
will leak. When handlingsqlite3_open_v2
failure, make sure to close the database or else resources allocated to it will never get freed. I know that this seems unintuitive, but the documentation is clear (emphasis added):Whether or not an error occurs when it is opened, resources associated with the database connection handle should be released by passing it to
sqlite3_close()
when it is no longer required.Method names. You have a method called
deleteById(_:)
, which does not make it clear what you are deleting. Besides the use of a preposition and parameter name in the method name is common in Objective-C, but not Swift. I would suggestdeleteUser(id:)
or something like that. That makes the domain and functional intent of the method clear, and moves the parameter label where it belongs.Be careful with string interpolation of SQL. In your user deletion routine, you are building the SQL statement through string interpolation of the identifier. That happens to work because the parameters are numeric, but can be problematic with string parameters. Usually we would use a prepared statement and then bind values to
?
placeholders in the SQL.Methods that
throw
should not return optionals. Optionals are useful when writing functions that must return eithernil
or some value. But in this case, you always either return a value orthrow
errors, so use of optionals is not needed any only complicates the calling point. Remove the?
in the return values of E.g.,enum DatabaseError: Error { case sqlError(Int32, String) } func deleteUser(by identifier: Int64) throws -> Int32 { let sql = "DELETE FROM user WHERE id = \(identifier)" guard sqlite3_exec(database, sql, nil, nil, nil) == SQLITE_OK else { throw DatabaseError.sqlError(sqlite3_errcode(database), String(cString: sqlite3_errmsg(database))) } return sqlite3_changes(database) } func findAllDepartments() throws -> [Department] { var statement: OpaquePointer? = nil let sql = "SELECT id, name FROM department" guard sqlite3_prepare_v2(database, sql, -1, &statement, nil) == SQLITE_OK else { throw DatabaseError.sqlError(sqlite3_errcode(database), String(cString: sqlite3_errmsg(database))) } defer { sqlite3_finalize(statement) } var departments: [Department] = [] while sqlite3_step(statement) == SQLITE_ROW { departments.append(Department(id: sqlite3_column_int64(statement, 0), name: String(cString: sqlite3_column_text(statement, 1)))) } return departments }
Strive for highly cohesive, loosely coupled types. In this case, you have some other code opening the SQLite database and passing the
sqlite3
pointer to theDatabase
class. This is bad because it tightly couples the caller with theDatabase
class, littering SQLite calls in multiple classes. It also is problematic because theDatabase
type is less cohesive, not encompassing all the SQLite calls.If, for example, you replace SQLite with some other storage mechanism at some later date, you are going to have to not only replace your storage class,
Database
, but go through all the caller code, too.Database
is tightly coupled with model types. In a similar vein as my previous point, we generally want to keep model objects and database objects a little less tightly coupled than this. For example, rather than havingDepartment
specific methods in our baseDatabase
class, you would want to define a protocol for fetching and storing of data. Or, at the very least, I would moveDepartment
and other model-specific methods in their own extensions, to avoid cluttering the mainDatabase
class with model-specific methods.Consider SQLite wrapper class. Before you write your own SQLite wrapper class, you might want to consider one of the existing ones out there. They have already wrestled with many of the basic considerations and might have interfaces to handle this.
Just a few observations.