I have written a code to parse some JSON and save data to database via magical record:
NSMutableArray *resultsArray = [NSMutableArray array];
NSArray *timesArray = JSON[#"results"];
for (NSDictionary *record in timesArray) {
Time *newTime = [Time MR_createEntity];
newTime.distance = record[#"distance"];
newTime.time = record[#"time"];
newTime.date = [[MMXFormatter instance] dateFromString:record[#"date"]];
newTime.createdAt = [[MMXFormatter instance] dateFromString:record[#"createdAt"]];
newTime.updatedAt = [[MMXFormatter instance] dateFromString:record[#"updatedAt"]];
[resultsArray addObject:newTime];
}
[MagicalRecord saveWithBlock:nil];
The above code does not save to persistent store. I haven't used Magical Record in a while, and seems saving is different from what it used to be. How do i save my data now?
If you want to use saveWithBlock, the code should be
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
Time *newTime = [Time MR_createEntityInContext:localContext];
newTime.distance = ...
...
}
another way is just replace saveWithBlock with MR_saveToPersistentStoreAndWait
NSMutableArray *resultsArray = [NSMutableArray array];
NSArray *timesArray = JSON[#"results"];
for (NSDictionary *record in timesArray) {
Time *newTime = [Time MR_createEntity];
newTime.distance = record[#"distance"];
newTime.time = record[#"time"];
newTime.date = [[MMXFormatter instance] dateFromString:record[#"date"]];
newTime.createdAt = [[MMXFormatter instance] dateFromString:record[#"createdAt"]];
newTime.updatedAt = [[MMXFormatter instance] dateFromString:record[#"updatedAt"]];
[resultsArray addObject:newTime];
}
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
For More Understanding about CoreData With MegicalRecord I would recommend you to go through this tutorial
http://code.tutsplus.com/tutorials/easy-core-data-fetching-with-magical-record--mobile-13680
Related
This is database design with to-many relations.This is my method
-(void)fetchChannelData:(id)responseObject{
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
NSArray *channels = [responseObject objectForKey:#"channels"] ;
for (NSDictionary *channelStack in channels) {
NSLog(#"Tenant id %#",[[ NSUserDefaults standardUserDefaults ] valueForKey:#"test"]);
ChannelsData *channels = [ChannelsData MR_importFromObject:channelStack inContext:localContext];
[channels setCategory:[[channelStack valueForKey:#"category"] firstObject]];
[channels setTenantID:[[ NSUserDefaults standardUserDefaults ] valueForKey:#"test"]];
NSArray *channelLogo = [channelStack objectForKey:#"channelImages"] ;
for (NSDictionary *channelLogoStack in channelLogo) {
ChannelImages *images = [ChannelImages MR_importFromObject:channelLogoStack inContext:localContext];
[channels addImagesObject:images];
[images setChannelID:channels.channelID];
}
}
} completion:^(BOOL contextDidSave, NSError *error) {
if (contextDidSave) {
[self fetchListing];
}
}];
}
Where I am importing my data. I have dropdown where I selected different API points for specific data. All fetching different data.Now issue is that when I launch my app again service is again called and instead data is updated it insert it again.I already Used relatedByAttribute on each entity letting their "ID" be the primary key.]1]1
I am using Magical Record for my app so I can make use of their core data stack that should automatically propagate my changes in my worker context (core data thread) to the default context (of the main thread). I have been creating and updating my objects in a core data write queue and everything was working fine.
Then I ran into this issue that Magical Record was able to save my changes on my worker context, but when it tries to save to the default context, it detects no changes and therefore doesn't save.
Where did I do wrong? In the rest of my app, I am creating and updating in pretty much the same way and it works. Please help. Thank you!
Below is the related code:
Where no changes were detected after all these changes:
dispatch_async(CoreDataWriteQueue(), ^{
if (self.person) {
FTPerson *localPerson = [FTPerson fetchWithID:self.person.id];
[localPerson setName:self.nameField.text];
[localPerson trainWithImages:self.addedImages];
} else {
FTPerson *newPerson = [[FTPerson alloc] initWithName:self.nameField.text andInitialTrainingImages:self.addedImages];
FTGroup *localGroup = [FTGroup fetchWithID:self.group.id];
[newPerson addGroup:localGroup];
}
[[NSManagedObjectContext MR_contextForCurrentThread] MR_saveToPersistentStoreAndWait];
});
I have also tried the saveWithBlock method and no luck:
dispatch_async(CoreDataWriteQueue(), ^{
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
if (self.person) {
FTPerson *localPerson = [FTPerson fetchWithID:self.person.id];
[localPerson setName:self.nameField.text];
[localPerson trainWithImages:self.addedImages];
} else {
FTPerson *newPerson = [[FTPerson alloc] initWithName:self.nameField.text andInitialTrainingImages:self.addedImages];
FTGroup *localGroup = [FTGroup fetchWithID:self.group.id];
[newPerson addGroup:localGroup];
}
}];
});
And here is where I created the person and group objects:
dispatch_async(CoreDataWriteQueue(), ^{
FTPerson *newPerson = [[FTPerson alloc] initWithName:#"test" andInitialTrainingImages:#[[UIImage imageNamed:#"test.jpg"]]];
});
dispatch_async(CoreDataWriteQueue(), ^{
FTGroup *newGroup = [[FTGroup alloc] init];
[self setGroup:newGroup];
});
Also the init methods:
Person
- (id)initWithName:(NSString *)name andInitialTrainingImages:(NSArray *)images {
NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
self = [FTPerson MR_createInContext:context];
self.name = name;
self.id = [[NSUUID UUID] UUIDString];
self.objectIDString = [[self.objectID URIRepresentation] absoluteString];
self.trainingImages = [[NSMutableArray alloc] init];
[context MR_saveToPersistentStoreAndWait];
return self;
}
Group
- (id)init {
NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
self = [FTGroup MR_createInContext:context];
self.id = [[NSUUID UUID] UUIDString];
self.didFinishTraining = NO;
self.didFinishProcessing = NO;
self.photosTrained = #(0);
self.lastProcessedDate = [NSDate date];
[context MR_saveToPersistentStoreAndWait];
return self;
}
FIXED
So the problem ended up being unrelated to magical record.
I was adding objects to my core data NSSet incorrectly. Instead of making it a NSMutableSet and do addObject:, I should have:
NSMutableSet *mutablePhotos = [self mutableSetValueForKey:#"photos"];
[mutablePhotos addObject:photo];
So the problem ended up being unrelated to magical record. I was adding objects to my core data NSSet incorrectly. Instead of making it a NSMutableSet and do addObject:, I should have:
NSMutableSet *mutablePhotos = [self mutableSetValueForKey:#"photos"];
[mutablePhotos addObject:photo];
I don't think you should be using your own dispatch queues when dealing with Core Data. The whole point of the -performBlock: methods on NSManagedObjectContext is that Core Data takes care of executing the provided block on the correct queue.
Now to answer your question. First, are you using MagicalRecord 2.x or 3.0? If 2.x, please make sure you use the develop branch, or alternatively grab the latest release (v2.3beta5 at this time). There are a lot of improvements in the latest dev and release branches.
Second, I think it is important when multithreading with Core Data to do two things:
Always turn on concurrency debugging by editing your scheme and setting -com.apple.CoreData.ConcurrencyDebug 1
Always be very explicit as to which context you mean to use. There is a reason that the method -MR_contextForCurrentThread is no longer recommended by the MagicalRecord team. Modify your create methods to take an NSManagedObjectContext as a parameter so there is never a doubt as to what context is creating it.
Try making the following changes:
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
if (self.person) {
FTPerson *localPerson = [self.person MR_inContext:localContext];
[localPerson setName:self.nameField.text];
[localPerson trainWithImages:self.addedImages];
} else {
FTPerson *newPerson = [[FTPerson alloc] initWithName:self.nameField.text andInitialTrainingImages:self.addedImages inContext:localContext];
FTGroup *localGroup = [self.group MR_inContext:localContext];
[newPerson addGroup:localGroup];
}
}];
Person:
- (id)initWithName:(NSString *)name andInitialTrainingImages:(NSArray *)images inContext:(NSManagedObjectContext*)context {
self = [FTPerson MR_createInContext:context];
self.name = name;
self.id = [[NSUUID UUID] UUIDString];
self.objectIDString = [[self.objectID URIRepresentation] absoluteString];
self.trainingImages = [[NSMutableArray alloc] init];
[context MR_saveToPersistentStoreAndWait];
return self;
}
For Person, I notice you pass in an array of images but you assign an empty array to the new person entity. Do you mean to do that?
Group:
-(id)initWithContext:(NSManagedObjectContext*)context {
self = [FTGroup MR_createInContext:context];
self.id = [[NSUUID UUID] UUIDString];
self.didFinishTraining = NO;
self.didFinishProcessing = NO;
self.photosTrained = #(0);
self.lastProcessedDate = [NSDate date];
[context MR_saveToPersistentStoreAndWait];
return self;
}
I'm missing something in my logic when trying to sync web service data with local store and I need your help. This is what I've got:
I have one NSArray of NSDictionaries describing each event object (downloaded from web), which I sort by event id. Then I fetch local store using IN predicate and also sort it by event id. Then I try to iterate and match the ids and if they match, i update record and if they don't match i create new NSManagedObject. It works fine if the newly downloaded event object has a greater eventID than last eventID in local store, but if the eventID from web service is smaller than the one in local store then it INSERTS ALL OBJECTS, no matter if they exist or not and that exactly is my problem.
So in other words, if a new record is at the beginning of sorted array it will add every object, but if it is at the end of sorted array it will update all except the new one. I need it to create the new one and update old ones.
Here's some code:
The function with the logic where I believe I'm missing something:
- (void)findOrCreateObject:(NSArray*)eventArray
{
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
//get sorted stored records
NSArray *fetchedRecords = [self.fetchedResultsController fetchedObjects];
//sort dictionaries
NSSortDescriptor *aSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"id" ascending:YES];
NSArray *downloadedRecords = [self.events sortedArrayUsingDescriptors:[NSArray arrayWithObject:aSortDescriptor]];
NSLog(#"DOWNLOADED EVENTS = %#", downloadedRecords);
NSLog(#"FETCHED EVENTS = %#", fetchedRecords);
//if store is not empty we need to walk through data and add/update records, otherwise/ELSE we need to import initial data
if (fetchedRecords.count != 0) {
//stores has records already
NSLog(#"FIND OR CREATE PROCESS");
if ([downloadedRecords count] > 0) {
NSArray *storedRecords = [self fetchEvents:eventArray withContext:context];
NSUInteger currentIndex = 0;
for (NSDictionary* event in downloadedRecords) {
Event* eventObject = nil;
if ([storedRecords count] > currentIndex) {
eventObject = [storedRecords objectAtIndex:currentIndex];
}
NSLog(#"STRING VALUE OF KEY = %#", [[eventObject valueForKey:#"eventID"]stringValue]);
if ([[event valueForKey:#"id"] isEqualToString:[[eventObject valueForKey:#"eventID"] stringValue]]) {
//Update Record
NSLog(#"Updating Record!!!");
[self updateManagedObject:eventObject withRecord:event inContext:context];
}
else
{
//New Record
NSLog(#"Inserting Record!!!");
eventObject = (Event*)[NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:context];
eventObject.eventID = [self makeNumberFromString:[event valueForKey:#"id"]];
eventObject.title = [event valueForKey:#"title"];
eventObject.venue = [event valueForKey:#"venue"];
}
currentIndex++;
}
}
}
else
{
//import initial data
NSLog(#"IMPORTING INITIAL DATA");
for (NSDictionary* event in downloadedRecords) {
Event *eventObject = (Event*)[NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:context];
eventObject.eventID = [self makeNumberFromString:[event valueForKey:#"id"]];
eventObject.title = [event valueForKey:#"title"];
eventObject.venue = [event valueForKey:#"venue"];
}
}
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
The FETCHEVENTS method:
-(NSArray*)fetchEvents:(NSArray*)eIDs withContext:(NSManagedObjectContext*)context
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Event" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(eventID IN %#)", eIDs];
[fetchRequest setPredicate:predicate];
[fetchRequest setSortDescriptors:#[ [[NSSortDescriptor alloc] initWithKey: #"eventID" ascending:YES] ]];
NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
NSLog(#"No rows returned");
}
return fetchedObjects;
}
The Update Object method:
- (void)updateManagedObject:(NSManagedObject*)object withRecord:(NSDictionary*)record inContext:(NSManagedObjectContext*)context
{
[object setValue:[self makeNumberFromString:[record valueForKey:#"id"]] forKey:#"eventID"];
[object setValue:[record valueForKey:#"title"] forKey:#"title"];
[object setValue:[record valueForKey:#"venue"] forKey:#"venue"];
}
I'm calling findOrCreate method once I download the web service data and parse it.
Let me know if you have any other questions.
Try this,
- (void)findOrCreateObject:(NSArray*)eventArray {
//if store is not empty we need to walk through data and add/update records, otherwise/ELSE we need to import initial data
if (fetchedRecords.count != 0) {
//stores has records already
NSLog(#"FIND OR CREATE PROCESS");
if ([downloadedRecords count] > 0) {
NSArray *storedRecords = [self fetchEvents:eventArray withContext:context];
for (NSDictionary* event in downloadedRecords) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"eventID = %#",[event valueForKey:#"id"]];
NSArray *matchedArray = [storedRecords filteredArrayUsing
Predicate:predicate];
Event* eventObject = nil;
if ([matchedArray count] > 0) {
//Update Record
NSLog(#"Updating Record!!!");
eventObject = [matchedArray objectAtIndex:0];
[self updateManagedObject:eventObject withRecord:event inContext:context];
}
else
{
//New Record
NSLog(#"Inserting Record!!!");
eventObject = (Event*)[NSEntityDescription insertNewObjectForEntityForName:#"Event" inManagedObjectContext:context];
eventObject.eventID = [self makeNumberFromString:[event valueForKey:#"id"]];
eventObject.title = [event valueForKey:#"title"];
eventObject.venue = [event valueForKey:#"venue"];
}
}
}
} else {
.....
}
}
I think, every time you insert a new event object, you should update storedObjects such that it should now contain the inserted object.
Or more simply, you should put the initialisation line of storedObjects inside your for loop. (This would make sure that as you enumerate from the beginning of downloadedObjects every eventObject will have the same index on it as on storedObjects. But, with regards to your code this will only be true if all elements of storedObjects will always be found in downloadedObjects which, I assume is the case.)
One thing though, isn't fetchedRecords supposed to be the same as storedObjects, if they are you should just reassign storedObjects as [self.fetchedResultsController fetchedObjects], as it would reflect the changes in your context without executing another fetch request which would solve the inefficiency of the suggestion above.
This is a SyncEngine from an RW tutorial. I need help understanding how only UPDATED records from the web are fetched and processed into Core Data.
- (void)processJSONDataRecordsIntoCoreData {
NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
// Iterate over all registered classes --- CHECK!
for (NSString *className in self.registeredClassesToSync) {
if (![self initialSyncComplete]) {
NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
NSArray *records = [JSONDictionary objectForKey:#"results"];
for (NSDictionary *record in records) {
[self newManagedObjectWithClassName:className forRecord:record];
}
} else {
NSArray *downloadedRecords = [self JSONDataRecordsForClass:className sortedByKey:#"objectId"];
if ([downloadedRecords lastObject]) {
NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:#"objectId" usingArrayOfIds:[downloadedRecords valueForKey:#"objectId"] inArrayOfIds:YES];
int currentIndex = 0;
//if dl count is < current index, there is an updated object dl from the web
for (NSDictionary *record in downloadedRecords) {
NSManagedObject *storedManagedObject = nil;
//Quick check to see if they indeed match, if they do, update the stored object with remote service objects
if ([storedRecords count] > currentIndex) {
storedManagedObject = [storedRecords objectAtIndex:currentIndex];
}
//Othwerwise its a new object and you need to create a new NSManagedObject to represent it in CDdb
if ([[storedManagedObject valueForKey:#"objectId"] isEqualToString:[record valueForKey:#"objectId"]]) {
[self updateManagedObject:[storedRecords objectAtIndex:currentIndex] withRecord:record];
} else {
[self newManagedObjectWithClassName:className forRecord:record];
}
currentIndex++;
}
}
}
// After all NSMO are created in your context, save it!
[managedObjectContext performBlockAndWait:^{
NSError *error = nil;
if (![managedObjectContext save:&error]) {
NSLog(#"Unable to save context for class %#", className);
}
}];
// Cleanup time
[self deleteJSONDataRecordsForClassWithName:className];
[self executeSyncCompletedOperations];
}
[self downloadDataForRegisteredObjects:NO];
}
From what I understand, on the first or initial sync, it fetches JSONDictionaryForClassWithName which reads the downloaded data from disk and creates a newManagedObjectWithClassName.
My confusion is in the update else block. downloadedRecords is populated from JSONDataRecordsForClass which simply calls JSONDictionaryForClassWithName. Then it checks to see if there is at least 1 object in that array. If there is it does this:
NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:#"objectId" usingArrayOfIds:[downloadedRecords valueForKey:#"objectId"] inArrayOfIds:YES];
This fetches all managedObjectsForClass:sortedByKey which is below:
- (NSArray *)managedObjectsForClass:(NSString *)className sortedByKey:(NSString *)key usingArrayOfIds:(NSArray *)idArray inArrayOfIds:(BOOL)inIds {
__block NSArray *results = nil;
NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className];
NSPredicate *predicate;
if (inIds) {
predicate = [NSPredicate predicateWithFormat:#"objectId IN %#", idArray];
} else {
predicate = [NSPredicate predicateWithFormat:#"NOT (objectId IN %#)", idArray];
}
[fetchRequest setPredicate:predicate];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:
[NSSortDescriptor sortDescriptorWithKey:#"objectId" ascending:YES]]];
[managedObjectContext performBlockAndWait:^{
NSError *error = nil;
results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
}];
return results;
}
The next bit which compares the [storedRecords count] > currentIndex is confusing. Can someone please explain this? I think my confusion lies in what the managedObjectsForClass method does with the usingArraysOfIds & inArrayOfIds.
I would expect that at some point it gets the the updatedAt field from the downloaded records and compares it to the updatedAt field of the CoreData fetched records.
This function is processing the stored JSON. The actual remote fetching and updateAt checking happens in downloadDataForRegisteredObjects and mostRecentUpdatedAtDateForEntityWithName.
[storedRecords count] > currentIndex is a bit crazy. Although in defense of the original programmer, writing any decent syncengine will quickly make you go googoo. Basically he needs to work out which records are existing and which ones are new and update the local data store accordingly, that's all.
I had another look and this code is actually horribly broken. It will only works if either you have the same records both locally and remotely. Or if the new objects have an objectId that sort-wise comes after the last object the local store has. Which is not the case with Parse objectId's.
If you are testing with just one device this works because new objects will be inserted locally before being pushed to the server. Therefor you will always have the same amount of records. If additional records get inserted any other way, this code will do weird things.
I have 30 Rooms and each Room should have 5 same RoomAttributes.
I have a many to many relationship between Room and RoomAttributes.
My Solutions was, to create 30 * 5 = 150 RoomAttributes and make NSSet's of RoomAttributes for every Room. This is allot of work.
How i creat a Room:
Raum *raum = [NSEntityDescription insertNewObjectForEntityForName:#"Raum" inManagedObjectContext:context];
raum.raumName = #"Main";
raum.etage = #"2. Stock, Raum 1.203";
raum.beschreibung = #"Gut beleuchtet";
raum.raumpreis = [NSNumber numberWithDouble:210];
raum.raumname = #"Besprechungsraum";
How i create RoomAttributes:
Raumattribute *attribute =[NSEntityDescription insertNewObjectForEntityForName:#"Raumattribute" inManagedObjectContext:context];
attribute.attributname = #"Beamer";
attribute.schalter = [NSNumber numberWithBool:NO];
Raumattribute *attribute2 =[NSEntityDescription insertNewObjectForEntityForName:#"Raumattribute" inManagedObjectContext:context];
attribute2.attributname = #"Behindertengerecht";
attribute2.schalter = [NSNumber numberWithBool:NO];
How i create the NSSet:
NSSet *attributeFurRaum = [NSSet setWithObjects:attribute1, attribute2,nil];
raum.raumattribute = attributeFurRaum;
How can i make this easier?
**EDITED
ahh I see - sorry I misunderstood the original question - the edit makes it easier.
For that I would create three helper methods
-(RaumAttribute*)roomAttributeWithName:(NSString *)name andSchalter:(BOOL)schalter
{
Raumattribute *att =[NSEntityDescription insertNewObjectForEntityForName:#"Raumattribute" inManagedObjectContext:context];
att.attributname = name;
att.schalter = schalter;
return att;
}
-(NSSet *)roomAttributes
{
NSArray *atts = [#"Beamer,Behindertengerecht" componentsSeparatedByString:#","];
NSMutableSet *roomAttributes = [NSMutableSet set];
for(NSString *name in atts)
{
[roomAttributes addObject:[self roomAttributeWithName:name andSchalter:NO]];
}
return roomAttributes;
}
-(Raum *)raumFromDictionary:(NSDictionary *)details
{
Raum *raum = [NSEntityDescription insertNewObjectForEntityForName:#"Raum" inManagedObjectContext:context];
raum.raumName = [details valueForKey:#"raumName"];
raum.etage = [details valueForKey:#"etage"];
raum.beschreibung = [details valueForKey:#"beschreibung"];
raum.raumpreis = [details objectForKey:#"raumpreis"];
raum.raumname = [details objectForKey:#"raumname"];
return raum;
}
then you could store your pre-determined object data in a plist or JSON - parse it into a dictionary then go something like:
NSArray *raumDictionaries = //code to get array of dictionaries from a plist or whatever source
NSSet *raumAttributeSet = [self roomAttributes];
for(NSDictionary *raumDict in raumDictionaries)
{
Raum *raum = [self raumFromDictionary:raumDict];
raum.raumattribute = raumAttributeSet;
//save context
}