NSPersistentStore (Swift 4.0). Часть 1

coredata

#1

CoreData это фреймворк который отвечает за mdoel layer вашего MVC приложения и предоставляет инфраструктуру для изменения/сохранения/извлечения объектов из хранилища. Хранилище (NSPersistentStore) в свою очередь абстрагирует нас от реальных типов данных и методов работы с ними, и если нужно поменять тип данных достаточно просто поменять хранилище.

NSPersistentStore бывает двух типов:

  • Atomic (NSAtomicStore) - всё или ничего, то есть читает и или пишет все данные сразу
  • Incremental (NSIncrementalStore) - напротив позволяет сохранять/читать данные постепенно

CoreData имеет несколько встроенных хранилищ.

Atomic:

  • In-Memory Store (NSInMemoryStoreType)
  • XML Store (NSXMLStoreType) только macOS
  • Binary Store (NSBinaryStoreType)

Incremental:

  • SQLite Store (NSSQLiteStoreType)

Я решил разбить писанину на две части и в первой речь пойдёт о NSAtomicStore, во второй о NSIncrementalStore.

И так, NSAtomicStore позволят создать хранилище с любым типом данных (можно даже придумать свой) и вполне логичный выбор (на мой взгляд) это json, если вы будете грузить данные с сервера наверняка это будет json и json вполне легко читается (на случай если вы хотите лучше понять как работает CoreData).

Создадим JSONStore подкласс NSAtomicStore и документация рекомендует нам переопределись девять методов:

Начнём с metadata:

static let type = "JSONStore"
private static let uuid = UUID().uuidString

private var _metadata: [String : Any] = [NSStoreTypeKey: JSONStore.type, NSStoreUUIDKey: JSONStore.uuid]

override var metadata: [String : Any]! {
    get { return _metadata }
    set { _metadata = newValue }
}

Metadata как понятно из названия хранит некие метаданные, как минимум metadata должна содержать NSStoreTypeKey и NSStoreUUIDKey и тот и тот строка и уникальные для каждого хранилища (не экземпляра). Так же metadata содержит версию и хеш моделей, на случай миграции, если вы решите её реализовать, то metadata нужно писать в файл и загружать в методе metadataForPersistentStoreWithURL:error:. Type и identifier можно не переопределять они есть в metadata.

newReferenceObjectForManagedObject:

override func newReferenceObject(for managedObject: NSManagedObject) -> Any {
    return UUID().uuidString
}

Этот метод вызывается при сохранении контекста (managed object context) и должен возвращать уникальный (в приделах хранилища) объект для каждого NSManagedObject, может быть любое число, UUID, или другое случайное значение.

newCacheNodeForManagedObject:

override func newCacheNode(for managedObject: NSManagedObject) -> NSAtomicStoreCacheNode {
    let node = NSAtomicStoreCacheNode(objectID: managedObject.objectID)
    updateCacheNode(node, from: managedObject)
    return node
}

Должен возвращать новый NSAtomicStoreCacheNode для каждого соответствующего NSManagedObject. NSAtomicStoreCacheNode это NSManagedObject только на стороне хранилища и на стороне хранилища нужно работать именно с ним.

updateCacheNode:fromManagedObject:

private func getNode(for objectID: NSManagedObjectID) -> NSAtomicStoreCacheNode {
    if let node = cacheNode(for: objectID) {
        return node
    } else {
        let node = NSAtomicStoreCacheNode(objectID: objectID)
        addCacheNodes([node])
        return node
    }
}

override func updateCacheNode(_ node: NSAtomicStoreCacheNode, from managedObject: NSManagedObject) {
    let entity = managedObject.entity
    if node.propertyCache == nil { node.propertyCache = NSMutableDictionary() }
    node.propertyCache!.addEntries(from: managedObject.dictionaryWithValues(forKeys: Array(entity.attributesByName.keys)))
    node.propertyCache!.addEntries(from: managedObject.dictionaryWithValues(forKeys: Array(entity.relationshipsByName.keys)).mapValues { value -> Any in
        switch value {
        case let object as NSManagedObject:
            return getNode(for: object.objectID)
        case let objects as Set<NSManagedObject>:
            return Set(objects.map { getNode(for: $0.objectID) })
        default:
            return NSNull()
        }
    })
}

