SQLite.swift 的类型扩展 — 存储 UUID 的二进制值

首先要知道 SQLite 只支持如下五种存储类型:

存储类 描述
NULL 值是一个 NULL 值。
INTEGER 值是一个带符号的整数,根据值的大小存储在 1、2、3、4、6 或 8 字节中。
REAL 值是一个浮点值,存储为 8 字节的 IEEE 浮点数字。
TEXT 值是一个文本字符串,使用数据库编码格式(默认为 UTF-8)存储。
BLOB 值是一个二进制数据,完全根据它的输入存储。

SQLite.swift 将这五种存储类型与 Swift 数据类型做了一一对应:

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

其中 SQLite.Blob 类型是 SQLite.swift 自己定义的类型。另外,对于 Swift 的 Bool 类型,SQLite.swift 会将其转换成 Int64 然后以 INTEGER 类型进行存储(0 表示 false, 1 表示 true);对于 Swift 的 Date 类型,SQLite.swift 会将其转换成 String 类型后,通过 TEXT 类型进行存储;Int 类型也会自动转换成 Int64 后进行存储。

如果我们想定义其他的 Swift 数据类型到 SQLite 存储类型的转换,就得实现扩展 Swift 数据类型,使其满足 SQLite.Value 协议。Value 协议的作用就是将 Swfit 数据类型 A 转换成上述五种基本的 Swfit Type 之一,然后在存储时自动转换成对应的 SQLite Type 进行存储。

以 Swift 的 UUID 类型为例,最简单的做法就是将其转化成字符串进行存储。更“困难”一点的,也是本文主要想描述的,就是将 UUID 类型转换成 SQLite.Blob 类进行存储。

import SQLite

struct Book {
    var id: Int64
    var uuid: UUID
    var title: String
    var readed: Bool
    var read_at: Date?
}

// 扩展 UUID,使其满足 SQLite.Value 协议
// 实现 UUID <==> SQLite.Blob 的转换
extension UUID: Value {

    // 它的目标类型是 SQLite.Blob
    public typealias Datatype = Blob
    
    public static var declaredDatatype: String {
        return Blob.declaredDatatype
    }
    
    // 将目标类型转换成 UUID
    // SQLite.Blob 类型有个 bytes: [UInt8] 的存储属性,存储着实际的二进制数据
    public static func fromDatatypeValue(_ datatypeValue: Datatype) -> UUID {
        let bytes = datatypeValue.bytes
        return UUID(uuid: (bytes[0], bytes[1], bytes[2], bytes[3],
                           bytes[4], bytes[5], bytes[6], bytes[7],
                           bytes[8], bytes[9], bytes[10], bytes[11],
                           bytes[12], bytes[13], bytes[14], bytes[15]))
    }

    // 将 UUID 类型转换成目标类型
    // 可以通过 UUID.uuid 计算属性获取其二进制值的表示,包含 16 个 UInt8 数据的元组:(UInt8, UInt8, ...)
    public var datatypeValue: Datatype {
        var bytes = [UInt8](repeating: 0, count: 16)
        (bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]) = self.uuid
        return Datatype(bytes: bytes)
    }
}


/**
 * 以下是测试代码
 */
    #if DEBUG
        db.trace { print("execute sql: \($0)" ) }
    #endif
   
    // 声明表的字段类型
    let id = Expression<Int64>("id")
    let uuid = Expression<UUID>("uuid")        // 注意!这里用的是 UUID,不是 Blob!
    let title = Expression<String>("title")
    let readed = Expression<Bool>("readed")    // 这里用 Bool,实际存储时候会被自动转换成整形
    let read_at = Expression<Date?>("read_at") // 这里用 Date,实际存储时候会被自动转换成字符串。并且用可选型表示该字段可以为空

    let books = Table("books")
    
    // 创建表
    try! db.run(books.create { t in
        t.column(id, primaryKey: true)
        t.column(uuid, unique: true)
        t.column(title)
        t.column(readed)
        t.column(read_at)
    })

    // 插入数据
    try! db.run(books.insert( uuid <- UUID(), title <- "1984", readed <- true, read_at <- Date() ))
    try! db.run(books.insert( uuid <- UUID(), title <- "美丽新世界", readed <- false ))

    // 读取数据
    for book in try! db.prepare(books) {
        let b = Book(id: book[id], uuid: book[uuid], title: book[title], readed: book[readed], read_at: book[read_at] ?? nil)
        print(b)
    }

执行完后,我们打开 SQLite 数据库文件看一下:

表结构如下:

一个题外话,SQLite 的主键如果是整形(以上面的主键 id 为例),如果插入新数据时没有给出 id 值,那么系统默认就会使用(当前最大 id + 1)来进行填充。这看起来就像默认自增一样,这个值其实是来自 SQLite 的一个隐藏字段 rowid。不过与强制定义该列为 AUTOINCREMENT 不同的是,自增字段会记录上次用到的自增值(由系统自动记录在额外的一张 sqlite_sequence 表中),下次新增时候就以该值 +1。而默认的非自增整形主键,是会查询当前表中最大的 id 值来进行 +1。举个例子,如果表中已有 id = 1、2、3、4、5 五行数据,然后删除了 id = 4、5 这两行后。对于非自增整形主键,下次插入的 id 会是 4;对于自增主键,下次插入的 id 会是 6。

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top