programing

핵심 데이터 배경 컨텍스트 모범 사례

nasanasas 2020. 10. 18. 18:19
반응형

핵심 데이터 배경 컨텍스트 모범 사례


핵심 데이터로 수행해야하는 큰 가져 오기 작업이 있습니다.
내 핵심 데이터 모델이 다음과 같다고 가정 해 보겠습니다.

Car
----
identifier 
type

내 서버에서 자동차 정보 JSON 목록을 가져온 다음 핵심 데이터 Car개체 와 동기화하고 싶습니다 . 즉
, 새 자동차 인 경우-> Car새 정보에서 새 Core Data 개체를 만듭니다 .
자동차가 이미있는 경우-> Core Data Car개체를 업데이트 합니다.

따라서 UI를 차단하지 않고 백그라운드에서이 가져 오기를 수행하고 사용하는 동안 모든 자동차를 표시하는 자동차 테이블보기를 스크롤합니다.

현재 나는 다음과 같이하고있다.

// create background context
NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[bgContext setParentContext:self.mainContext];

[bgContext performBlock:^{
    NSArray *newCarsInfo = [self fetchNewCarInfoFromServer]; 

    // import the new data to Core Data...
    // I'm trying to do an efficient import here,
    // with few fetches as I can, and in batches
    for (... num of batches ...) {

        // do batch import...

        // save bg context in the end of each batch
        [bgContext save:&error];
    }

    // when all import batches are over I call save on the main context

    // save
    NSError *error = nil;
    [self.mainContext save:&error];
}];

하지만 여기에서 옳은 일을하고 있는지 잘 모르겠습니다. 예를 들면 다음과 같습니다.

내가 사용해도 setParentContext되나요?
나는 이것을 사용하는 몇 가지 예를 보았지만을 호출하지 않는 다른 예를 보았습니다 setParentContext. 대신 다음과 같은 작업을 수행합니다.

NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
bgContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;  
bgContext.undoManager = nil;

내가 확실하지 않은 또 다른 점은 주 컨텍스트에서 save를 호출 할 때입니다. 내 예제에서는 가져 오기가 끝날 때 save를 호출하지만 다음을 사용하는 예제를 보았습니다.

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
    NSManagedObjectContext *moc = self.managedObjectContext;
    if (note.object != moc) {
        [moc performBlock:^(){
            [moc mergeChangesFromContextDidSaveNotification:note];
        }];
    }
}];  

이전에 언급했듯이 업데이트하는 동안 사용자가 데이터와 상호 작용할 수 있기를 원하는데, 가져 오기가 동일한 차를 변경하는 동안 사용자가 차 유형을 변경하면 어떻게 작성합니까?

최신 정보:

@TheBasicMind 덕분에 옵션 A를 구현하려고하므로 코드는 다음과 같습니다.

다음은 AppDelegate의 핵심 데이터 구성입니다.

AppDelegate.m  

#pragma mark - Core Data stack

- (void)saveContext {
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            DDLogError(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}  

// main
- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    _managedObjectContext.parentContext = [self saveManagedObjectContext];

    return _managedObjectContext;
}

// save context, parent of main context
- (NSManagedObjectContext *)saveManagedObjectContext {
    if (_writerManagedObjectContext != nil) {
        return _writerManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _writerManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_writerManagedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _writerManagedObjectContext;
}  

그리고 이것은 내 가져 오기 방법이 이제 어떻게 생겼는지입니다.

- (void)import {
    NSManagedObjectContext *saveObjectContext = [AppDelegate saveManagedObjectContext];

    // create background context
    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    bgContext.parentContext = saveObjectContext;

    [bgContext performBlock:^{
        NSArray *newCarsInfo = [self fetchNewCarInfoFromServer];

        // import the new data to Core Data...
        // I'm trying to do an efficient import here,
        // with few fetches as I can, and in batches
        for (... num of batches ...) {

            // do batch import...

            // save bg context in the end of each batch
            [bgContext save:&error];
        }

        // no call here for main save...
        // instead use NSManagedObjectContextDidSaveNotification to merge changes
    }];
}  

그리고 다음과 같은 관찰자가 있습니다.

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {

    NSManagedObjectContext *mainContext = self.managedObjectContext;
    NSManagedObjectContext *otherMoc = note.object;

    if (otherMoc.persistentStoreCoordinator == mainContext.persistentStoreCoordinator) {
        if (otherMoc != mainContext) {
            [mainContext performBlock:^(){
                [mainContext mergeChangesFromContextDidSaveNotification:note];
            }];
        }
    }
}];

