AHDataModel 0.5.1

AHDataModel 0.5.1

测试测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2017年10月
SwiftSwift版本3.0
SPM支持SPM

Andy Tong维护。



  • 作者
  • ivsall2012

AHDataModel

如果你喜欢 SQLite,那么这个包装器就是为你准备的!
它抽象了许多与 SQL 相关的繁琐细节,但又保持了灵活性和可定制性。

内容

建模

(这可能有点复杂。请耐心等待,好东西很快就会到来!)
AHDataModel 适用于您的模型需要实现的最少5个方法

#### Three Core Methods
/// In method, you need to privide necessary column(or property) infomations.
static func columnInfo() -> [AHDBColumnInfo]

/// Core method, it's for AHDataModel to create models when data being queried from a SQLite database.
init(with dict: [String: Any])

/// Core method, it's for saving model's datas into database.
/// You can intentionally ignore some properties in here by not assigning a key-value pair to the returning dict.
func toDict() -> [String: Any]
###

/// The table name for this model
static func tableName() -> String

/// Database file path
static func databaseFilePath() -> String

AHDataModel 协议真正关注的是上面提到的三个核心方法提供的信息。
确保您正确处理

init(with dict: [String: Any?])
func toDict() -> [String: Any]

以及由这两种方法产生的信息与您在提供的信息

static func columnInfo() -> [AHDBColumnInfo]

相匹配,那么一切都将正常。

示例

以下示例适用于具有非空属性的模型。代码看似长,但实际上并不复杂。您可以轻松快速地阅读它们:)
注意:每个模型都必须有一个主键!
稍后将向您展示一个模型实际上没有与主键有任何关联的情况。

struct User: Equatable {
var id: Int
var firstName: String
var lastName: String
var age: Int
var isVIP: Bool
/// Optional properties can be nil then being inserted or updated into database with NULL value.
/// Don't specify it as 'NOT NULL' in columnInfo's contraint.
var balance: Double?

/// If we want this property to have nothing to do with the database, we simplely just ignore it in the protocol methods.
var position: String = "PM"

/// Like normal struct, you use an initializer to create it, you can insert it into the database later.
public init(id: Int, firstName: String, lastName: String, age: Int, isVIP: Bool, balance: Double?) {
self.id = id
self.firstName = firstName
self.lastName = lastName
self.age = age
self.isVIP = isVIP
self.balance = balance
}



/// Here we assume the id is unique and will be our primary key
public static func ==(lhs: User, rhs: User) -> Bool {
return lhs.id == rhs.id
}
}

/// Imlement AHDataModel protocol
extension User: AHDataModel {
/// Core method, handling data coming from database
init(with dict: [String : Any?]) {
self.id = dict["id"] as! Int
self.firstName = dict["firstName"] as! String
self.lastName = dict["lastName"] as! String
self.age = dict["age"] as! Int
/// NOTE: You have to use Bool() to convert an integer to boolean value.
/// See columnInfo() for how to define boolean property.
self.isVIP = Bool(dict["isVIP"])!
/// Even though 'balance' is optional, we can ignore it in any of the three core methods.
self.balance = dict["balance"] as? Double
}

/// There are 3 types for columns(or properties): text(or String), real(or Double), integer(or Int).
/// NOTE: Every model must have a primary key!
/// Will show you the case when a model indeed doesn't have anything to do with a primary key later.
static func columnInfo() -> [AHDBColumnInfo] {
/// add constraint terms as you use SQLite before.
let id = AHDBColumnInfo(name: "id", type: .integer, constraints: "primary key")
let firstName = AHDBColumnInfo(name: "firstName", type: .text, constraints: "not null")
let lastName = AHDBColumnInfo(name: "lastName", type: .text)
let age = AHDBColumnInfo(name: "age", type: .integer)

/// Since SQLite can't represent a boolean value in the database, you use integer instead.
let isVIP = AHDBColumnInfo(name: "isVIP", type: .integer)
/// Even though 'balance' is optional, we can ignore it in any of the three core methods.
let balance = AHDBColumnInfo(name: "balance", type: .real)

return [id,firstName,lastName,age,isVIP,balance]
}
/// This method is for converting the Swift Struct model into a dict data so that AHDataModel can manipulate it in database level.
func toDict() -> [String : Any] {
var dict = [String: Any]()
dict["id"] = self.id
dict["firstName"] = self.firstName
dict["lastName"] = self.lastName
dict["age"] = self.age
/// Don't need to convert the boolean value to integer.
dict["isVIP"] = self.isVIP
/// Even though 'balance' is optional, we can ignore it in any of the three core methods.
dict["balance"] = self.balance
return dict
}

/// Use the struct's name as the table name
static func tableName() -> String {
return "\(self)"
}
/// Return a path in the cache directory
static func databaseFilePath() -> String {
return (NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! as NSString).appendingPathComponent("db.sqlte")
}

}

