Bids
Bids

Reputation: 2449

Core Data doesn't update related objects

I have a class A that has a "to many" relationship with B. A and its Bs are fetched from a REST endpoint, we use this approach to create the objects.

A has a unique ID and some other properties (not shown in the example). B has an ID (bID) that is unique when combined with A, and a relationship to A called a. For A the constraint is "uniqueID". For B, the constraint is "a, bID" - we want each B to only exist for each combination of A and bID. The delete rule for A to its Bs is "Cascade". The merge policy for the context and all child contexts is NSOverwriteMergePolicy.

The code looks something like this:

public class A: NSManagedObject, Decodable {
    enum CodingKeys: String, CodingKey {
        case uniqueID, bData
    }

    public required convenience init(from decoder: Decoder) throws {
        guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext!] as? NSManagedObjectContext else {
            throw MyCoreDataError.missingManagedObjectContext
        }

        self.init(context: context)

        let values = try decoder.container(keyedBy: CodingKeys.self)

        uniqueID = try values.decode(String.self, forKey: .uniqueID)
        bData = try NSSet(set: values.decode(Set<B>.self, forKey: .bData))
    }
}

public class B: NSManagedObject, Decodable {
    enum CodingKeys: CodingKey {
        case bID, someData
    }

    public required convenience init(from decoder: Decoder) throws {
        guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext            
            throw MyCoreDataError.missingManagedObjectContext
        }

        self.init(context: context)

        let values = try decoder.container(keyedBy: CodingKeys.self)

        bID = try values.decode(String.self, forKey: .bID)
        someData = try values.decode(String.self, forKey: .someData)
    }
}


This works fine for the initial fetch, however sometime later we fetch A and its Bs again as they have been updated. A is updated successfully, but the Bs are not updated. Looking at the SQLite data, the Bs have just been ignored. The objects are definitely updated in-memory (I can see the new Bs have the right values for their properties), but when the context is saved, they are not persisted.

If I remove the B constraint the new Bs are persisted but they don't replace the old ones.

If I save the context before setting bData then the new Bs are persisted but the old Bs aren't deleted (though their relationship with A is set to NULL).

It seems that there is some sort of problem with Core Data merging the changed A at the same time as its changed Bs. Does anyone know if there is a clean solution to this?

Upvotes: 1

Views: 225

Answers (1)

Phil Dukhov
Phil Dukhov

Reputation: 87804

I'm using the following extension to solve this problem:

extension Identifiable where Self: NSManagedObject {
    init(fetchOrCreate id: ID, context: NSManagedObjectContext) {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: String(describing: Self.classForCoder()))
        request.predicate = NSPredicate(format: "id == \(id)")
        request.fetchLimit = 1
        if let result = (try? context.execute(request) as? NSAsynchronousFetchResult<Self>)?.finalResult?.first
        {
            self = result
            return
        }
        self.init(context: context)
        setValue(id, forKeyPath: "id")
    }
}

So my initializer will return existing object if it's already presented in the database, or create a new one.

And use it like this:

public class B: NSManagedObject, Decodable {
    enum CodingKeys: CodingKey {
        case bID, someData
    }

    public required convenience init(from decoder: Decoder) throws {
        guard let context = decoder.userInfo[CodingUserInfoKey.managedObjectContext            
            throw MyCoreDataError.missingManagedObjectContext
        }

        self.init(
            fetchOrCreate: try values.decode(String.self, forKey: .bID),
            context: context
        )

        let values = try decoder.container(keyedBy: CodingKeys.self)

        someData = try values.decode(String.self, forKey: .someData)
    }
}

I'm not sure why you're using different names for you'r ids(uniqueID, bID) - if you really have this in your production code, swift extension of NSManagedObject won't let you return fetched object that easily:self = result will produce an error: Cannot assign to value: 'self' is immutable.

So to use custom id keys you can create some custom protocol that could be extended instead of Identifiable, or use ObjC extension, which won't have such limitations:

@implementation NSManagedObject (initOrGet)

- (instancetype)initOrGet:(NSObject *)id
                      key:(NSString *)key
                  context:(NSManagedObjectContext*)moc
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass(self.class).pathExtension];
    request.predicate = [NSPredicate predicateWithFormat:@"%@ == %@", key, id];
    request.fetchLimit = 1;
    NSError *error;
    NSManagedObject *result = [moc executeFetchRequest:request error:&error].firstObject;
    if (result != nil) {
        return result;
    }
    self = [self initWithContext:moc];
    [self setValue:id forKey:key];
    return self;
}

@end

Upvotes: 1

Related Questions