在validateForInsert中执行获取请求过于昂贵

时间:2014-09-20 02:23:12

标签: ios core-data

我最近在我的核心数据模型中做了一个重构传递,我在这里使用了多层托管对象上下文模型:http://www.cocoanetics.com/2012/07/multi-context-coredata/

我成功地隔离了所有核心数据解析,以便在后台线程上解析并插入新的托管对象并将其插入子MOC中,并将这些更改最终批量保存到父/主MOC,然后最终通过其父/作者MOC写入持久性商店协调员。

这显着改善了我的UI响应能力,因为之前大批量写入是在父/主MOC上完成的,并锁定了UI线程。

我想进一步改进对象的插入和验证。每次应用程序打开时,在一定程度的间隔内,都会有一个配置文件请求,在该请求期间,使用新值向下发送数十或数百个对象。我选择简单地为所有这些对象创建NSManagedObjects,将它们插入到子MOC中,并允许验证以消除重复项。

我的问题是,在NSFetchRequest的每次通话中是否执行validateForInsert:因为NSManagedObject是昂贵的。我在StackOverflow上看到了几个似乎使用这种模式的答案,例如:https://stackoverflow.com/a/2245699/2184893。我想在创建实体之前执行此操作而不是验证,因为如果两个线程同时同时创建同一个对象,则两者都将被创建,并且验证必须在父线程的插入/合并时发生。 / p> 那么,使用这种方法贵吗?这是常见做法吗?另外,使用validateForInsertvalidate

有区别吗?
-(BOOL)validateUniqueField:(id *)ioValue error:(NSError * __autoreleasing *)outError{

    // The property being validated must not already exist

    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([self class])];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"uniqueField == %@", *ioValue];

    int count = [self.managedObjectContext countForFetchRequest:fetchRequest error:nil];
    if (count > 0) {
        if (outError != NULL) {
            NSString *errorString = NSLocalizedString(
                                                      @"Object must have unique value for property",
                                                      @"validation: nonunique property");
            NSDictionary *userInfoDict = @{ NSLocalizedDescriptionKey : errorString };
            *outError = [[NSError alloc] initWithDomain:nil
                                                   code:0
                                               userInfo:userInfoDict];
        }
        return NO;
    }
    return YES;
}

我可能会创建相同对象的多个线程的用例例如,如果我异步请求区域内的所有用户,这些区域中的两个重叠并且几乎同时给我相同的用户对象,并且每个线程尝试在自己的context. findOrCreate中创建相同的用户将无法验证该对象是否已在其他线程/上下文中创建。目前我正在通过validateForInsert进行检查来处理这个问题。

3 个答案:

答案 0 :(得分:1)

获取验证方法?

你的问题很聪明,因为它隐藏了几个问题!

那么,使用这种方法贵吗?

这可能非常昂贵,因为您至少要对每个要验证的对象进行提取作为保存的一部分(在保存期间自动调用验证)。

这是常见做法吗?

我真的希望不会!我之前只看过一次这样做,结果并不好(继续阅读)。

另外,使用validateForInsert并验证是否存在差异?

我不确定你的意思。托管对象具有以下验证方法:validateForInsertvalidateForUpdatevalidateForDelete。这些中的每一个都执行它自己的规则,并为各个属性调用validateValue:forKey:error:,这反过来将调用模式validate<Key>:error:的任何实现。例如,validateForInsert将在调用其他验证方法之前执行托管对象模型中定义的任何插入验证规则(例如,在模型编辑器中标记建模属性是非可选的是插入验证规则)。 保存上下文时会自动调用验证,您可以随时调用它。如果要显示必须更正的用户错误,以便完成保存等,这将非常有用。

也就是说,请继续阅读以解决您似乎试图解决的问题。

关于在验证方法中获取...

在验证方法中访问对象图是不明智的。当您执行获取时,您正在更改该上下文中的对象图 - 访问对象,触发故障等。在保存期间自动验证,并在此时更改内存中对象图 - 即使您不是直接改变属性值 - 可能会有一些戏剧性的,难以预测的副作用。这不是快乐的时光。

唯一性的正确解决方案:查找或创建

您似乎在尝试确保托管对象是唯一的。 Core Data没有为此提供内置机制,但它有一个推荐的模式来实现:“find-or-create”。这是在访问对象时完成的,而不是在验证或保存对象时完成的。

确定此实体的唯一性。这可能是单个属性值(在您的情况下,它似乎是单个属性),或几个的组合(例如“firstName”和“lastName”一起使“人”唯一)。根据该唯一性标准,您可以查询现有对象匹配的上下文。如果找到匹配项,则返回它们,否则使用这些值创建一个对象。