Тут нужно передать свойства из NSManagedObject в NSAtomicStoreCacheNode. У NSAtomicStoreCacheNode есть propertyCache где эти свойства хранятся, у NSManagedObject можно получить словарь свойств по ключам. Получаем свойства сначала по ключам атрибутов, потом по ключам связей, для связей (в mapValues) нужно по objectID получить NSAtomicStoreCacheNode (вместо NSManagedObject), при чём эти NSAtomicStoreCacheNode могут еще не быть в хранилище (контекст может сохранятся в любом порядке), по этому в getNode мы проверяем есть ли такой NSAtomicStoreCacheNode, если нет возвращаем новы и добавляем в хранилище.

save:

override func save() throws {
    let array = cacheNodes().map { node -> [String: Any] in
        var result = (node.propertyCache as! [String: Any]).mapValues { value -> Any in
            switch value {
            case is String, is Int16, is Int32, is Int64, is Double, is Float, is Bool, is NSNull:
                return value
            case let date as Date:
                return dateFormatter.string(from: date)
            case let data as Data:
                return data.base64EncodedString()
            case let node as NSAtomicStoreCacheNode:
                return referenceObject(for: node.objectID)
            case let nodes as Set<NSAtomicStoreCacheNode>:
                return nodes.map { referenceObject(for: $0.objectID) }
            default:
                fatalError()
            }
        }
        result["entityName"] = node.objectID.entity.name!
        result["referenceId"] = referenceObject(for: node.objectID)
        return result
    }
    let jsonData = try JSONSerialization.data(withJSONObject: array)
    try jsonData.write(to: url!)
}

В методе save, нам нужно записать в файл все имеющиеся в хранилище NSAtomicStoreCacheNode. Что бы их получить используем метод cachNodes, который возвращает массив NSAtomicStoreCacheNode. В map мы возвращаем словарь который берем у NSAtomicStoreCacheNode и в mapValues преобразуем его значения в совместимые с json, для связей достаточно записать их UUID которые можно получить по objectID.

load:

override func load() throws {
    guard FileManager.default.fileExists(atPath: url!.relativePath) else { return }
    
    let jsonData = try Data(contentsOf: url!)
    let array = try JSONSerialization.jsonObject(with: jsonData) as! [[String: Any]]
    
    let managedObjectModel = persistentStoreCoordinator!.managedObjectModel
    
    let nodes = array.map { dict -> NSAtomicStoreCacheNode in
        var dict = dict
        
        let entityName = dict.removeValue(forKey: "entityName") as! String
        let referenceId = dict.removeValue(forKey: "referenceId") as! String
        
        let entity = managedObjectModel.entitiesByName[entityName]!
        let node = NSAtomicStoreCacheNode(objectID: objectID(for: entity, withReferenceObject: referenceId))
        
        let objects = dict.flatMap { (key, value) -> (String, Any)? in
            if let attribute = entity.attributesByName[key] {
                switch (attribute.attributeType, value) {
                case (.integer16AttributeType, _), (.integer32AttributeType, _), (.integer64AttributeType, _), (.doubleAttributeType, _), (.floatAttributeType, _), (.booleanAttributeType, _):
                    return (key, value)
                case (.stringAttributeType, let str as String):
                    return (key, str)
                case (.dateAttributeType, let str as String):
                    return (key, dateFormatter.date(from: str)!)
                case (.binaryDataAttributeType, let str as String):
                    return (key, Data(base64Encoded: str)!)
                default: break
                }
            } else if let relationship = entity.relationshipsByName[key] {
                switch value {
                case let ids as [String]:
                    return (key, Set(ids.map { NSAtomicStoreCacheNode(objectID: objectID(for: relationship.destinationEntity!, withReferenceObject: $0)) }))
                case let id as String:
                    return (key, NSAtomicStoreCacheNode(objectID: objectID(for: relationship.destinationEntity!, withReferenceObject: id)))
                default: break
                }
            }
            return nil
        }
        node.propertyCache = NSMutableDictionary(objects: objects.map { $0.1 }, forKeys: objects.map { $0.0 as NSString })
        return node
    }
    addCacheNodes(Set(nodes))
}

