对值类型使用领域

在本文中,我想探讨如何使用基于struct的模型和类型安全的查询在Realm之上构建数据持久层

如果您在过去的两年中一直被碳酸盐岩所冻结,那么Realm是一种从头开始为移动设备构建的数据库技术,它实际上是Apple世界中Core Data的替代品。

与Core Data一样,Realm需要子类化来定义模型对象,并且仅允许在创建它们的线程上使用它们。 这些要求和其他要求通常会增加复杂性,并且通常会在应用程序中的任何地方增加与Realm的耦合。

这就是为什么将这些框架视为实现细节并将它们抽象到持久层下通常是一个好主意的原因。

楷模

例如,让我们创建一个持久层,以将漫画书中的超级英雄角色与其发布者一起存储。 我们可以使用几个结构来定义我们的模型:

  公共结构发布者{ 
公共租用标识符:Int
公开名称:String
}
  公共结构字符{ 
公共租用标识符:Int
公开名称:String
public let realName:字符串
公开让发布者:发布者?
}

尽管我们仅将它们仅用作传输机制并将它们对应用程序的其余部分隐藏,但我们仍然需要创建相应的RealmSwift.Object子类。

  最后一个类 PublisherObject:对象{ 
动态var标识符= 0
动态var名称=“”
  覆盖静态函数 primaryKey()->字符串?  { 
返回 “标识符”
}
}
  最终课程 CharacterObject:对象{ 
动态var标识符= 0
动态var名称=“”
动态var realName =“”
动态var发布者:PublisherObject?
  覆盖静态函数 primaryKey()->字符串?  { 
返回 “标识符”
}
}

制图

我们需要一种机制来将基于struct的模型转换为其相应的Realm对象。 让我们为此定义一个协议:

  公共协议持久{ 
  relatedtype ManagedObject:RealmSwift.Object 
  初始化 (managedObject:ManagedObject) 
  funcmanagedObject()-> ManagedObject 
}

符合Persistable任何类型都可以在对应的RealmSwift.Object子类中进行序列化。

这是使我们的Character模型符合Persistable的代码:

  扩展字符:Persistable { 
  公共初始化 (managedObject:CharacterObject){ 
标识符= managedObject.identifier
名称= managedObject.name
realName = managedObject.realName
发布者= managedObject.publisher
.flatMap(Publisher.init(managedObject :))
}
  public funcmanagedObject()-> CharacterObject { 
角色= CharacterObject()
  character.identifier =标识符 
character.name =名称
character.realName = realName
character.publisher = Publisher?.managedObject()
  返回字符 
}
}

创建

有了这些工具,我们准备实现持久层的插入方法。

但是首先,让我们回顾一下如何使用Realm插入对象:

  //创建发布者对象 
发布者= PublisherObject()
Publisher.id = 1
Publisher.name =“惊奇”
  //获取默认领域 
境界= 尝试! 领域()
  //添加到领域 
尝试! realm.write {
realm.add(发布者)
}

我们可以在此处抽象出几件事。 一方面,我们拥有数据库本身,另一方面,我们可以在写事务中执行操作。

即使Realm对象提供了这两个概念,我们WriteTransaction它们封装为单独的类型:分别为ContainerWriteTransaction

让我们从WriteTransaction类型开始:

  公共最终课程 WriteTransaction { 
  私人租赁领域:Realm 
  内部初始化 (领域:领域){ 
自我 .realm =领域
}
  public func add (_值:T,更新:Bool){ 
realm.add(value.managedObject(),更新:更新)
}
}

注意,我们将构造函数设置为internal ,因为只有Container会创建此类的实例。 我们可以添加到WriteTransaction接口的其他操作是deleteupdate

实施Container非常简单:

  公共最终课程容器{ 
  私人租赁领域:Realm 
  公共便利init ()引发{ 
尝试 自我初始化 (领域:Realm())
}
  内部初始化 (领域:领域){ 
自我 .realm =领域
}
  公共功能写( _块:(WriteTransaction) 抛出 ->无效) 
引发 {
交易= WriteTransaction(领域:领域)
尝试 realm.write {
试试块(交易)
}
}
}

