SQLite.swift:如何优雅存储数组和字典

4,655 阅读5分钟

初识SQLite.swift

在使用Objective-C编程语言构建项目的时候,对于SQLite数据库的操作,基本上使用的是FMDB。等到Swift发布之后,开始了SwiftObjective-C的混合开发,依然选择的是FMDB进行数据库操作。直到进行纯Swift开发,才开始寻找新的SQLite封装库。从Github的排名来看,就SQLite.swiftstar数量最高了。阅读完readme之后,感觉不错就入手了。

借助于Swift语言强大的泛型系统、类型安全和自定义操作符等特性,把表单定义、数据库的增删改查操作封装得特别友好。未使用过得朋友,建议点击查看SQLite.swift。对于熟悉SQLite.swift的朋友,可以直接查看我封装的SQLiteValueExtension库。

当前存储数组、字典的方案

比如属性是[BasicInfoModel]自定义类型数组,先定义字符类型的Expression:Expression<String?>

在存储的时候,借助于ObjectMapper库,把数组转换为String,代码如下:

let string = basic.infos?.toJSONString() ?? ""
let insert = config.insert(infos <- string)
try? connection.run(insert)

在从数据库读取的时候,也需要把String转换为[BasicInfoModel]类型,代码如下:

        for data in try! connection.prepare(config) {
            let basic = BasicDataModel(JSON: [String : Any]())
            basic?.infos = Array<BasicInfoModel>.init(JSONString: data[parlayRules])
            return basic
        }

通过上面的代码可以看到最大的问题,就是数组与String类型的转换代码,散落在业务代码里面。如果有多个属性都是数组,那么同样的样板代码会写多次。如何优雅地解决这个问题呢?

好了,下面进入主题,如何通过SQLite.swift优雅地存储数组和字典数据类型。我会从以下3个部分进行讲解:

  • SQLite3数据库支持的数据类型
  • 遵从Value协议支持自定义类型存储
  • 探讨如何实现存储数组和字典数据类型

SQLite3数据库支持的数据类型

对于SQLite3数据库仅支持以下基础类型,换言之,只要是其他的数据类型,都需要转换成其中某个类型再进行存储。从数据库取的时候,再转换为对应的数据类型。

Swift Type SQLite Type
Int64 INTEGER
Double REAL
String TEXT
nil NULL
SQLite.Blob BLOB

遵从Value协议支持自定义类型存储

对于自定义类型想要存储到数据库,SQLite.swift定义了Value协议,来规范转换过程。这是官方文档:custom-types

下面是一个枚举类型支持Value的示例代码:

enum Gender: String, Value {
    case female
    case male

    typealias Datatype = String
    static var declaredDatatype: String { String.declaredDatatype }
    static func fromDatatypeValue(_ datatypeValue: String) -> Gender {
        return Gender(rawValue: datatypeValue) ?? .female
    }
    var datatypeValue: String { rawValue }
}

探讨如何实现存储数组和字典数据类型

通过上面的铺垫之后,我们有了思路。那就是让数组和字典遵从Value协议。

1.数组和字典转换成什么类型

首先要确定的是把数组和字典转换成什么类型,我的选择是String

2.如何转换为String

然后要确定的是如何转换为String。先拿数组来说明,我们可以通过JSONSerialization类把符合规范的数组转换为Data,然后再转换为String。示例代码如下:

if let data = try? JSONSerialization.data(withJSONObject: stringArray, options: []) {
    return String(data: data, encoding: .utf8) ?? ""
}

3.如何转换为符合规范的JSONObject

然后要解决的是如何把存储任意类型的数组,转换为符合规范的JSONObject。在软件领域有一个包治百病的方案,就是“没有加一层抽象不能解决的问题,如何有,那就再加一层”。这个时候,引入StringValueExpressible协议,让Array.Elment遵从于该协议,让其实现与String的互相转换。StringValueExpressible定义如下:

public protocol StringValueExpressible  {
    associatedtype ValueType = Self
    static func fromStringValue(_ stringValue: String) -> ValueType
    var stringValue: String { get }
}