이것은 처음으로 Core Data에 접근하는 사람들에게 매우 혼란스러운 주제입니다. 가볍게 말하지는 않지만 경험상 Apple 문서가이 문제에 대해 다소 오해의 소지가 있다고 확신합니다 (실제로 매우주의 깊게 읽으면 일관성이 있지만 데이터 병합이 남아있는 이유를 적절하게 설명하지 못합니다). 많은 경우에 부모 / 자식 컨텍스트에 의존하고 단순히 자식에서 부모로 저장하는 것보다 더 나은 솔루션).

문서는 부모 / 자식 컨텍스트가 백그라운드 처리를 수행하는 새로운 선호 방법이라는 강한 인상을줍니다. 그러나 Apple은 몇 가지 강력한 경고를 강조하지 않습니다. 첫째, 자식 컨텍스트로 가져 오는 모든 것은 먼저 부모를 통해 가져옵니다. 따라서 기본 스레드에서 실행되는 기본 컨텍스트의 자식을 기본 스레드의 UI에 이미 표시된 데이터 처리 (편집)로 제한하는 것이 가장 좋습니다. 일반적인 동기화 작업에 사용하는 경우 현재 UI에 표시되는 범위를 훨씬 넘어서는 데이터를 처리하고 싶을 것입니다. NSPrivateQueueConcurrencyType을 사용하더라도 하위 편집 컨텍스트의 경우 잠재적으로 주 컨텍스트를 통해 많은 양의 데이터를 끌어와 성능 저하 및 차단으로 이어질 수 있습니다. 이제 기본 컨텍스트를 동기화에 사용하는 컨텍스트의 하위로 만들지 않는 것이 가장 좋습니다. 수동으로 수행하지 않는 한 동기화 업데이트에 대한 알림을받지 않고 잠재적으로 오래 실행되는 작업을 기본 컨텍스트의 하위 인 편집 컨텍스트에서 기본 연락처를 통해 데이터 저장소까지 계단식으로 시작된 저장에 응답해야 할 수 있습니다. 데이터를 수동으로 병합하고 기본 컨텍스트에서 무효화해야하는 항목을 추적하고 다시 동기화해야합니다. 가장 쉬운 패턴은 아닙니다. 또한 기본 컨텍스트의 하위 인 편집 컨텍스트에서 기본 연락처를 통해 데이터 저장소까지 계단식으로 시작된 저장에 응답해야 할 수있는 컨텍스트에서 잠재적으로 오래 실행되는 작업을 실행하게됩니다. 데이터를 수동으로 병합하고 기본 컨텍스트에서 무효화해야하는 항목을 추적하고 다시 동기화해야합니다. 가장 쉬운 패턴은 아닙니다. 또한 기본 컨텍스트의 하위 인 편집 컨텍스트에서 기본 연락처를 통해 데이터 저장소까지 계단식으로 시작된 저장에 응답해야 할 수있는 컨텍스트에서 잠재적으로 오래 실행되는 작업을 실행하게됩니다. 데이터를 수동으로 병합하고 기본 컨텍스트에서 무효화해야하는 항목을 추적하고 다시 동기화해야합니다. 가장 쉬운 패턴은 아닙니다.

Apple 문서에서 명확하지 않은 것은 작업을 수행하는 "오래된"스레드 제한 방식과 작업을 수행하는 새로운 부모-자식 컨텍스트 방식을 설명하는 페이지에 설명 된 기술의 혼합이 필요할 가능성이 가장 높다는 것입니다.