Читаем json, знаем что он массив словарей и в map возвращаем NSAtomicStoreCacheNode, для этого перво наперво берем из словаря имя сущности и referenceId (UUID) по имени получаем сущность, objectID(for:withReferenceObject:) возвращает NSManagedObjectID с помощью которого создаём NSAtomicStoreCacheNode. Из того что осталось в словаре мы делаем массив кортежей, где в зависимости от атрибута или связей преобразуем данные (надеюсь не сильно сложный код для понимания). Заполняем propertyCach и последний штрих добавляем полученный массив в хранилище.

Создаём небольшой стек:

typealias CDContext = NSManagedObjectContext

extension CDContext {
    private class PersistentStoreCoordinator: NSPersistentStoreCoordinator {
        
        static let `default` = PersistentStoreCoordinator()
        
        lazy var viewContext: CDContext = {
            let context = CDContext(concurrencyType: .mainQueueConcurrencyType)
            context.persistentStoreCoordinator = self
            return context
        }()
        init() {
            let url = Bundle.main.url(forResource: "JSONStore", withExtension: "momd")!
            super.init(managedObjectModel: NSManagedObjectModel(contentsOf: url)!)
            
            let filePath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/db.json"
            print("file path: \(filePath)")
            NSPersistentStoreCoordinator.registerStoreClass(JSONStore.self, forStoreType: JSONStore.type)
            do {
                try addPersistentStore(ofType: JSONStore.type, configurationName: nil, at: URL(fileURLWithPath: filePath), options: nil)
            } catch {
                fatalError(error.localizedDescription)
            }
        }
    }
    static var view: CDContext {
        return PersistentStoreCoordinator.default.viewContext
    }
}

Тут ничего нового, единственное, перед тем как добавить хранилище, его нужно зарегистрировать.

Создаём сущность User у которого есть дети:

Создаём юзеров, сохраняем, смотрим что получилось:

let context = CDContext.view
for i in (0..<2) {
    let user = User(context: context)
    user.name = "User: \(i)"
    for i in (0..<2) {
        let child = User(context: context)
        child.name = "Child: \(i)"
        child.parent = user
    }
}
try? context.save()

На выходе, о чудо :clap::clap::clap: json:

[
  {
    "name" : "User: 1",
    "referenceId" : "0F7C453B-51C9-4C05-BE97-E6588F7BBC30",
    "childs" : [
      "039DD101-6F1D-4D0D-8400-3CE755270141",
      "78E85405-7645-415A-BCEC-D092C3BA72E8"
    ],
    "parent" : null,
    "entityName" : "User"
  },
  {
    "name" : "Child: 1",
    "referenceId" : "64761B71-392D-4C32-A1C1-6B863E20B54C",
    "childs" : [],
    "parent" : "068D15DE-54EA-4E49-9927-F6656102ACB4",
    "entityName" : "User"
  },
  {
    "name" : "Child: 1",
    "referenceId" : "039DD101-6F1D-4D0D-8400-3CE755270141",
    "childs" : [],
    "parent" : "0F7C453B-51C9-4C05-BE97-E6588F7BBC30",
    "entityName" : "User"
  },
  {
    "name" : "Child: 0",
    "referenceId" : "78E85405-7645-415A-BCEC-D092C3BA72E8",
    "childs" : [],
    "parent" : "0F7C453B-51C9-4C05-BE97-E6588F7BBC30",
    "entityName" : "User"
  },
  {
    "name" : "Child: 0",
    "referenceId" : "1974F371-4D4A-4BFF-9BBA-82FBDFFB3F12",
    "childs" : [],
    "parent" : "068D15DE-54EA-4E49-9927-F6656102ACB4",
    "entityName" : "User"
  },
  {
    "name" : "User: 0",
    "referenceId" : "068D15DE-54EA-4E49-9927-F6656102ACB4",
    "childs" : [
      "64761B71-392D-4C32-A1C1-6B863E20B54C",
      "1974F371-4D4A-4BFF-9BBA-82FBDFFB3F12"
    ],
    "parent" : null,
    "entityName" : "User"
  }
]

Если я что то коряво описал, скачать и поэкспериментировать можно здесь :slight_smile:


NSPersistentStore (Swift 4.0). Часть 2
#2

haymob, круто!! Красиво получилось!!