i'm inserting new objects into the database by core data. Is there any way to check if there is any duplicate in the database before i insert the values in?
AccountDetails * newEntry = [NSEntityDescription insertNewObjectForEntityForName:#"AccountDetails" inManagedObjectContext:self.managedObjectContext];
newEntry.acc_date=date;
newEntry.bank_id=bank_id1;
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Whoops, couldn't save: %#", [error localizedDescription]);
}
[self.view endEditing:YES];
everytime i run the app , it reinsert the values again. i want to check if there is any new category in it if there isnt then i will add that new one in only.
thanks in advance..
You can fetch or you can count. Counting is much faster than fetching. Depends on what you are trying to do.
If you just want to insert new and skip duplicates then use -[NSManagedObjectContext countForFetchRequest: error:] to determine if the object exists.
You can pre-build the predicate and just replace the unique value on each loop so that even the cost of the predicate is low. This is fairly performant but not the best solution because it hits the disk on each loop.
Another option would be to change the fetch to have:
Just the unique value
A NSDictionary result
Then grab all of the unique values from your insertable array into an array of strings (for example) then do a single fetch with:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"myUnique in %#", uniqueIDArray]];
Then you have an array of uniques that ARE in the store already. From there as you loop over your objects you check against that array, if the unique in there you skip, otherwise you insert. That will yield the best performance for a straight insert or skip requirement.
You need to fetch from the db and check, your code will be doing something like this helper method I use frequently in my code, if the results.count is > 1, then DUPLICATE found :
- (NSManagedObject*) findOrCreateObjectByValue:(id)value
propertyName:(NSString*)propertyName
entityName:(NSString*)entityName
additionalInfo:(NSDictionary*)additionalInfo
context:(NSManagedObjectContext*)context
error:(NSError* __autoreleasing*)error
{
NSManagedObject* res = nil;
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:entityName];
[r setPredicate:[NSPredicate predicateWithFormat:#"%K == %#",propertyName,value]];
NSArray* matched = [context executeFetchRequest:r
error:error];
if (matched) {
if ([matched count] < 2) {
res = [matched lastObject];
if (!res) { //No existing objects found, create one
res = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:context];
[res setValue:value
forKey:propertyName];
}
} else {
if (error) {
*error = [NSError errorWithDomain:#"some_domain"
code:9999
userInfo:#{#"description" : #"duplicates found"}];
}
}
}
return res;
}
Related
I have a problem checking whether a particular attribute of an Entity exists in the Core Data Database (through predicates) before creating a new object; if the object exists, I'd rather return it than create a new object.
I have a simple App which has a table view with a plus button in the Navigation Bar; the user clicks that and is presented with a View Controller with 4 text fields. They fill in that information, press save and it gets saved to Core Data and displayed in the TableView (through the use of NSFetchedResultsControllers).
The data model is as follows:
Transaction Entity with isReceived BOOL attribute
Person Entity with name string attribute
Occasion Entity with title string attribute
Item Entity with amount string attribute
The transaction has a relationship to the Person (whoBy), Occasion (Occasion) and Item entities.
In the view controller with the save method, I have the code below which will insert new objects into the Transaction, Person, Occasion Entities, etc. Each Transaction is of course unique, but with each transaction, the user can select an existing PERSON and/or Occasion and if that person then does not exist, it will be created (likewise with the occasion).
I'm slightly confused as to the format of the code here.
EDIT: I have tried a combination of code and can just not get this working. In the code below, I'm referencing person.name in the predicate, but I also tried creation a local NSString variable to hold the self.nameTextField.text code but that did nothing. I tried creating a NSString property to reference it that way and that not work. I tried using the words MATCHES, LIKE, CONTAINS, == and every combination in-between.
- (IBAction)save:(id)sender
{
NSManagedObjectContext *context = [self managedObjectContext];
Transaction *transaction= [NSEntityDescription insertNewObjectForEntityForName:#"Transaction" inManagedObjectContext:context];
Person *person = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:context];
Occasion *occasion = [NSEntityDescription insertNewObjectForEntityForName:#"Occasion" inManagedObjectContext:context];
Item *amount = [NSEntityDescription insertNewObjectForEntityForName:#"item" inManagedObjectContext:context];
NSFetchRequest *personFind = [NSFetchRequest fetchRequestWithEntityName:#"Person"];
personFind.predicate = [NSPredicate predicateWithFormat:#"name == %#", person.name];
// I have tried every combination of the predicate like MATCHES, LIKE.
// I created a local NSString variable and an NSString property
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES];
personFind.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSError *error = nil;
NSArray *matches = [context executeFetchRequest:personFind error:&error];
if (!matches || ([matches count] > 1))
{
// Handle Error
}
else if ([matches count] == 0)
{
person.name = self.nameTextField.text;
transaction.whoBy = person;
occasion.title = self.occasionTextField.text;
transaction.occasion = occasion;
}
else
{
person = [matches lastObject];
transaction.whoBy = person;
occasion.title = self.occasionTextField.text
transaction.occasion = occasion;
}
if (![context save:&error])
{
NSLog(#"Can't save! %# %#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
Logically, what I want to achieve is:
When the user is adding a Transaction, check if it's for a new person or an existing one — if it's an existing one, choose it from a list of Persons (and when the user selects a person, get its NSManagedObjectID). If it's a new one, create it on the spot.
The same for the Occasion.
Set all the other fields of the Transaction object (amount, etc.).
My question is:
What predicate do I use to get this working?
When I put a break point in this method, a NEW NAME (one that doesn't exist before) correctly calls the else if ([matches count] == 0) method and if I create an entry with an existing name, it calls the
else
{
person = [matches lastObject];
transaction.whoBy = person;
occasion.title = self.occasionTextField.text
transaction.occasion = occasion;
}
Even with the this statement, it is still creating a new person object for the same name.
I will correctly implement the occasion after getting the person working, but I'm just lost on how to get this working.
Any help would be massively appreciated!
"Is this correct?":
No. You are creating a new Person and Occasion objects whether you are using an existing person/occasion or not.
First check for existence and only if the object not already exist, insert a new one.
Alternatively, if the person/occasion exist, delete the inserted object.
"How do I retrieve the managedObjectID for person/event?":
Person* person = /*Get an existing person object*/
NSManagedObjectID* personId = person.objectID /*This is the person object ID, will work for any NSManagedObject subclass*/
To find a person that start with a string str use this predicate in a fetch request:
/*UNTESTED*/
[NSPredicate predicateWithFormat:#"(name BEGINSWITH[cd] %#)", str];
Edit:
To be more precise, you practice find or create using something like this:
(this is very limited, and only good for a single object performance-wise)
(NOT TESTED)
- (NSManagedObject*) findOrCreateObjectByValue:(id)value
propertyName:(NSString*)propertyName
entityName:(NSString*)entityName
additionalInfo:(NSDictionary*)additionalInfo
context:(NSManagedObjectContext*)context
error:(NSError* __autoreleasing*)error
{
NSManagedObject* res = nil;
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:entityName];
[r setPredicate:[NSPredicate predicateWithFormat:#"%K == %#",propertyName,value]];
NSArray* matched = [context executeFetchRequest:r
error:error];
if (matched) {
if ([matched count] < 2) {
res = [matched lastObject];
if (!res) { //No existing objects found, create one
res = [NSEntityDescription insertNewObjectForEntityForName:entityName
inManagedObjectContext:context];
[res setValue:value
forKey:propertyName];
[res setValuesForKeysWithDictionary:additionalInfo];
}
} else {
if (error) {
*error = [NSError errorWithDomain:#"some_domain"
code:9999
userInfo:#{#"description" : #"duplicates found"}];
}
}
}
return res;
}
So now, your save: method should look something like:
(I assume here that the person name and occasion title are held by a UITextField on the view controller [txtPersonName and txtOccasionTitle respectively] )
- (void) save:(id)sender
{
//create a clean context so that changes could be discarded automatically on failure
NSManagedObjectContext* context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setParentContext:[self managedObjectContext]];
//A Transaction is always created in save event, so add it to the context
Transaction* transaction = [NSEntityDescription insertNewObjectForEntityForName:#"Transaction" inManagedObjectContext:context];
__block NSError* error = nil;
Person* p = (Person*)[self findOrCreateObjectByValue:self.txtPersonName.text
propertyName:#"name"
entityName:#"Person"
additionalInfo:nil
context:context
error:&error];
if (!p) {
NSLog(#"Error: %#, person name: %#",error,self.txtPersonName.text);
return;
}
Occasion* o = (Occasion*)[self findOrCreateObjectByValue:self.txtOccasionTitle.text
propertyName:#"title"
entityName:#"Occasion"
additionalInfo:nil
context:context
error:&error];
if (!o) {
NSLog(#"Error: %#, occasion title: %#",error,self.txtOccasionTitle.text);
return;
}
transaction.whoBy = p;
transaction.occasion = o;
//Not sure what you are using this property for
transaction.item = [NSEntityDescription insertNewObjectForEntityForName:#"Item"
inManagedObjectContext:context];
NSManagedObjectContext* ctx = context;
if ([context obtainPermanentIDsForObjects:[context.insertedObjects allObjects]
error:&error])
{
//save your changes to the store
__block BOOL saveSuccess = YES;
while (ctx && saveSuccess) {
[ctx performBlockAndWait:^{
saveSuccess = [ctx save:&error];
}];
ctx = [ctx parentContext];
}
if (!saveSuccess) {
NSLog(#"Could not save transaction, error: %#",error);
}
} else {
NSLog(#"Could not obtain IDs for inserted objects, error: %#",error);
}
//Do what you have to do next
}
This is just for making things a bit clearer on what you should do to avoid duplications, and reuse existing objects.
I'm trying to add unique values into my sqlite DB.
I've tried to create unique attribute into the .xcdatamodeld but I didn't succeed.
This part of code adds all terminaison in the Table F016Terminaison. How can I add only unique terminaison values?
for (NSString *terminaison in [[verbe objectForKey:temps] objectForKey:#"terminaison"])
{
F016Terminaison *newTerminaison = [NSEntityDescription insertNewObjectForEntityForName:#"F016Terminaison" inManagedObjectContext:self.managedObjectContext];
newTerminaison.name = terminaison;
}
Each call to insertNewObjectForEntityForName creates a new object and inserts it into
the managed object context.
If you want only unique values, you have to check first if an object with that value
already exists. That is done with an NSFetchRequest with a predicate like
[NSPredicate predicateWithFormat:#"name == %#", terminaison];
If you have to insert many values, then it is more effective to fetch all objects having
values from the new list first instead of many fetch requests.
Compare "Implementing Find-or-Create Efficiently" in the "Core Data Programming Guide".
You need to do a fetch to find existing instances of an object, then add it if it doesn't exist.
eg:
// assumes your object has a "uniqueAttribute" and you supply a uniqueValue to test
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"uniqueAttribute == %#", uniqueValue];
NSFetchRequest * fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setPredicate:predicate];
[fetchRequest setFetchLimit:1];
NSError * error = nil;
NSArray * results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
if ([results count] == 1) {
id existingObject = [results objectAtIndex:0];
// do something with existing object
}
else if (results) {
// create a new object
}
else {
NSLog(#"executeFetchRequest returned error: %# (userInfo: %#)", [error localizedDescription], [error userInfo]);
}
I have 90 CoreData entities called "ItemModel" with 2 attributes 'uid', 'description', where each of the item is inserted as an NSManagedObject:
NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName: #"ItemModel" inManagedObjectContext: AFYDelegate.managedObjectContext];
The first server call assigns the 'uid' to each of the 90 items fetched above for key "uid". The context is not saved here.
On a later second server call, I like to update 'description' for the 90 items, for each of the NSManagedObject using indexPath - by fetching and passing each object to the following method and saving the context:
[self updateItemToDataModel:object withData: description];
....
....
- (void)updateItemToDataModel:(NSManagedObject *) object withData:(NSString *)data
{
[object setValue:data forKey:#"description"];
NSError * error = nil;
if (![self.managedObjectContext save:&error]) {
//Handle any error with the saving of the context
NSLog(#"%#",error.localizedDescription);
}
}
The above works fine in updating CoreData BUT after closing the Simulator and running the code again, there will be two duplicates for each item with the same 'uid' and 'description'. This means I have 180 items now. Repeatedly closing and running the code creates more and more items.
I tried removing updateItemToDataModel method, resetting the Simulator and it works fine with 90 items.
I'm new to CoreData if someone can help. What's wrong with my code if I only wished to update existing items?
You are inserting a new object into the MOC (managed object context) each time--instead of doing a fetch and finding an existing instance of the object you wish to update.
To fetch the existing object you might execute a fetch request like so...
NSPredicate * predicate = [NSPredicate predicateWithFormat:#"uid == %#", uidToMatch];
NSFetchRequest * fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setPredicate:predicate];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"ItemModel" inManagedObjectContext:managedObjectContext]];
NSError * error = nil;
NSArray * results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
if ([results count]) {
// you may need to handle more than one match in your code...
// you could also set a fetch limit of 1 and guarantee you only get the first object, eg: [fetchRequest setFetchLimit:1];
}
else {
// no results
}
You might want to wrap that in a helper function so you can re-use it. And read up on NSFetchRequest, NSPredicate and writing predicates in order to do fancier fetch requests.
I'm dong data processing in a child moc in a background queue. I need to query the database by ID so that I can differentiate updating-existing-object from creating-new-object. I found most of the time(the total processing time is about 2s for 50 items) is consumed by executeFetchRequest:error:. The NSPredicate is of the simplest form — only to match a single ID attribute(ID attribute is already indexed), and the NSFetchRequest should return one or none(ID is unique). Is there any way to optimize this kind of NSFetchRequest?
Here is my current code:
+ (User *)userWithID:(NSNumber *)ID inManagedObjectContext:(NSManagedObjectContext *)context {
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"User"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ID == %#", ID];
[fetchRequest setPredicate:predicate];
[fetchRequest setFetchBatchSize:1];
NSError *error = nil;
NSArray *users = [context executeFetchRequest:fetchRequest error:&error];
if (error) {
abort();
}
if ([users count] == 1) {
return [users objectAtIndex:0];
} else if ([users count] > 1) {
// Sanity check.
…
} else {
return nil;
}
}
As #ChrisH pointed out in comments under the question, doing a fetch for every ID is no good. So I changed my processing flow to this:
Enumerate data the first time to extract IDs.
Do a single fetch to fetch all existing users matching IDs and put them in a dictionary keyed by ID(named as existingUsers).
Enumerate data the second time to do the real processing: in each iteration, either update one existing user found in existingUsers or create a new user, add it into existingUsers if it is new.
The code is almost doubled, but so is the performance. Really good tradeoff!
To expand on my comment to the original question, it's not efficient to repeatedly perform fetch requests with Core Data when importing data.
The simplest approach, as #an0 indicated, is to perform one fetch of all the existing objects you will be checking against, and then constructing an NSDictionary containing the objects with the attribute you will be checking as keys. So sticking with the original User and userID example:
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"User"];
NSError *error = nil;
NSArray *users = [context executeFetchRequest:fetchRequest error:&error];
if (error) {
//handle appropriately
}
NSMutableDictionary *userToIdMap = [NSMutableDictionary dictionary];
for (User *user in users){
[userToIdMap setObject:user forKey:user.ID];
}
Now in your method that processes new data you can check the userToIdMap dictionary instead of making fetch requests.
A more sophisticated approach, suited to larger data sets, is outlined in the Core Data Programming Guide's Efficently Importing Data. Take a look at the section called 'Implementing Find-Or-Create Efficiently'. The approach suggested by Apple here misses out some code regarding how to walk the arrays you create, and my solution to that problem is in this SO question: Basic array comparison algorithm
I'm importing data into a Core Data store using RestKit and need to check for duplicates. If the item is already in the store, I'd like to update it with the latest attributes. If it's a new item, I'd like to create it.
The import was slow so I used Instruments and saw that the longest part of importing was checking to see if the item already exists (with a fetch request)
So I'd like to know if checking to see if the item is already in the store, is it faster to:
use countForFetchRequest to see if the item already exists, then executeFetchRequest to return the item to update or
just executeFetchRequest to get the item to update
or is there a better way to do this?
I thought countForFetchRequest would be faster since the entire NSManagedObject isn't returned and only execute the fetch request if I know there's going to be a NSManagedObject.
Thanks
- (Product *)productWithId:(int)productID {
NSManagedObjectContext *context = [Model sharedInstance].managedObjectContext;
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"product_id == %d", productID];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Product" inManagedObjectContext:context];
request.predicate = predicate;
request.fetchLimit = 1;
NSError *error = nil;
NSUInteger count = [context countForFetchRequest:request error:&error];
if (!error && count == 1) {
NSArray *results = [context executeFetchRequest:request error:&error];
if (!error && [results count]) {
return [results objectAtIndex:0];
}
return nil;
}
return nil;
}
As far I know, the best way to find and/or import objects within Core Data is described in Implementing Find-or-Create Efficiently.
The documentation describes a find or create pattern that it's based on sorting data: the data you download from the service and the data you grab form the store.
I really suggest you to read the link I provided. You will see a speed up on your performances.
Obviously you should do the work in background, preventing the main thread to freeze, using thread confinement or new iOS Core Data queue API.
Hope that helps.