가장 좋은 방법은 NSPrivateQueueConcurrencyType 저장 컨텍스트를 최상위 상위 항목으로 사용하여 데이터 저장소에 직접 저장하는 것입니다 (여기서는 일반적인 솔루션을 제공하고 있으며 최상의 솔루션은 세부 요구 사항에 따라 달라질 수 있음). [편집 : 당신은이 맥락에서 그다지 직접적으로하지 않을 것입니다] 그런 다음 그 저장 맥락에 적어도 두 명의 직접적인 자식을 제공하십시오. UI에 사용하는 NSMainQueueConcurrencyType 기본 컨텍스트 중 하나는 [편집 :이 컨텍스트에서 데이터를 편집하지 않는 것이 가장 좋습니다], 다른 하나는 NSPrivateQueueConcurrencyType, 데이터의 사용자 편집을 수행하는 데 사용합니다. 첨부 된 다이어그램의 옵션 A) 동기화 작업.

그런 다음 기본 컨텍스트를 동기화 컨텍스트에 의해 생성 된 NSManagedObjectContextDidSave 알림의 대상으로 만들고 알림 .userInfo 사전을 기본 컨텍스트의 mergeChangesFromContextDidSaveNotification :으로 보냅니다.

고려해야 할 다음 질문은 사용자 편집 컨텍스트 (사용자가 편집 한 내용이 인터페이스에 다시 반영되는 컨텍스트)를 넣는 위치입니다. 사용자의 작업이 항상 소량의 제공된 데이터에 대한 편집으로 제한되는 경우 NSPrivateQueueConcurrencyType을 사용하여이를 다시 기본 컨텍스트의 하위로 만드는 것이 최선의 방법이며 관리하기 가장 쉽습니다 (저장하면 편집 내용을 기본 컨텍스트에 직접 저장하고 NSFetchedResultsController가 있으면 적절한 대리자 메서드가 자동으로 호출되어 UI가 업데이트를 처리 할 수 ​​있습니다. controller : didChangeObject : atIndexPath : forChangeType : newIndexPath :) (다시 옵션 A).

반면에 사용자 작업으로 인해 많은 양의 데이터가 처리 될 수있는 경우 저장 컨텍스트에 세 개의 직접적인 하위 항목이 있도록 기본 컨텍스트 및 동기화 컨텍스트의 다른 피어로 만드는 것을 고려할 수 있습니다. main , sync (비공개 대기열 유형) 및 편집 (비공개 대기열 유형). 이 배열을 다이어그램에 옵션 B로 표시했습니다.

동기화 컨텍스트와 마찬가지로 데이터가 저장 될 때 (또는 더 세분화가 필요한 경우 데이터가 업데이트 될 때) [편집 : 알림을 수신하도록 기본 컨텍스트 구성]하고 데이터를 병합하는 조치를 취해야합니다 (일반적으로 mergeChangesFromContextDidSaveNotification 사용 : ). 이 배열을 사용하면 주 컨텍스트가 save : 메소드를 호출 할 필요가 없습니다.여기에 이미지 설명 입력

상위 / 하위 관계를 이해하려면 옵션 A를 선택하십시오. 상위 하위 접근 방식은 단순히 편집 컨텍스트가 NSManagedObject를 가져 오면 먼저 저장 컨텍스트에 "복사"(등록됨) 된 다음 기본 컨텍스트, 마지막으로 컨텍스트 편집을 의미합니다. 변경 사항을 적용한 다음 save를 호출하면 편집 컨텍스트에서 변경 사항이 기본 컨텍스트에만 저장 됩니다 . 기본 컨텍스트에서 save :를 호출 한 다음 저장 컨텍스트에서 save :를 호출해야 디스크에 기록됩니다.

하위에서 상위까지 저장하면 다양한 NSManagedObject 변경 및 저장 알림이 실행됩니다. 예를 들어 결과 가져 오기 컨트롤러를 사용하여 UI의 데이터를 관리하는 경우 대리자 메서드가 호출되어 UI를 적절하게 업데이트 할 수 있습니다.