现在,我们有了最小的基础结构来插入CharacterPublisher值:

   character = Character( 
标识符:1455,
名称:“钢铁侠”,
realName:“托尼·史塔克”,
发布者:Publisher(标识:1,名称:“ Marvel”)
  容器= 尝试! 容器() 
  尝试!  container.write { 
transaction.add(字符)
}

到目前为止,这看起来不错。 我们已经成功地维持了基于struct的模型,并且在呼叫站点上与Realm没有任何联系。

部分更新

到目前为止,我们已经实现了什么,如果要更新数据库中的字符,我们必须提供完整的值集:

  //更新钢铁侠的真实姓名 
updateCharacter = Character(
标识符:1455,
名称:“钢铁侠”,
实名: “安东尼·爱德华·斯塔克”
发布者:Publisher(标识:1,名称:“ Marvel”)
  尝试!  container.write { 
transaction.add(updatedCharacter,update:true)
}

我们可以通过使Character结构可变来改善这种情况,但这仍然需要在更新模型之前获取模型。

幸运的是,Realm提供了一种通过传递值的子集和主键来部分更新对象的方法:

  尝试!  realm.write { 
realm.create(
CharacterObject。 自我
值:[
“标识符”:1455,
“ realName”: “ Anthony Edward Stark”
],
更新: true

}

我们如何以类型安全的方式传递值的子集?

我们可以使用带有关联值的enum来对不同的属性及其值进行建模。 通过使用这种方法,部分更新将如下所示:

  尝试!  container.write { 
transaction.update(Character。self,属性:[
.identifier(1455),
.realName( “ Anthony Edward Stark”
])
}

这似乎是个好主意,它将带来自动完成功能。 我们将需要添加一些东西来使其工作。

让我们从创建一个协议开始,该协议提供属性值的内部表示形式:

  公共类型别名 PropertyValuePair =(名称:字符串,值:任意) 
  公共协议 PropertyValueType { 
var propertyValuePair:PropertyValuePair { 获取 }
}

我们稍后将使用此协议来实现WriteTransaction.update

为提供enum类型向Persistable添加关联类型是有意义的,因为它将与模型本身紧密相关。

  公共协议持久{ 
  relatedtype ManagedObject:对象 
relatedtype PropertyValue:PropertyValueType
...
}

现在我们准备在WriteTransaction实现update方法:

  公共最终课程 WriteTransaction { 
...
公共功能更新(_类型:T.Type,值:
[T.PropertyValue]){
var字典:[String:Any] = [:]
  values.forEach { 
对= $ 0.propertyValuePair
字典[pair.name] = pair.value
}

realm.create(T.ManagedObject。self,value:dictionary,
更新:true)
}
}

该实现只对值进行迭代,查询内部表示并将其添加到传递给Realm.create的字典中。

最后,我们需要为Character属性值实现enum

  扩展字符{ 
  公共枚举 PropertyValue:PropertyValueType { 
案例标识符(整数)
大小写 realName(String)
...
  public var propertyValuePair:PropertyValuePair { 
切换自我 {
案例 .identifier( let id):
返回 (“标识符”,ID)
案例 .realName( realName):
返回 (“ realName”,realName)
...
}
}
}
}

现在,我们有了一种优雅且类型安全的方式来部分更新持久层中的对象。

查询方式

我们在持久层中仍然缺乏必要的功能: 获取数据

让我们回顾一下如何使用Realm获取数据:

  //所有Marvel角色均按名称排序 
结果= realm.objects(CharacterObject。self)
.filter(“ publisher.name == Marvel”)
.sorted(byProperty:“名称”)

这与我们在处理部分更新时面临的问题非常相似。 我们需要找到一种以类型安全的方式传递谓词和一组排序描述符的方法。

我们可能会发疯并为功能齐全的类型安全查询实现DSL,但让我们采用一种更为实用的方法并实现一些更简单的方法。

与部分更新一样,我们将使用带有关联值的enum来表示可应用于持久层中特定模型的不同查询。

首先,我们创建将提供查询内部表示的协议:

  公用协议 QueryType { 
var谓词:NSPredicate? {得到}
var sortDescriptors:[SortDescriptor] {get}
}

然后,我们向Persistable添加一个必须符合该协议的关联类型:

  公共协议持久{ 
relatedtype ManagedObject:对象
relatedtype PropertyValue:PropertyValueType
关联类型查询:QueryType
...
}

回想一下,Realm将返回Results作为查询结果。 我们需要创建一个包装这些结果并将其映射到基于struct的模型的新类型:

  公共最终课程 FetchedResults  { 
  内部let结果:Results  
  public var count:Int { 
返回 results.count
}
  内部初始化 (结果:Results ){ 
自我结果=结果
}
  公共函数值(在索引处:Int)-> T { 
返回 T(managedObject:results [index])
}
}

现在我们准备在Container中添加一个返回查询结果的方法

  公共函数值(_类型:T.Type,匹配查询:T.Query)-> FetchedResults  { 
var结果= realm.objects(T.ManagedObject。self)
  如果让谓词= query.predicate { 
结果= results.filter(谓词)
}
 结果= results.sorted(通过:query.sortDescriptors) 
  返回 FetchedResults(结果:结果) 
}

让我们为Character查询创建一种类型,并实现一个按发布者名称过滤的类型:

  扩展字符{ 
  公共枚举查询:QueryType { 
案例 PublisherName(String)
  公共变量谓词:NSPredicate?  { 
切换自我 {
案例 .publisherName( let value):
返回 NSPredicate(格式:“ publisher.name ==%@”,
值)
}
}
  公共var sortDescriptors:[SortDescriptor] { 
返回 [SortDescriptor(property:“ name”)]
}
}
}

完成所有这些操作后,按发布者名称查询字符的外观如下:

  结果= container.values( 
字符。 自我
匹配:.publisherName(“奇迹”)

我想说这可以非常优雅地完成工作。

结论

我们已经成功地使用结构实现了一个简单的持久层,并将底层的持久性技术(Realm)与应用程序的其余部分解耦了。

请注意,如果您具有复杂的对象图并且依赖活动对象,则映射到值类型可能不是最佳解决方案。 在这些情况下,最好直接使用托管对象并对其进行处理或转换以进行呈现。