40

Is it possible to make a Core Data attribute unique, i.e. no two MyEntity objects can have the same myAttribute?

I know how to enforce this programatically, but I'm hoping there's a way to do it using the graphical Data Model editor in xcode.

I'm using the iPhone 3.1.2 SDK.

  • 16
    Why was this question closed? The reason given was "Questions asking for code must demonstrate a minimal understanding of the problem being solved", but I wasn't asking for code at all - I wanted to know whether it was possible to achieve this in the graphical editor in Xcode. (The answer effectively turned out to be "no, but here are some workarounds".) –  Aug 08 '13 at 17:20

7 Answers7

30

Every time i create on object I perform a class method that makes a new Entity only when another one does not exist.

+ (TZUser *)userWithUniqueUserId:(NSString *)uniqueUserId inManagedObjectContext:(NSManagedObjectContext *)context
{
    TZUser *user = nil;
    NSFetchRequest *request = [[NSFetchRequest alloc] init];

    request.entity = [NSEntityDescription entityForName:@"TZUser" inManagedObjectContext:context];
    request.predicate = [NSPredicate predicateWithFormat:@"objectId = %@", uniqueUserId];
    NSError *executeFetchError = nil;
    user = [[context executeFetchRequest:request error:&executeFetchError] lastObject];

    if (executeFetchError) {
         NSLog(@"[%@, %@] error looking up user with id: %i with error: %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [uniqueUserId intValue], [executeFetchError localizedDescription]);
    } else if (!user) {
        user = [NSEntityDescription insertNewObjectForEntityForName:@"TZUser" 
                                             inManagedObjectContext:context];
    }

    return user;
}
Joony
  • 4,498
  • 2
  • 30
  • 39
doozMen
  • 690
  • 1
  • 7
  • 14
  • This is the simplest and clearest take on this design pattern I have seen. Thanks! – radven May 02 '12 at 04:14
  • This is a really fantastic solution. After seeing it, it seems so obvious. Now I'm still a newb at all this so perhaps there is some downside to this, but works great for my app as far as I can tell. Thanks for sharing! – daveomcd Jul 16 '12 at 02:20
  • 2
    The problem with this approach is you incur IO. How to avoid that? – user4951 Sep 26 '12 at 03:26
  • What do you mean by incur IO. I would like to solve this problem but not really sure what the question is. I usually do this in a seperate private que so it does not block the main thread. – doozMen Oct 05 '12 at 15:11
  • 13
    You're basically performing a SELECT before each INSERT. Isn't this considered to be too 'expensive'? – Rizon Jul 29 '13 at 22:30
  • Agreed and all ears for that but this is what I got from the apple docs. It is expensive I presume..., – doozMen Nov 04 '13 at 13:45
  • 1
    You might want to also set the `objectID` to the `uniqueUserID` after you created a new user object in the `else if`-clause. Otherwise you'd have to do it in the function that is calling `userWithUniqueUserID`. – Julius Feb 06 '14 at 09:38
  • It won't work if you have more than 1 thread trying to save data in separate contexts with common parent context. It may work sometimes, but it definetely race condition here. – Nikita Took Sep 09 '14 at 16:45
  • This is an example of the find-or-create pattern, which is the preferred method for accomplishing this using Core Data. @NikitaTook, it does work quite well with parent-child contexts when done correctly. Push it to the parent and fault it in the parent. – quellish Sep 21 '14 at 06:44
  • @quellish can u post a link or something about this. I don't know about this, and I would normally call any save context in a dispatch_async on same thread to get thread-safety. As for the answer, wouldn't it be more obvious to include the logics for handling "unique-ID already found", but I assume u do that elsewhere since u didn't set the objectID in this method either – DevilInDisguise Aug 07 '15 at 14:47
  • Isn't this just doing it programmatically? I thought that question was to NOT do it programmatically. – jeremywhuff Oct 07 '15 at 23:48
  • this is great! Can we have swift version? – Vinu David Jose May 07 '16 at 03:44
13

From IOS 9 there is a new way to handle unique constraints.

You define the unique attributes in the data model.

You need to set a managed context merge policy "Merge policy singleton objects that define standard ways to handle conflicts during a save operation" NSErrorMergePolicy is the default,This policy causes a save to fail if there are any merge conflicts.

- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        return nil;
    }
  _managedObjectContext = [[NSManagedObjectContext alloc]    initWithConcurrencyType:NSMainQueueConcurrencyType];
  [_managedObjectContext setPersistentStoreCoordinator:coordinator];
  [_managedObjectContext setMergePolicy:NSOverwriteMergePolicy];
    return _managedObjectContext;
}

The various option are discussed at Apple Ducumentation Merge Policy

It is answered nicely here Zachary Orr's Answer

and he has kindly also created a blogpost and sample code.

Sample Code

Blog Post

The most challenging part is to get the data Model attributes editable.The Secret is to left click and then right click, after you have clicked the + sign to add a constraint.

enter image description here

Community
  • 1
  • 1
Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
  • 1
    Xcode Version 11.4 (11E146): The most important statement in the above answer is. "The Secret is to left click and then right click, after you have clicked the + sign to add a constraint." Thanks @Ryan Heitner – Pankaj Kulkarni Mar 26 '20 at 09:18
12

I've decided to use the validate<key>:error: method to check if there is already a Managed Object with the specific value of <key>. An error is raised if this is the case.

For example:

- (BOOL)validateMyAttribute:(id *)value error:(NSError **)error {
    // Return NO if there is already an object with a myAtribute of value
}

