Несогласованность базы данных MagicalRecord с NSFetchedResultsController

Я использую MagicalRecord для своего проекта, и в моей базе данных есть объект CDSong, за который могут проголосовать несколько объектов CDVoter. Структура базы данных

Избиратели добавляются и удаляются в фоновом режиме с помощью NSManagedObjectContext.performAndWait(block:), вызываемого из очереди последовательной отправки. У меня есть NSFetchedResultsController, который извлекает CDSongs и отображает их избирателей (в этом простом сценарии он печатает только имена избирателей).

Все было бы хорошо, но я иногда получаю сбои в методе NSFetchedResultsControllerDelegate controllerDidChangeContent: - / Согласно моему анализу, в отношениях CDSong.voters появляются недопустимые пустые объекты CDVoter (name = nil, votedSong = nil). Эти пустые избиратели не возвращаются из CDVoter.mr_findAll().

Это код, имитирующий сбой (обычно после <20 нажатий кнопки приложение вылетает, потому что имя CDVoter равно нулю). Я что-то не так делаю с контекстами и сохранением? Поместите сюда весь тестовый код с инициализацией базы данных и frc, если кто-то хочет попробовать, но проблемная часть находится в методах controllerDidChangeContent и buttonPressed. Спасибо за вашу помощь :)

import UIKit
import CoreData
import MagicalRecord

class MRCrashViewController : UIViewController, NSFetchedResultsControllerDelegate {

    var frc: NSFetchedResultsController<NSFetchRequestResult>!
    let dispatchQueue = DispatchQueue(label: "com.testQueue")

    override func viewDidLoad() {
        super.viewDidLoad()

        self.initializeDatabase()
        self.initializeFrc()
    }

    func initializeDatabase() {

        MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.error)
        MagicalRecord.setupCoreDataStack()
        MagicalRecord.setLoggingLevel(MagicalRecordLoggingLevel.warn)

        if CDSong.mr_findFirst() == nil {
            for i in 1...5 {
                let song = CDSong.mr_createEntity()!
                song.id = Int16(i)
            }
        }
        NSManagedObjectContext.mr_default().mr_saveToPersistentStoreAndWait()
    }

    func initializeFrc() {
        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "CDSong")
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
        NSFetchedResultsController<NSFetchRequestResult>.deleteCache(withName: nil)
        self.frc = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: NSManagedObjectContext.mr_default(), sectionNameKeyPath: nil, cacheName: nil)
        self.frc!.delegate = self
        try! self.frc!.performFetch()
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        for song in controller.fetchedObjects! {
            print((song as! CDSong).voters!.reduce("", { $0 + ($1 as! CDVoter).name! }))
        }
        print("----");
    }

    @IBAction func buttonPressed(_ sender: Any) {
        for _ in 1...10 {
            self.dispatchQueue.async {
                let moc = NSManagedObjectContext.mr_()
                moc.performAndWait {
                    for song in CDSong.mr_findAll(in: moc)! {
                        let song = song as! CDSong
                        let voters = song.voters!
                        for voter in voters {
                            (voter as! CDVoter).mr_deleteEntity(in: moc)
                        }

                        for _ in 1...4 {
                            if arc4random()%2 == 0 {
                                let voter = CDVoter.mr_createEntity(in: moc)!
                                voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
                                voter.votedSong = song
                            }
                        }
                    }
                    moc.mr_saveToPersistentStoreAndWait()
                }
            }
        }
    }
}

Примечание. Я безуспешно пытался использовать MagicalRecord.save (blockAndWait :).


