Swift e CoreData. Ou como construir Swift ORM baseado em Objective-C ORM

Habr, olá! Meu nome é Geor, e estou desenvolvendo projetos iOS na Prisma Labs. Como vocês provavelmente entenderam, hoje falaremos sobre a cordata e muitos de vocês já se cansaram neste momento. Mas não se desespere, pois falaremos principalmente sobre a magia de Swift e sobre o metal. Piada - sobre metal em outro momento. A história será sobre como derrotamos o boilerplate NSManaged, reinventamos as migrações e tornamos a cordata ótima novamente.





Desenvolvedores, vamos.





Algumas palavras sobre motivação

É difícil trabalhar com uma cordata. Especialmente em nosso tempo rápido. Esta é uma estrutura muito antiga que foi criada como uma camada de dados com ênfase na otimização de E / S, que por padrão a tornou mais complexa do que outras formas de armazenamento de dados. Mas a produtividade do ferro com o tempo deixou de ser um gargalo e a complexidade da cordata, infelizmente, não foi a lugar nenhum. Em aplicativos modernos, muitas pessoas preferem outros frameworks ao cordata: Realm, GRDB (topo), etc. Ou eles apenas usam arquivos (por que não). Até mesmo a Apple em novos tutoriais usa serialização / desserialização codificável para persistência.





, (. NSPersistentContainer), - NSManaged , / , , - . NSManaged-.





- , , ( SQL) , .





, .





- 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