通过上面的操作,我们就可以让[StringValueExpressible]转换为[String]

4.数组遵从Value的实现代码

extension Array: Value where Element: StringValueExpressible {
    public typealias Datatype = String
    public static var declaredDatatype: String { String.declaredDatatype }
    public static func fromStringValue(_ stringValue: String) -> Self {
        var result = [Element]()
        if let object = try? JSONSerialization.jsonObject(with: Data(stringValue.utf8), options: []) as? [String] {
            for string in object {
                let value = Element.fromStringValue(string) as! Element
                result.append(value)
            }
        }
        return result
    }
    public var stringValue: String {
        let stringArray = self.map { $0.stringValue }
        if let data = try? JSONSerialization.data(withJSONObject: stringArray, options: []) {
            return String(data: data, encoding: .utf8) ?? ""
        }
        return ""
    }
    public static func fromDatatypeValue(_ datatypeValue: Datatype) -> Self {
        return fromStringValue(datatypeValue)
    }
    public var datatypeValue: Datatype {
        return stringValue
    }
}

我们通过where条件语句进行了Array.Elment遵从StringValueExpressible协议的限制。

最后,可以总结为,只要数组中的元素遵从于StringValueExpressible协议,该数组就遵从了Value协议,就可以通过SQLite.swift进行数据库存储。然后封装的SQLiteValueExtension库,内部已经让IntDoubleBoolStringDataDate等类型遵从了StringValueExpressible协议。所以,例如[Int][Date]等数组类型,可以直接存储了。

存储数组、字典示例代码

其中BasicInfoModel是一个遵从ValueStringValueExpressible协议的自定义数据类。

class BasicDAO: DataBaseAccessObject {
    static let config = Table("config")
    static let normalFloat = Expression<Float?>("normal_float")
    static let normalInt = Expression<Int?>("normal_int")
    static let normalModel = Expression<BasicInfoModel?>("normal_model")
    static let normalString = Expression<String?>("normal_string")
    static let intArray = Expression<[Int]?>("int_array")
    static let stringArray = Expression<[String]?>("string_array")
    static let modelArray = Expression<[BasicInfoModel]?>("model_array")
    static let intStringDict = Expression<[Int:String]?>("int_string_dict")
    static let stringModelDict = Expression<[String:BasicInfoModel]?>("string_model_dict")

    class func createTable() throws {
        try connection.run(config.create(ifNotExists: true) { t in
            t.column(normalFloat)
            t.column(normalInt)
            t.column(normalString)
            t.column(intArray)
            t.column(intStringDict)
            t.column(stringArray)
            t.column(normalModel)
            t.column(modelArray)
            t.column(stringModelDict)
        })
    }

    class func insertEntity(_ basic: BasicDataModel) throws {
        let insert = config.insert(normalFloat <- basic.normalFloat,
                                   normalInt <- basic.normalInt,
                                   normalString <- basic.normalString,
                                   normalModel <- basic.normalModel,
                                   intArray <- basic.intArray,
                                   stringArray <- basic.stringArray,
                                   modelArray <- basic.modelArray,
                                   intStringDict <- basic.intStringDict,
                                   stringModelDict <- basic.stringModelDict)
        try connection.run(insert)
    }

    class func queryRows() throws -> [BasicDataModel]? {
        do {
            let rows = try connection.prepare(config)
            var result = [BasicDataModel]()
            for data in rows {
                let basic = BasicDataModel(JSON: [String : Any]())!
                basic.normalFloat = data[normalFloat]
                basic.normalInt = data[normalInt]
                basic.normalString = data[normalString]
                basic.normalModel = data[normalModel]
                basic.intArray = data[intArray]
                basic.stringArray = data[stringArray]
                basic.modelArray = data[modelArray]
                basic.stringModelDict = data[stringModelDict]
                basic.intStringDict = data[intStringDict]
                result.append(basic)
            }
            return result
        } catch let error {
            print("queryError:\(error.localizedDescription)")
        }
        return nil
    }
}

如何使用SQLiteValueExtension

通过cocoapods引入即可:

pod 'SQLiteValueExtension'

Github地址

SQLiteValueExtension