FMDB是使用OC语言对原生的SQLite的包装库,可使用原生SQLite语句对其进行操作,且支持多线程从而保证线程安全性。
一:安装使用步骤
SPM: https://github.com/ccgus/fmdb
使用时导入包即可。例如 import FMDB
二:创建DBHelper类
创建单例工具类供访问。
import Foundation
import FMDB
class SQLiteManager: NSObject {
// Create Single instance.
private static let manger: SQLiteManager = SQLiteManager()
class func getInstance() -> SQLiteManager {
return manger
}
// Database's name.
private let dbName = "table_basic.db"
// URL
lazy var dbURL: URL = {
let fileURL = try! FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask,
appropriateFor: nil, create: true)
.appendingPathComponent(dbName)
print("------>Database URL:", fileURL)
return fileURL
}()
// FMDatabase Object.
lazy var db: FMDatabase = {
let database = FMDatabase(url: dbURL)
return database
}()
// FMDatabaseQueue Object.
lazy var dbQueue: FMDatabaseQueue? = {
let databaseQueue = FMDatabaseQueue(url: dbURL)
return databaseQueue
}()
}
helper类里面存有数据表名称(sqlite对于用户而言无数据库,仅有数据表),每次使用时获取实例对象进行增删改查即可。
三:增删改查类型操作
1.初始化
@IBAction func initListener(_ sender: Any) {
let db = SQLiteManager.getInstance().db
if db.open() {
print("DATABASE INIT SUCCEED.")
} else {
print("ERROR AT: \(db.lastError())")
}
}
2.创建表
增删改查均可类使用Java中的预处理方式进行运行。例如,SQL语句已经拼写完成,只需将SQL语句中待定参数替换为?即可,在withArgumentsIn: []
将变量对应传入即可。需要说明的是,在FMDB中,只有查询为executeQuery()
,其余均为executeUpdate()
。
@IBAction func createListener(_ sender: Any) {
let sql = "create table if not exists table_basic (id integer primary key autoincrement, name text);"
let db = SQLiteManager.getInstance().db
if db.open(){
try! db.executeUpdate(sql, withArgumentsIn: [])
} else{
print("ERROR AT \(db.lastError())")
}
db.close()
}
3.增
@IBAction func addListener(_ sender: Any) {
let sql = "insert into table_basic (id, name) values(?,?);"
let db = SQLiteManager.getInstance().db
if db.open(){
try! db.executeUpdate(sql, withArgumentsIn: [2007001, "jack"])
} else{
print("ERROR AT \(db.lastError())")
}
db.close()
}
4.删
@IBAction func delListener(_ sender: Any) {
let sql = "delete from table_basic where id = 2007001;"
let db = SQLiteManager.getInstance().db
if db.open(){
try! db.executeUpdate(sql, withArgumentsIn: [])
} else{
print("ERROR AT \(db.lastError())")
}
db.close()
}
当然也可以写成如下预处理方式:
@IBAction func delListener(_ sender: Any) {
let sql = "delete from table_basic where id = ?;"
let db = SQLiteManager.getInstance().db
if db.open(){
try! db.executeUpdate(sql, withArgumentsIn: [2007001])
} else{
print("ERROR AT \(db.lastError())")
}
db.close()
}
5.改
@IBAction func updateListener(_ sender: Any) {
let sql = "update table_basic set name = "" where id = ?;"
let db = SQLiteManager.getInstance().db
if db.open(){
try! db.executeUpdate(sql, withArgumentsIn: ["rose", 2007001])
} else{
print("ERROR AT \(db.lastError())")
}
db.close()
}
6.查
这里的查询类似于JDBC的ResultSet,FMDB也存在类似于FMDBResultSet的查询结果集对象。需要说明的是,String类型的数据一般为optional类型,不要忘记解包。
@IBAction func searchListener(_ sender: Any) {
let sql = "select * from table_basic;"
let db = SQLiteManager.getInstance().db
if db.open() {
if let resultSet = db.executeQuery(sql, withArgumentsIn: []) {
while resultSet.next() {
print("DATA IS \(resultSet.int(forColumn: "id"))")
print("DATA IS \(resultSet.string(forColumn: "name")!)")
}
}
} else {
print("ERROR AT \(db.lastError())")
}
db.close()
}
四:多数据类型存储
SQLite不仅可存储常见的date、varchar等类型数据,还可存储二进制类型数据,是为blob。下面将演示
将图片以二进制类型存储至数据库并读取后显示在视图上。
首先创建表的操作如下:
@IBAction func createListener(_ sender: Any) {
let sql = "create table if not exists table_bin (id integer primary key autoincrement, content_data blob);"
let db = SQLiteManager.getInstance().db
if db.open(){
try! db.executeUpdate(sql, withArgumentsIn: [])
} else{
print("ERROR AT \(db.lastError())")
}
db.close()
}
可以看到这里创建了一个名为content_data的blob类型的字段,其余并无特别之处。
接下来是存储的操作。这里需要说明的是,需要用预处理语句将图片存入(图片需要转换为Data类型)。
@IBAction func alterListener(_ sender: Any) {
let sql = "insert into table_bin (id, content_data) values(?,?);"
let db = SQLiteManager.getInstance().db
if db.open() {
// Convert UIImage object into Data object.
let image = UIImage(named: "testimg")
let data = image?.pngData()
// Store into preprocessing statement.
try! db.executeUpdate(sql, withArgumentsIn: [2007003, data])
} else{
print("ERROR AT \(db.lastError())")
}
db.close()
}
接下来就是读取了,读取时注意数据的解包即可。操作如下:
@IBAction func searchListener(_ sender: Any) {
let sql = "select * from table_bin;"
let db = SQLiteManager.getInstance().db
if db.open() {
if let resultSet = db.executeQuery(sql, withArgumentsIn: []) {
while resultSet.next() {
let imageData = resultSet.data(forColumn: "content_data")! as? Data
self.imageView.image = UIImage(data: imageData!)
}
}
} else {
print("ERROR AT \(db.lastError())")
}
db.close()
}
五:事物类操作
FMDB同样支持事物类型的操作。事物具有四大特性(ACID)。这里演示的是事物队列进行批量操作,成功与失败的情况。
@IBAction func searchListener(_ sender: Any) {
if let queue = SQLiteManager.getInstance().dbQueue {
queue.inTransaction { db, rollback in
do {
for i in 0..<10 {
try db.executeUpdate("insert into table_basic (id, name) values (?,?);",
values: [i, "jack"])
}
} catch {
print("ERROR AND ROLLBACK.")
rollback.pointee = true
}
}
}
}
正常情况下上述内容会全部正常插入。下面是异常情况:
@IBAction func searchListener(_ sender: Any) {
if let queue = SQLiteManager.getInstance().dbQueue {
queue.inTransaction { db, rollback in
do {
for i in 0..<10 {
if i == 5 {
try db.executeUpdate("insert into table_error (id, name) values (?,?);",
values: [i, "jack"])
}
try db.executeUpdate("insert into table_basic (id, name) values (?,?);",
values: [i, "jack"])
}
} catch {
print("ERROR AND ROLLBACK.")
rollback.pointee = true
}
}
}
}
由于设置了回滚,第五次执行的语句是不存在的数据表,正常情况下前面四组插入成功的数据也会被删除,是为回滚。
六:配合Model
这里做个简单的演示,顺便做个分层。
首先要展示的功能很简单,一张数据表用来存储id、name、major,再来个查询功能将数据表内数据查出显示。
那么dbhelper(dao)内应该是与这张数据表相关的内容,如下:
import Foundation
import FMDB
class SQLiteManager: NSObject {
private static let manger: SQLiteManager = SQLiteManager()
class func getInstance() -> SQLiteManager {
return manger
}
private let dbName = "tast_user.db"
lazy var dbURL: URL = {
let fileURL = try! FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask,
appropriateFor: nil, create: true)
.appendingPathComponent(dbName)
print("Database Address:", fileURL)
return fileURL
}()
lazy var db: FMDatabase = {
let database = FMDatabase(url: dbURL)
return database
}()
lazy var dbQueue: FMDatabaseQueue? = {
let databaseQueue = FMDatabaseQueue(url: dbURL)
return databaseQueue
}()
}
接着我们在model层定义一个model(类似于Java中的Bean),其作用是作为实体类进行操纵读取写入,内容如下:
import Foundation
class User: NSObject {
var id: Int?
var name: String?
var major: String?
init(id: Int, name: String, major: String) {
self.id = id
self.name = name
self.major = major
}
override init() {
}
}
然后我们在service(业务层)定义一个协议,其作用是规定返回数据类型及应该完成的功能,内容如下:
import Foundation
protocol SearchUserService {
func findUser() -> Array<User>
}
既然定义了一个这样的协议,就肯定要遵循了,即应存在一个实现SearchService的一个类,内容如下:
import Foundation
import FMDB
class SearchServiceImpl: SearchUserService {
init() {
let db = SQLiteManager.getInstance().db
if db.open() {
print("DATABASE INIT SUCCEED.")
} else {
print("ERROR AT: \(db.lastError())")
}
}
func findUser() -> Array<User> {
var tempArray: Array<User> = [User]()
let sql = "select * from test_user;"
let db = SQLiteManager.getInstance().db
if db.open() {
if let resultSet = db.executeQuery(sql, withArgumentsIn: []) {
while resultSet.next() {
tempArray.append(User(id: Int(resultSet.int(forColumn: "id")), name: resultSet.string(forColumn: "name")!, major: resultSet.string(forColumn: "major")!))
}
}
} else {
print("ERROR AT \(db.lastError())")
}
db.close()
return tempArray
}
}
在这个类里面我们首先初始化数据库来拿到数据库对象,然后在findUser()
方法里面定义泛型集合,拼接查询条件,执行,取出数据到model内再放入泛型集合内,最后待迭代集迭代完成便返回这个泛型集合。
最后一步就是显示。这里是将结果直接打印,出于方便角度并没有直接显示在屏幕上,而是打印出来。内容如下:
let searchServiceImpl = SearchServiceImpl()
let myArray: Array<User> = searchServiceImpl.findUser()
print("------> DATA CONTENT: id: \(myArray[0].id!) name: \(myArray[0].name!) major: \(myArray[0].major!)")
整体步骤类似于SpringBoot的操作。当然这不一定是完整、标准的,应该存在其它写法,欢迎讨论,不吝赐教。
七:模糊查询
SQLite支持like、glob语句。like不区分大小写,glob区分大小写。下面是两者的示例:
@IBAction func searchListener(_ sender: Any) {
let sql = "select * from table_basic where name like '%j%';"
let db = SQLiteManager.getInstance().db
if db.open() {
if let resultSet = db.executeQuery(sql, withArgumentsIn: []) {
while resultSet.next() {
print("DATA IS \(resultSet.int(forColumn: "id"))")
print("DATA IS \(resultSet.string(forColumn: "name")!)")
}
}
} else {
print("ERROR AT \(db.lastError())")
}
db.close()
}
glob仅与上者语句不同:
let sql = "select * from table_basic where name glob '*j*';"
八:分组
let sql = "select name from table_basic group by name"
对name进行剔除重复的分组查询,结果中每个不同的name值仅有对应的唯一一条行记录,当然这个语句要看具体的业务逻辑。
九:排序
let sql = "select * from table_basic order by id asc"
对id进行以升序为条件的查询
十:触发器
触发器的创建使用是executeUpdate()
语句。假设存在一个这样的业务逻辑:某张表记录用户每次的账号、密码、上次密码。上次密码的作用是防止用户待修改与上次密码重复。那么其创表语句如下:
let sql = "create table if not exists test_trigger(id integer primary key,password varchar(20),pass_password varchar(20))"
触发器语句如下:
@IBAction func triggerListener(_ sender: Any) {
let sql = "create trigger record_password before update on test_trigger begin insert into test_trigger(pass_password) values(old.password); end"
let db = SQLiteManager.shareManger().db
if db.open(){
try! db.executeUpdate(sql, withArgumentsIn: [])
} else{
print("ERROR AT \(db.lastError())")
}
db.close()
}
需要说明的是不要让触发语句的外部条件为for earch row
。这里并不是说在ios的sqlite不支持for each row
,而是该条件是可选的。因为大部分情况下触发语句是针对行操作,也就是说外部条件为for each row
而内部为update等情况下会造成循环引用。
十一:索引
索引就比较简单了,同样也是executeUpdate()
,语句如下:
let sql = "create index my_index on table_basic(id);"