现在让我们使用 User 模型。

/// Every model has a write closure for globel transaction. All write operations have to be executed in a write closure.
/// NOTE: currently there's no difference for which model's write closure to use. They all share the same queue. If you want, you can use some other model's write closure, but not recommended.
/// More info described in the 'Write' section later.
/// That's why the operations in the write block is like a exclusive transaction in database level, not table level.
User.write {
let user1 = User(id: 42, firstName: "Michael", lastName: "Jackson", age: 29, isVIP: true, balance: nil)
/// insert user1 with a nil balance.
try! User.insert(model: user1)

var user1_copy = User.query(byPrimaryKey: 42)!
if user1_copy.balance == nil {
print("balance is nil")
}
user1_copy.balance = 9999999.0
try! User.update(model: user1_copy)

// reassign value to user1_copy
user1_copy = User.query(byPrimaryKey: 42)!
if user1_copy.balance != nil {
print("balance is not nil")
}
}

现在我们创建一个仅包含 3 个主要属性(text、userId、addedAt)的 chat message 模型。但由于每个模型都必须有一个主键,我们必须给它一个 'id' 属性,但它只是放在那里。

/// Remember to implement Equatable for a struct, always!
struct Chat: Equatable {
/// Since we don't are about primary key for a chat message. We only query them by their userId. So we give it an optional so that we don't have to put it in the initializer, or in this case, use Swift's implicit struct initializer.
var id: Int?
var text: String
var userId: Int

/// This is a custom initializer, NOT the AHDataModel's core init method!!
/// It's for the convenience to create them.
init(text: String, userId: Int) {
self.text = text
self.userId = userId
}

public static func ==(lhs: Chat, rhs: Chat) -> Bool {
return lhs.text == rhs.text && lhs.userId == rhs.userId
}
}

/// AHDataModel implementaion
extension Chat: AHDataModel {
init(with dict: [String : Any]) {
/// Though we will not be using the id property, but we still have to put it there!!
/// It will be treated as an implicit primary key(rowid) in SQLite.
self.id = dict["id"] as? Int
self.text = dict["text"] as! String
self.userId = dict["userId"] as! Int
}

static func columnInfo() -> [AHDBColumnInfo] {
/// Though we will not be using the id property, but we still have to put it there!!
/// It will be treated as an implicit primary key(rowid) in SQLite.
let id = AHDBColumnInfo(name: "id", type: .integer, constraints: "primary key")
let text = AHDBColumnInfo(name: "text", type: .text)

/// NOTE: Since userId here is a foregin key, when the corresponding user gets deleted, the related chats would be deleted too.
let userId = AHDBColumnInfo(foreginKey: "userId", type: .integer, referenceKey: "id", referenceTable: "\(User.tableName())")
return [id,text,userId]
}

func toDict() -> [String : Any] {
var dict = [String: Any]()
/// Again:
/// Though we will not be using the id property, but we still have to put it there!!
/// It will be treated as an implicit primary key(rowid) in SQLite.
dict["id"] = self.id
dict["text"] = self.text
dict["userId"] = self.userId
return dict
}
static func databaseFilePath() -> String {
return (NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! as NSString).appendingPathComponent("db.sqlte")
}

static func tableName() -> String {
return "\(self)"
}
}

现在我们使用 Chat 模型以及 User 模型。

let chat1 = ChatModel(text: "There's a place ... chat_1", userId: 42)
let chat2 = ChatModel(text: "in your heart ... chat_2", userId: 42)
let chat3 = ChatModel(text: "and I know that ... chat_3", userId: 42)
let chat4 = ChatModel(text: "it is ... chat_4", userId: 42)
let chat5 = ChatModel(text: "love ... chat_5", userId: 42)

/// If all the insertions succeeded, the batch insert method would return 0 count.
/// If one of them failed, the method would return the unsuccessfully inserted ones.
let count = ChatModel.insert(models: [chat1,chat2,chat3,chat4,chat5])
if count == 0 {
print("batch insert succeeded!")
}
/// query chats for userId = 12
/// We don't need the id primary key at all. But we still have to put it there:)
var chats = ChatModel.query("userId", "=", 12).run()

建模就到这里。

如果您需要更多关于建模的示例,您可以克隆或下载此存储库,其中包含一个示例项目,其中包含多个样本模型以及大量的测试。您也可以从它们中学到东西!

此外,AHFMDataCenter 有一些关于分割数据模型的演示模型。
例如,所有来自您服务器的数据都存储在一个模型中。希望它们不是太多,否则您需要将它们分类并放入不同的模型中。
并且您应该将这些本地管理属性分类到不同的模型中,例如 PlayerItemDownloadInfo,用于存储下载进度及其本地文件路径。

