Reputation: 2449
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
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