몇 가지 결과 : 편집 컨텍스트에서 개체 및 NSManagedObject A를 가져온 다음 수정하고 저장하면 수정 사항이 기본 컨텍스트로 반환됩니다. 이제 기본 및 편집 컨텍스트에 대해 수정 된 개체가 등록되었습니다. 그렇게하는 것은 나쁜 스타일이지만, 이제 주 컨텍스트에서 개체를 다시 수정할 수 있으며 이제 편집 컨텍스트에 저장되므로 개체와 달라집니다. 그런 다음 편집 컨텍스트에 저장된대로 개체를 추가로 수정하려고하면 수정 내용이 기본 컨텍스트의 개체와 동기화되지 않고 편집 컨텍스트를 저장하려고하면 오류가 발생합니다.

이러한 이유로 옵션 A와 같은 배열을 사용하면 개체를 가져 와서 수정하고 저장하고 편집 컨텍스트를 재설정하는 것이 좋습니다 (예 : [editContext reset]). 주어진 블록이 [editContext performBlock :])에 전달됩니다. 또한 규율을 잘 지키고 주 컨텍스트에 대한 편집을 피하는 것이 가장 좋습니다 . 또한 다시 반복하는 것이 좋습니다. 주 컨텍스트에 대한 모든 처리가 주 스레드이기 때문입니다. 편집 컨텍스트에 대한 많은 개체, 기본 컨텍스트는 기본 스레드 에서 가져 오기 처리 수행합니다.이러한 객체는 부모에서 자식 컨텍스트로 반복적으로 복사됩니다. 처리중인 데이터가 많은 경우 UI에서 응답하지 않을 수 있습니다. 예를 들어 관리되는 개체의 큰 저장소가 있고 모든 개체를 편집 할 수있는 UI 옵션이있는 경우입니다. 이 경우 옵션 A와 같이 앱을 구성하는 것은 좋지 않습니다.이 경우 옵션 B가 더 나은 방법입니다.

수천 개의 개체를 처리하지 않는 경우 옵션 A로 충분할 수 있습니다.

BTW는 선택하는 옵션에 대해 너무 걱정하지 마십시오. A로 시작하고 B로 변경해야하는 경우에는 좋은 생각 일 수 있습니다. 이러한 변경은 생각보다 쉽고 일반적으로 예상보다 결과가 적습니다.


첫째, 부모 / 자식 컨텍스트는 백그라운드 처리 용이 아닙니다. 여러 뷰 컨트롤러에서 생성 될 수있는 관련 데이터의 원자 적 업데이트를위한 것입니다. 따라서 마지막 뷰 컨트롤러가 취소되면 부모에 부정적인 영향을주지 않고 자식 컨텍스트를 버릴 수 있습니다. 이것은 [^ 1]에서이 답변의 맨 아래에있는 Apple에 의해 완전히 설명됩니다. 이제는 불가능하고 일반적인 실수에 빠지지 않았으므로 백그라운드 코어 데이터를 올바르게 수행하는 방법에 집중할 수 있습니다.

새로운 영구 저장소 코디네이터 (iOS 10에서는 더 이상 필요하지 않음, 아래 업데이트 참조) 및 개인 대기열 컨텍스트를 만듭니다. 저장 알림을 듣고 변경 사항을 기본 컨텍스트에 병합합니다 (iOS 10에서는 컨텍스트에이를 자동으로 수행하는 속성이 있음).

For a sample by Apple see "Earthquakes: Populating a Core Data Store Using a Background Queue" https://developer.apple.com/library/mac/samplecode/Earthquakes/Introduction/Intro.html As you can see from the revision history on 2014-08-19 they added "New sample code that shows how to use a second Core Data stack to fetch data on a background queue."

Here is that bit from AAPLCoreDataStackManager.m:

// Creates a new Core Data stack and returns a managed object context associated with a private queue.
- (NSManagedObjectContext *)createPrivateQueueContext:(NSError * __autoreleasing *)error {

    // It uses the same store and model, but a new persistent store coordinator and context.
    NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[AAPLCoreDataStackManager sharedManager].managedObjectModel];

    if (![localCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil
                                                  URL:[AAPLCoreDataStackManager sharedManager].storeURL
                                              options:nil
                                                error:error]) {
        return nil;
    }

    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [context performBlockAndWait:^{
        [context setPersistentStoreCoordinator:localCoordinator];

        // Avoid using default merge policy in multi-threading environment:
        // when we delete (and save) a record in one context,
        // and try to save edits on the same record in the other context before merging the changes,
        // an exception will be thrown because Core Data by default uses NSErrorMergePolicy.
        // Setting a reasonable mergePolicy is a good practice to avoid that kind of exception.
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;

        // In OS X, a context provides an undo manager by default
        // Disable it for performance benefit
        context.undoManager = nil;
    }];
    return context;
}