查询

查询很简单。阅读以下示例,您就可以开始使用了。

let chats = ChatModel.query("userId", "=", 55).OrderBy("userId", isASC: true).run()

/// Don't need to .run()
let dog = Dog.query(byPrimaryKey: 11)

var masters = Master.query("age", "IS NOT", nil).run()
masters.aster.query("age", "IS", nil).run()
masters = Master.query("age", "IN", [33,66,88]).run()

masters = Master.queryAll().OrderBy("id", isASC: false).Limit(2).run()
masters = Master.queryAll().OrderBy("id", isASC: false).Limit(3, offset: 2).run()
masters = Master.query("name", "LIKE", "fun%").AND("age", "<=", "77").AND("score", ">", 65).OrderBy("score", isASC: false).run()

写操作

所有写操作必须在写封闭期内执行,包括插入、更新和删除。

插入

只有在数据库中没有与相同主键重复的记录时,插入操作才会成功。
两种插入方法

/// Single insertion, throws
public static func insert(model: Self) throws

/// Batch insertion, return those unsuccessfully inserted ones.
/// NOTE: This method surpresses exceptions!!
public static func insert(models: [Self]) -> [Self]

示例

let dog1 = Dog(masterId: master.id, name: "dog_1", age: 12)
let dog2 = Dog(masterId: master.id, name: "dog_2", age: 12)
let dog3 = Dog(masterId: master.id, name: "dog_3", age: 12)
/// this could be Master.write{}, but you are doing things related to Dog, why use Master? Though both closure are identical.
Dog.write{
/// Return value can be ignored
let ones = Dog.insert(models: [dog1,dog2,dog3])

if ones.count == 0 {
// all of the models are successfully inserted
}else{
// there's at least one model failed to be inserted, which most of the time, due to duplication.
// you can do updates here to make sure that old values to be overridden, if needed.
}
}

更新

只有当已经存在具有相同主键的记录时,更新操作才会成功。
四种更新方法

public static func update(model: Self) throws

/// Return those unsuccessfully updated ones.
/// NOTE: This method surpresses exceptions!!
public static func update(models: [Self]) -> [Self]

/// Update specific properties of this model into the database
/// Note: This will override existing values.
public static func update(model: Self, forProperties properties: [String]) throws

/// Update specific properties of this model into the database
/// Note1: This will override existing values.
/// Note2: You can't set a property to nil for now since dict cannot contain nil value. Use the model method to set a property to nil if you already have one.
public static func update(byPrimaryKey primaryKey: Any, forProperties properties: [String : Any]) throws

前两种更新方法类似于两种插入方法——单条插入或批量插入,会抛出异常或返回失败的对象。

最后两种用于更新特定属性,或者部分更新。
对于‘update(model: Self, forProperties properties: [String]) throws’,你需要至少一个模型,这可以只是一个新创建的或通过查询得到的模型。
然后你为属性赋予一些新的值,然后使用该方法进行部分更新。
你可能会问,为什么不使用第二种更新方法呢?
原因是,有时候你只是想更新一些属性,然后迅速切换去做其他事情,直到某个时候你已经收集了所有信息,现在可以进行完整的更新。

对于最后的更新方法,你事先不需要模型,你只需要知道主键的值和你需要更新的键值对。
此方法的短处在于,它不能将属性更新为nil,因为键值对字典不允许包含nil值。所以在这种情况下,请使用前面的更新方法(先查询模型)。

Dog.write {
/// assumeing the name of the dog is the primary key
var dog42 = Dog.query(byPrimaryKey: 42)!
dog42.age = 99
dog42.masterId = 122
try! Dog.update(model: dog42, forProperties: ["masterId", "age"])

try! Dog.update(byPrimaryKey: 42, forProperties: ["masterId": 122, "age": 99])
}

删除

有五种删除方法

public static func delete(model: Self) throws

/// Return unsuccessfully deleted ones.
public static func delete(models: [Self]) -> [Self]

public static func delete(byPrimaryKey primaryKey: Any) throws

/// Returns unsuccessfully deleted primary keys
public static func delete(byPrimaryKeys primaryKeys: [Any]) -> [Any]

/// Internally, it will drop the table containing the data model
public static func deleteAll() throws

它们基本上是自我解释的。
示例没有提供。你可以查看示例项目的测试来了解更多。

事务

AHDataModel的事务功能来自其超协议AHDB。
如果你想要深入了解,可以查看AHDB协议。
一般来说,所有模型的.write{}封闭方法能满足大多数情况。

Dog.write {
// do stuff here
}

