Swift and CoreData. Or how to build Swift ORM based on Objective-C ORM

Habr, hello! My name is Geor, and I am developing iOS projects at Prisma Labs. As you probably understood, today we will talk about the cordata and many of you got bored already at this moment. But do not rush to despair, as we will talk mostly about the magic of Swift and about metal. Joke - about metal another time. The story will be about how we defeated the NSManaged boilerplate, reinvented migrations and made the cordata great again.





Developers, come on.





A few words about motivation

It is difficult to work with a cordata. Especially in our swift time. This is a very old framework that was created as a data layer with an emphasis on I / O optimization, which by default made it more complex than other ways of storing data. But the productivity of iron over time has ceased to be a bottleneck, and the complexity of the cordata, alas, has not gone anywhere. In modern applications, many people prefer other frameworks to cordata: Realm, GRDB (top), etc. Or they just use files (why not). Even Apple in new tutorials uses Codable serialization / deserialization for persistence.





Despite the fact that the API cordata was periodically replenished with various convenient abstractions (for example, NSPersistentContainer), developers should still monitor the life cycle of NSManaged objects, do not forget to read / write on the queue of the context to which these objects are attached and, of course, swear every time when something goes wrong. And surely in many projects there is a duplicate set of domain-level models and code for converting between them and their NSManaged pairs.





But cordata also has many advantages - a powerful visual editor for the data schema, automatic migrations, a simplified (compared to SQL) query system, secure multi-threaded access to data, and so on.





In Prism, we wrote a simple and powerful framework that allows you to forget about the drawbacks of cordata and at the same time use all the power of its light side.





- Sworm.





- , .





NSManagedObject- CoreData-

NSManagedObject' key-value . , , KV- . 3 :

















- :





struct Foo {
    static let entityName: String = "FooEntity"
}
      
      



"" - . , :





Foo.entityName
      
      



, destination-, . . -, , NSManageObject , , , -, Relation<T: >(name: String), , . , - . , 1:





protocol ManagedObjectConvertible {
    static var entityName: String { get }
}
      
      



:





Relation<T: ManageObjectConvertible>(name: String)
      
      



:





struct Foo: ManageObjectConvertible {
    static var entityName: String = "FooEntity"

    static let relation1 = Relation<Bar1>(name: "bar1")
    static let relation2 = Relation<Bar2>(name: "bar2")
}
      
      



() , , ? . -, , -, , Relation - one/many/orderedmany, , . , . , . , - 2:





protocol ManagedObjectConvertible {
    associatedtype Relations

    static var entityName: String { get }
    static var relations: Relations { get }
}
      
      



, :





struct Foo: ManageObjectConvertible {
    static let entityName: String = "FooEntity"

    struct Relations {
        let relation1 = Relation<Bar1>(name: "bar1")
        let relation2 = Relation<Bar2>(name: "bar2")
    }

    static let relations = Relations()
}
      
      



- :





extension ManagedObjectConvertible {
    func relationName<T: ManagedObjectConvertible>(
        keyPath: KeyPath<Self.Relations, Relation<T>>
    ) -> String {
        Self.relations[keyPath: keyPath].name
    }
}
      
      



- , :)





-

.





, "" , , , . , : . , WritableKeyPath + String key. , , - , .





Attribute<T>, T - . `[Attribute<T>]` T Self. , - 3:





public protocol ManagedObjectConvertible {
    associatedtype Relations

    static var entityName: String { get }
    static var attributes: [Attribute<Self>] { get }
    static var relations: Relations { get }
}
      
      



Attribute. , / KV-. , :





final class Attribute<T: ManagedObjectConvertible, V> {
    let keyPath: WritableKeyPath<T, V>
    let key: String

    ...

    func update(container: NSManagedObject, model: T) {
        container.setValue(model[keyPath: keyPath], forKey: key)
    }

    func update(model: inout T, container: NSManagedObject) {
        model[keyPath: keyPath] = container.value(forKey: key) as! V
    }
}
      
      



, [Attribute<T, V>] - . V , ? , :





final class Attribute<T: ManagedObjectConvertible> {
    ...

    init<V>(
        keyPath: WritableKeyPath<T, V>,
        key: String
    ) { ... }

    ...
}
      
      



V . , , BFG - :





final class Attribute<T: ManagedObjectConvertible> {
    let encode: (T, NSManagedObject) -> Void
    let decode: (inout T, NSManagedObject) -> Void