以下是基于您问题中的代码的示例。这将使用“uniqueField”的值作为唯一性标准,显然如果你有多个属性,这些属性一起使你的实体变得独一无二,这会变得更加复杂。

示例:

// I am using NSValue here, as your example doesn't indicate a type.
+ (void) findOrCreateWithUniqueValue:(NSValue *)value inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext completion:(void (^)(NSArray *results, NSError *error))completion {

    [managedObjectContext performBlock:^{
        NSError             *error      = nil;
        NSEntityDescription *entity     = [NSEntityDescription entityForName:NSStringFromClass(self) inManagedObjectContext:managedObjectContext];
        NSFetchRequest *fetchRequest    = [[NSFetchRequest alloc] init];
        fetchRequest.entity = entity;
        fetchRequest.predicate = [NSPredicate predicateWithFormat:@"uniqueField == %@", value];

        NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
        if ([results count] == 0){
            // No matches found, create a new object
            NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:managedObjectContext];
            object.uniqueField = value;
            results = [NSArray arrayWithObject:object];
        }

        completion(results, error);
    }];

}

这将成为获取对象的主要方法。在您在问题中描述的方案中,您定期从某些源中获取必须应用于托管对象的数据。使用上面的方法,该过程看起来像....

[MyEntityClass findOrCreateWithUniqueValue:value completion:^(NSArray *results, NSError *error){
    if ([results count] > 0){
        for (NSManagedObject *object in results){
            // Set your new values.
            object.someValue = newValue;
        }
    } else {
        // No results, check the error and handle here!
    }
}];

可以高效,高效地完成,并具有适当的数据完整性。如果您愿意接受内存命中,则可以在获取实现等中使用批处理错误。对所有传入数据执行上述操作后,可以保存上下文,并将对象及其值有效地推送到父存储。

这是使用Core Data实现唯一性的首选方法。在Core Data Programming Guide中间接地,间接地提到了这一点。

要扩展这个...... 必须进行“批量”查找或创建并不罕见。在您的方案中,您将获得需要应用于托管对象的更新列表,如果它们不存在则创建新对象。显然,上面的示例查找或创建方法可以做到这一点,但您也可以更有效地执行此操作。

Core Data具有“批量错误”的概念。而不是在访问时单独对每个对象进行错误处理,如果您知道将要使用多个对象,则可以同时对它们进行批处理。这意味着更少的磁盘访问和更好的性能。

批量查找或创建方法可以利用此功能。请注意,由于所有这些对象现在都会被“触发”,这将使用更多内存 - 但不会超过在每个上面调用上面的单个find-or-create。

我不会重复所有以前的方法,而是会解释:

 // 'values' is a collection of your unique identifiers.
+ (void) findOrCreateWithUniqueValues:(id <NSFastEnumeration>)values inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext completion:(void (^)(NSArray *results, NSError *error))completion {
    ...
    // Effective use of IN will ensure a batch fault
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"SELF.uniqueField IN %@", values];
    // returnsObjectsAsFaults works inconsistently across versions.
    fetchRequest.returnsObjectsAsFaults = NO;
    ...
    NSArray *results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    // uniqueField values we initially wanted
    NSSet   *wanted = [NSSet setWithArray:values];
    // uniqueField values we got from the fetch
    NSMutableSet    *got    = [NSMutableSet setWithArray:[results valueForKeyPath:@"uniqueField"]];
    // uniqueField values we will need to create, the different between want and got
    NSMutableSet    *need   = nil;

    if ([got count]> 0){
        need = [NSMutableSet setWithSet:wanted];
        [need minusSet:got];
    }

    NSMutableSet *resultSet = [NSMutableSet setWithArray:fetchedResults];
    // At this point, walk the values in need, insert new objects and set uniqueField values, add to resultSet
    ...
    // And then pass [resultSet allObjects] to the completion block.

}

批处理错误的有效使用对于任何一次处理许多对象的应用程序来说都是一个巨大的推动力。与往常一样,配备仪器。不幸的是,不同的Core Data版本之间的错误行为有很大不同。在旧版本中,使用托管对象ID进行额外提取更为有益。您的里程可能会有所不同。

答案 1 :(得分:0)

与针对一组标识符进行比较的单个调用相比,对数据库的单独调用是昂贵的。在与单个值进行比较时,可以使用集合或数组上的in运算符对一组值进行比较。因此,关闭该批次,使用可能的-valueForKey:提取ID并重写上面的内容以接受一组值。

答案 2 :(得分:0)

我认为在validation方法中进行提取是很好的,例如validateForInsert。事实上,如果所需的提取失败,它是唯一可以将错误传递回上下文的方法。只需确保将错误param传递到您的抓取中,如果抓取产生false结果,则返回nil