再次强调,该封闭方法与任何模型无关。你只需要一个模型来调用.write{}。
你甚至可以将AHDB与任何结构或类遵守,并从那里调用.write{}。

实际上,.write{}方法是一个“假的”事务。它不是一个具有回滚和其他花哨功能的数据库事务。
此方法在这里所做的是,你调用方法时创建的封闭被异步分派到内置队列并执行那里的代码。由于所有写操作都强制使用.write{},因此你在这个封闭内查询的所有数据都保证了原子性。
有些人可能会说,所有的写操作都在一个队列中,这会不会成为性能问题?
NO!你正在构建iOS移动应用程序,而不是后端服务器。你不应该一次写入太多数据,例如,一次性将1000条记录加载到内存中使用。NO!

迁移

添加和/或删除属性

AHDataModel实现了一个半自动化迁移过程——如果你只是删除或添加列(属性)而不使用任何旧数据,你不必做任何事情。
例如,如果一个Dog模型在初始版本中有一个masterId,它是零。然后你决定释放所有狗并删除Dog模型中与masterId相关的所有信息。在这种情况下,你需要做的只是

/// Do the migration within 'application didFinishLaunchingWithOptions'
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
/// Specify the toVersion to 1, since the initial version by default is 0.
try! Dog.migrate(toVersion: 1, migrationBlock: { (migrator, newProperty) in
// do nothing here
})
}

相同的代码也应用于添加属性。
注意:请记住在模型结构(或类)中删除或添加属性,
并且修改AHDataModel的三个核心方法
‘columnInfo() -> [AHDBColumnInfo]’
‘init(with dict: [String: Any?])’
‘toDict() -> [String: Any]’

在迁移过程中处理旧数据

在这种情况下,主要是在添加属性时,你想对旧数据进行一些操作,例如将一些数字聚合形成新属性的一个值,将‘firstName’、‘lastName’合并成‘fullName’属性。

重命名属性
/// Migrate property name 'sex' to 'gender'
try! User.migrate(ToVersion: 1, migrationBlock: { (migrator, newProperty) in
/// Check the newProperty is actually the one you want to do something about it
if newProperty == "gender" {
/// Using migrator's built-in method to do the work
migrator.renameProperty(from: "sex")
}

})
组合两个字符串属性
/// if the 'firstName' is "Michael", 'lastName' is "Jackson", then the newProperty 'fullName' will be "Jackson, Micheal"
try! User.migrate(toVersion: 1) { (migrator, newProperty) in
if newProperty == "fullName" {
/// the separator is the one between propertyA and propertyB
migrator.combineProperties(propertyA: "lastName", separator: ", ", propertyB: "firstName")
}

}
扩展迁移器以进行高级使用

如图所示,大部分的迁移工作都是通过使用迁移器内置的方法完成的。
那么,如果你想在与迁移过程中进行自定义操作呢?扩展迁移器。
迁移器有 4 个属性

public let oldTableName: String
public let tempTableName: String
/// This is the newProperty name, the same property name as the one passed in the migration closure paramter shown previously.
public let property: String
public let primaryKey: String

现在假设,我们想要为用户模型添加一个 'msgCount' 属性。这个 'msgCount' 是与 userId 等于 user.id 的所有聊天记录的数量。
下面是原始的 SQL 代码

-- tempTableName is the intermediate table's name for migrating. It will be changed to the original table' name later.
"UPDATE tempTableName SET 'msgCount' = (SELECT count(*) FROM Chat WHERE tempTableName.id = Chat.userId)"

在 Swift 中

extension Migrator {
func aggreateMsgCount(chatTableName: String, chatPrimaryKeyName: String) {
/// NOTE: the new property name is self.property
let sql = "UPDATE \(self.tempTableName) SET \(self.property) = (SELECT count(*) FROM \(chatTableName) WHERE \(chatTableName).\(chatPrimaryKeyName) = \(self.tempTableName).\(self.primaryKey))"
migrator.runRawSQL(sql: sql)
}
}

/// Then use the extension method
try! User.migrate(toVersion: 1) { (migrator, newProperty) in
/// NOTE: newProperty == migrator.property
if newProperty == "msgCount" {
migrator.aggreateMsgCount(chatTableName: "Chat", chatPrimaryKeyname: "userId")
}

}

示例

要运行示例项目,首先克隆仓库,然后从 Example 目录运行 pod install
您可以从项目中更多的测试中获得更多信息。
此外,一些示例模型来自 AHFMDataCenter

要求

安装

AHDataModel 通过 CocoaPods 提供。要安装它,只需将以下行添加到您的 Podfile
即可

pod 'AHDataModel'

作者

Andy Tong, [email protected]

许可证

AHDataModel 在 MIT 许可下可用。有关更多信息,请参阅 LICENSE 文件。