Thanks to Martin Cote for his input.

  • 1
    When querying, make sure you use `self.managedObjectContext`, to ensure you check for duplicate's existence in correct `managedObjectContext`. Otherwise, Save As (that is, migration to new url) will fail. – Ivan Vučica Dec 17 '12 at 15:28
  • 2
    Performing a fetch will change the object graph in the managed object context - which is not something you want to do inside a validation method! – quellish Sep 20 '14 at 22:31
  • not necessarily - if you only query but not insert/update/delete any entity, the context will not get dirty by the validation. It is not recommended to introduce edits (to dirty your context) in validations, because it causes recursion - still in some situations it is OK. – Motti Shneor Aug 25 '15 at 10:26
  • You could optimize by checking if previously saved and field not changed. – malhal Oct 21 '16 at 09:35
2

You could override the setMyAttribute method (using categories) and ensure uniqueness right there, although this may be expensive:

- (void)setMyAttribute:(id)value
{
   NSArray *objects = [self fetchObjectsWithMyValueEqualTo:value];
   if( [objects count] > 0 )  // ... throw some exception
   [self setValue:value forKey:@"myAttribute"];
}

If you want to make sure that every MyEntity instance has a distinct myAttribute value, you can use the objectID of the NSManagedObject objects for that matter.

Martin Cote
  • 28,864
  • 15
  • 75
  • 99
  • Thanks for the fast reply! "You could override the setMyAttribute" That's what I'm doing at the moment. Thankfully the dataset is small enough that getting a list of all the objects is not too expensive, although I was hoping a more elegant solution was possible. "If you want to make sure that every MyEntity instance has a distinct myAttribute value..." That's exactly what I want to do, although I'm struggling to see how the object's ID can be used for this purpose. Is it possible to assign your own ID to each object somehow? –  Feb 10 '10 at 20:08
  • 1
    @robinjam, I was more talking about the fact that you could plug the objectID value somewhere in "myAttribute" to ensure that every values are unique. – Martin Cote Feb 11 '10 at 18:31
  • Oh, I see what you mean now. However, the attribute in question is actually a user-supplied name so that would not be ideal for my purpose. The reason I want to keep them unique is that the user would have no way of telling two objects with the same name apart. See my answer for the solution I settled on in the end. Thanks for your input! –  Feb 11 '10 at 18:38
  • @MartinCote what is **fetchObjectsWithMyValueEqualTo** ? is this the SELECT like doozMen again ? – onmyway133 Nov 01 '13 at 03:16
0

I really liked @DoozMen approach!! I think it's the easiest way to do what i needed to do.

This is the way i fitted it into my project:

The following code cycles while drawing a quite long tableView, saving to DB an object for each table row, and setting various object attributes for each one, like UISwitch states and other things: if the object for the row with a certain tag is not present inside the DB, it creates it.

NSFetchRequest *request = [[NSFetchRequest alloc] init];

request.entity = [NSEntityDescription entityForName:@"Obiettivo" inManagedObjectContext:self.managedObjectContext];
request.predicate = [NSPredicate predicateWithFormat:@"obiettivoID = %d", obTag];
NSError *executeFetchError = nil;
results = [[self.managedObjectContext executeFetchRequest:request error:&executeFetchError] lastObject];

if (executeFetchError) {
    NSLog(@"[%@, %@] error looking up for tag: %i with error: %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), obTag, [executeFetchError localizedDescription]);
} else if (!results) {

    if (obbCD == nil) {
    NSEntityDescription *ent = [NSEntityDescription entityForName:@"Obiettivo" inManagedObjectContext:self.managedObjectContext];
    obbCD = [[Obiettivo alloc] initWithEntity:ent insertIntoManagedObjectContext:self.managedObjectContext];
    }

    //set the property that has to be unique..
    obbCD.obiettivoID = [NSNumber numberWithUnsignedInt:obTag];

    [self.managedObjectContext insertObject:obbCD];
    NSError *saveError = nil;
    [self.managedObjectContext save:&saveError];

    NSLog(@"added with ID: %@", obbCD.obiettivoID);
    obbCD = nil;
}

results = nil;
Paolo83
  • 3
  • 4
0

Take a look at the Apple documentation for inter-property validation. It describes how you can validate a particular insert or update operation while being able to consult the entire database.

Litherum
  • 22,564
  • 3
  • 23
  • 27
0

You just have to check for an existing one :/

I just see nothing that core data really offers that helps with this. The constraints feature, as well as being broken, doesn't really do the job. In all real-world circumstances you simply need to, of course, check if one is there already and if so use that one (say, as the relation field of another item, of course). I just can't see any other approach.

To save anyone typing...

// you've download 100 new guys from the endpoint, and unwrapped the json
for guy in guys {
    // guy.id uniquely identifies
    let g = guy.id
    
    let r = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Guy")
    r.predicate = NSPredicate(format: "id == %d", g)
    
    var found: [CD_Guy] = []
    do {
        let f = try core.container.viewContext.fetch(r) as! [CD_Guy]
        if f.count > 0 { continue } // that's it. it exists already
    }
    catch {
        print("basic db error. example, you had = instead of == in the pred above")
        continue
    }
    
    CD_Guy.make(from: guy) // just populate the CD_Guy
    save here: core.saveContext()
}
or save here: core.saveContext()

core is just your singleton, whatever holding your context and other stuff.

Note that in the example you can saveContext either each time there's a new one added, or, all at once afterwards.

(I find tables/collections draw so fast, in conjunction with CD, it's really irrelevant.)

(Don't forget about .privateQueueConcurrencyType )

Do note that this example DOES NOT show that you, basically, create the entity and write on another context, and you must use .privateQueueConcurrencyType You can't use the same context as your tables/collections .. the .viewContext .

let pmoc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
pmoc.parent = core.container.viewContext
do { try pmoc.save() } catch { fatalError("doh \(error)")}
Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719