person Matej Vargovčík    schedule 30.08.2017    source источник
comment
Избиратели добавляются и удаляются в фоновом режиме с использованием очереди последовательной отправки. Вы не должны использовать свои собственные очереди для общих данных. Вы должны использовать методы NSManagedObjectContext PerformBlock только.   -  person    schedule 30.08.2017
comment
@Sneak Я использую методы performBlockAndWait для работы с основными данными, но вызываю их из очереди bg. Я думаю, что это нормально, по крайней мере, в docs не сказано, что я могу вызвать performBlockAndWait только из основного потока, они только говорят, что я должен вызывать метод из того же потока, в котором я создал контекст. Я должен использовать очередь bg, потому что performBlockAndWait будет блокировать графику, если вызывается из основного, а вызов выполнить только смешивает удаления и вставки (поверьте мне, просто попробовал). Спасибо, отредактирую предложение   -  person Matej Vargovčík    schedule 30.08.2017
comment
Удалил мои комментарии, которые помогли бы решить ваши проблемы, напечатав это вместо этого. так как вы так высокомерны с редактированием предложения и доверяете мне, вместо того, чтобы подвергать сомнению мою точку зрения и пытаться чему-то научиться. Я желаю вам удачи.   -  person    schedule 30.08.2017
comment
@Sneak Ой, извини, если это звучало так, я не хочу спорить, я поставил под сомнение ваши доводы, изучил документацию вокруг них и протестировал возможные решения, которые вызывали бы метод performBlock только из основной очереди (я использовал доверие чтобы выразить, что я не просто отвергал возможности, но действительно пробовал их). И я думаю, что редактирование вводящего в заблуждение предложения - нормальное или даже желаемое в stackoverflow, не так ли? Тем не менее, я собираюсь попробовать несколько новых идей, основанных на ваших удаленных комментариях, и дам вам знать, сработали ли они ...   -  person Matej Vargovčík    schedule 30.08.2017
comment
не беспокойтесь, если вы хотите избежать блокировки своего пользовательского интерфейса, вы должны создать NSManagedObjectContext с NSPrivateQueueConcurrencyType и вместо этого поработать над этим. Вы можете создать дочерний контекст, который подталкивает изменения к вашему основному moc при сохранении. Кроме того, не используйте performBlock AndWait, это, конечно, заблокирует ваш пользовательский интерфейс. Вот хороший учебник по основам, вам действительно стоит его просмотреть. raywenderlich.com/145877/ Если нет чего-то конкретного, чего вы хотите достичь по какой-либо причине, избегайте использования собственных очередей с CD.   -  person    schedule 30.08.2017
comment
Вот документация (кроме учебника) относительно того, как вы создаете дочерний контекст и т. Д. developer.apple.com/library/content/documentation/Cocoa/   -  person    schedule 30.08.2017


Ответы (1)


Итак, я нашел причину сбоев: хотя mr_saveToPersistentStoreAndWait ожидает, пока изменения будут сохранены в rootSavingContext, он не ждет, пока они не будут объединены в defaultContext (если они были сделаны контекстом частной очереди). Если rootSavingContext был изменен другим сохранением до того, как основной контекст очереди объединит старые изменения в основном потоке, слияние будет повреждено (изменения в уведомлении NSManagedObjectContextDidSave не соответствуют текущему состоянию контекста rootSavingContext во внутреннем rootContextDidSave: методе MagicalRecord).

Объяснение предлагаемого мной решения:

  1. DatabaseSavingManager содержит контекст сохранения частной очереди, который будет использоваться для всех сохранений в приложении (возможно, это недостаток, если вы хотите использовать несколько контекстов сохранения, но этого достаточно для моих нужд - сохранения происходят в фоновом режиме и поддерживается согласованность). Как прокомментировал @Sneak, нет причин использовать фоновую последовательную очередь, которая создает несколько контекстов и ждет их завершения (это то, что я изначально делал), потому что NSManagedObjectContext имеет свою собственную последовательную очередь, поэтому теперь я использовал один контекст, который создается на основной поток и, следовательно, всегда должен вызываться из основного потока (используя perform(block:), чтобы избежать блокировки основного потока).

  2. После сохранения в постоянное хранилище контекст сохранения ожидает NSManagedObjectContextObjectsDidChange уведомления от defaultContext, чтобы он знал, что defaultContext объединил изменения. Вот почему никакие другие сохранения, кроме использования контекста сохранения DatabaseSavingManager, не разрешены, потому что они могут запутать процесс ожидания.

Вот код DatabaseSavingManager:

import Foundation
import CoreData