And in AAPLQuakesViewController.m

- (void)contextDidSaveNotificationHandler:(NSNotification *)notification {

    if (notification.object != self.managedObjectContext) {

        [self.managedObjectContext performBlock:^{
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

Here is the full description of how the sample is designed:

Earthquakes: Using a "private" persistent store coordinator to fetch data in background

Most applications that use Core Data employ a single persistent store coordinator to mediate access to a given persistent store. Earthquakes shows how to use an additional "private" persistent store coordinator when creating managed objects using data retrieved from a remote server.

Application Architecture

The application uses two Core Data "stacks" (as defined by the existence of a persistent store coordinator). The first is the typical "general purpose" stack; the second is created by a view controller specifically to fetch data from a remote server (As of iOS 10 a second coordinator is no longer needed, see update at bottom of answer).

The main persistent store coordinator is vended by a singleton "stack controller" object (an instance of CoreDataStackManager). It is the responsibility of its clients to create a managed object context to work with the coordinator[^1]. The stack controller also vends properties for the managed object model used by the application, and the location of the persistent store. Clients can use these latter properties to set up additional persistent store coordinators to work in parallel with the main coordinator.

The main view controller, an instance of QuakesViewController, uses the stack controller's persistent store coordinator to fetch quakes from the persistent store to display in a table view. Retrieving data from the server can be a long-running operation which requires significant interaction with the persistent store to determine whether records retrieved from the server are new quakes or potential updates to existing quakes. To ensure that the application can remain responsive during this operation, the view controller employs a second coordinator to manage interaction with the persistent store. It configures the coordinator to use the same managed object model and persistent store as the main coordinator vended by the stack controller. It creates a managed object context bound to a private queue to fetch data from the store and commit changes to the store.

[^1]: This supports the "pass the baton" approach whereby—particularly in iOS applications—a context is passed from one view controller to another. The root view controller is responsible for creating the initial context, and passing it to child view controllers as/when necessary.

The reason for this pattern is to ensure that changes to the managed object graph are appropriately constrained. Core Data supports "nested" managed object contexts which allow for a flexible architecture that make it easy to support independent, cancellable, change sets. With a child context, you can allow the user to make a set of changes to managed objects that can then either be committed wholesale to the parent (and ultimately saved to the store) as a single transaction, or discarded. If all parts of the application simply retrieve the same context from, say, an application delegate, it makes this behavior difficult or impossible to support.

업데이트 : iOS 10에서 Apple은 동기화를 sqlite 파일 수준에서 영구 코디네이터로 이동했습니다. 즉, 이제 개인 대기열 컨텍스트를 만들고 이전에 그렇게했을 때와 동일한 성능 문제없이 기본 컨텍스트에서 사용하는 기존 코디네이터를 재사용 할 수 있습니다.


그런데이 애플 문서 는이 문제를 매우 명확하게 설명하고 있습니다. 관심있는 사람을위한 위의 Swift 버전

let jsonArray = … //JSON data to be imported into Core Data
let moc = … //Our primary context on the main queue

let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc

privateMOC.performBlock {
    for jsonObject in jsonArray {
        let mo = … //Managed object that matches the incoming JSON structure
        //update MO with data from the dictionary
    }
    do {
        try privateMOC.save()
        moc.performBlockAndWait {
            do {
                try moc.save()
            } catch {
                fatalError("Failure to save context: \(error)")
            }
        }
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}

iOS 10 이상용 NSPersistentContainer사용하는 경우 더 간단합니다.

let jsonArray = …
let container = self.persistentContainer
container.performBackgroundTask() { (context) in
    for jsonObject in jsonArray {
        let mo = CarMO(context: context)
        mo.populateFromJSON(jsonObject)
    }
    do {
        try context.save()
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}

참고 URL : https://stackoverflow.com/questions/24657437/core-data-background-context-best-practice

반응형