    init<V>(keyPath: WritableKeyPath<T, V>, key: String) {
        self.encode = {
            $1.setValue($0[keyPath: keyPath], forKey: key)
        }

        self.decode = {
            $0[keyPath: keyPath] = $1.value(forKey: key) as! V
        }
    }
}
      
      



. NSManagedObject , NSManagedObject', , .





- 4, :





protocol ManagedObjectConvertible {
    associatedtype Relations

    static var entityName: String { get }
    static var attributes: Set<Attribute<Self>> { get }
    static var relations: Relations { get }

    init()
}
      
      



- NSManagedObject', .





.





- bool, int, double, string, data, etc. Transformable, . .





-:





Bool, Int, Int16, Int32, Int64, Float, Double, Decimal, Date, String, Data, UUID, URL





: , .





:





protocol PrimitiveAttributeType {}

protocol SupportedAttributeType {
    associatedtype P: PrimitiveAttributeType

    func encodePrimitive() -> P

    static func decode(primitive: P) -> Self
}
      
      



SupportedAttributeType Attribute





final class Attribute<T: ManagedObjectConvertible> {
    let encode: (T, NSManagedObject) -> Void
    let decode: (inout T, NSManagedObject) -> Void

    init<V: SupportedAttributeType>(keyPath: WritableKeyPath<T, V>, key: String) {
        self.encode = {
            $1.setValue($0[keyPath: keyPath].encodePrimitive(), forKey: key)
        }

        self.decode = {
            $0[keyPath: keyPath] = V.decode(primitive: $1.value(forKey: key) as! V.P)
        }
    }
}
      
      



Transformable, objc-.





NSManagedObject- - , , .





ManagedObjectConvertible . , data access DAO, DTO - data transfer .





NSManaged

NSManaged- DAO DTO, + DAO+DTO, , , NSManagedObject , . NSManaged- . DTO + ( ManagedObjectConvertible). :





+ raw NSManaged- + X = DAO+DTO





NSManaged raw - .





X - , , NSManaged-.





:





final class ManagedObject<T: ManagedObjectConvertible> {
    let instance: NSManagedObject

    ...
}
      
      



NSManaged , .





- , dynamicMemberLookup Swift.





ManagedObjectConvertible , - . , Keypaths . ManagedObject:





final class ManagedObject<T: ManagedObjectConvertible> {
    ...

    subscript<D: ManagedObjectConvertible>(
        keyPath: KeyPath<T.Relations, Relation<D>>
    ) -> ManagedObject<D> {
        let destinationName = T.relations[keyPath: keyPath]

        //     NSManaged API

        return .init(instance: ...)
    }
}
      
      



, , :





managedObject[keyPath: \.someRelation]







, - dynamicMemberLookup



:





@dynamicMemberLookup
final class ManagedObject<T: ManagedObjectConvertible> {
    ...

    subscript<D: ManagedObjectConvertible>(
        dynamicMember keyPath: KeyPath<T.Relations, Relation<D>>
    ) -> ManagedObject<D> { ... }
}
      
      



:





managedObject.someRelation







, - .





, :





"foo.x > 9 AND foo.y = 10"



\Foo.x > 9 && \Foo.y == 10



"foo.x > 9 AND foo.y = 10"







Attribute Equatable Comparable . .





>. KeyPath , - . \Foo.x > 9 "x > 9". - . ">". ManagedObjectConvertible Foo , . , :





final class Attribute<T: ManagedObjectConvertible> {
    let key: String
    let keyPath: PartialKeyPath<T>

    let encode: (T, NSManagedObject) -> Void
    let decode: (inout T, NSManagedObject) -> Void

    ...
}
      
      



, WritableKeyPath PartialKeyPath. , , Hashable. , , , .





, , KV-.





, . , Equatable / Comparable. , , (. SupportedAttributeType).





, , Equatable / Comparable:





func == <T: ManagedObjectConvertible, V: SupportedAttributeType>(
    keyPath: KeyPath<T, V>,
    value: V
) -> Predicate where V.PrimitiveAttributeType: Equatable {
    return .init(
        key: T.attributes.first(where: { $0.keyPath == keyPath })!.key,
        value: value.encodePrimitiveValue(),
        operator: "="
    )
}
      
      



Predicate - , .





. AND. "(\(left)) AND (\(right))"



.





, , swift .





, . , - , .





, Sworm , .





!








All Articles