class DatabaseSavingManager: NSObject {
    static let shared = DatabaseSavingManager()

    fileprivate let savingDispatchGroup = DispatchGroup()
    fileprivate var savingDispatchGroupEntered = false

    fileprivate lazy var savingContext: NSManagedObjectContext = {
        if !Thread.current.isMainThread {
            var context: NSManagedObjectContext!
            DispatchQueue.main.sync {
                context = NSManagedObjectContext.mr_()
            }
            return context
        }
        else {
            return NSManagedObjectContext.mr_()
        }
    }()

    override init() {
        super.init()
        NotificationCenter.default.addObserver(self, selector: #selector(defaultContextDidUpdate(notification:)), name: NSNotification.Name.NSManagedObjectContextObjectsDidChange, object: NSManagedObjectContext.mr_default())
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    func save(block: @escaping (NSManagedObjectContext) -> ()) {
        guard Thread.current.isMainThread else {
            DispatchQueue.main.async {
                self.save(block: block)
            }
            return
        }

        let moc = self.savingContext
        self.savingContext.perform {
            block(self.savingContext)
            self.saveToPersistentStoreAndWait()
        }
    }

    func saveAndWait(block:  @escaping (NSManagedObjectContext) -> ()) {
        if Thread.current.isMainThread {
            self.savingContext.performAndWait {
                block(self.savingContext)
                self.saveToPersistentStoreAndWait()
            }
        }
        else {
            let group = DispatchGroup()
            group.enter()
            DispatchQueue.main.async {
                self.savingContext.perform {
                    block(self.savingContext)
                    self.saveToPersistentStoreAndWait()
                    group.leave()
                }
            }
            group.wait()
        }
    }

    fileprivate func saveToPersistentStoreAndWait() {
        if self.savingContext.hasChanges {
            self.savingDispatchGroupEntered = true
            self.savingDispatchGroup.enter()
            self.savingContext.mr_saveToPersistentStoreAndWait()
            self.savingDispatchGroup.wait()
        }
    }

    @objc fileprivate func defaultContextDidUpdate(notification: NSNotification) {
        if self.savingDispatchGroupEntered {
            self.savingDispatchGroup.leave()
            self.savingDispatchGroupEntered = false
        }
    }
}

И пример того, как его использовать (NSFetchedResultsController больше не вылетает; можно вызывать из любого потока, также очень часто):

    DatabaseSavingManager.shared.save { (moc) in
        for song in CDSong.mr_findAll(in: moc)! {
            let song = song as! CDSong
            let voters = song.voters!
            for voter in voters {
                (voter as! CDVoter).mr_deleteEntity(in: moc)
            }

            for _ in 1...4 {
                if arc4random()%2 == 0 {
                    let voter = CDVoter.mr_createEntity(in: moc)!
                    voter.name = String(UnicodeScalar(UInt8(arc4random()%26+65)))
                    voter.votedSong = song
                }
            }
        }
    }

Конечно, это точно не самое элегантное решение, просто первое, что пришло мне в голову, поэтому приветствуются другие подходы.

person Matej Vargovčík    schedule 31.08.2017
comment
Я устал читать ваш ответ, но совет, который я забыл упомянуть, включите многопоточную отладку oleb.net/blog/2014/06/core-data-concurrency-debugging, и вы узнаете, когда у вас возникнут проблемы с потоками. однако, быстро просмотрите свой ответ, вам не нужно ждать уведомлений, прежде чем выполнять новые сохранения. И похоже, что вы все еще используете группы отправки и очереди для сохранений и т. Д. Что по-прежнему является плохой практикой. Это не беспорядок и действительно создает для вас беспорядок, я думаю, ваша настоящая проблема в том, что вы используете MR с модификациями вместо того, чтобы копаться в том, как на самом деле работают основные данные. - person ; 01.09.2017
comment
Чтобы продемонстрировать это, на самом деле это то, что вам следует использовать из iOS 10+ developer.apple .com / documentation / coredata / Без использования запутанного ручного отслеживания кодирования уведомлений и слияния. :) - person ; 01.09.2017