Is there a way to save NSManagedObjects 1 at a time - ios

I'm facing this problem where I have an NSMutableArray containing NSManagedObjects.
And I want to iterate each one NSManagedObject, set a value to a variable and then save them.
The problem is that I have a verification to see if the objects are in Database or not, and after I save my first NSManagedObject to the database all the others NSManagedObjects that are in that array are also inserted into the database...
Here's a piece of code that describes what is my issue:
for (Category* c in categories) {
Category* original = [Database getCategoryByID:c.categoryid];
// After the first save this will have value because it saves all the objects
// that are in categories Array and the version will be the same...
if (original != nil && original.version >= c.version) {
// This object is already up to date so do not make any changes
} else {
// This object is not up to date, or it does not exist
// update it's contents
c.name = name;
[[c managedObjectContext] MR_saveToPersistentStoreAndWait];
// Here it saves all the objects instead of only 1 object
}
}
Is there a way to only save 1 object at a time while having other NSManagedObjects inside an NSMutableArray ?

With Core Data, you tell a context to save, and it saves everything it has. There's no way to save just one object unless that object is the only one in a context with changes.
In your case, it looks like your array of Category objects are managed objects and that they belong to a managed object context. The easiest way to avoid unexpected saving is to move the save command until after the loop. When you reach that point,
Any Category that needed to be created or updated is ready to save
Any Category that didn't need to be updated has no changes, so it won't be affected by saving the context.
So it should be safe to save everything in the context then. If it has changes, it gets updated, and if it doesn't, it won't change.

Related

What happens to local variables storing references to deleted NSManagedObjects

When I delete an NSMangedObject from the database, what happens to local variables who were assigned to it?
For example, I have a simple NSManagedObject:
class MyManagedObject: NSManagedObject {
#NSManaged var name: String
}
And then in my ViewController, I pull it out of the database, and assign it locally:
class ViewController: UIViewController {
var myManagedObject: MyManagedObject!
}
Then I delete it from the database.
If print the object name I get the following in the console
print("myManagedObject.name = \(myManagedObject.name)")
//prints: "myManagedObject.name = "
As if the object isn't there? But if I turn the variable into an optional and check it for nil, I am told it's not nil.
I'm not quite sure how to reconcile this in my mind. There seems to be something pointing to the local variable, but its properties are gone.
If I have many disparate UI objects that rely on that object for its properties, I can't assume that there is some local deep copy of it in memory?
Here is more complete code:
In viewDidLoad I create the new object, save the context, fetch the object, then assign it locally.
class ViewController: UIViewController {
var myManagedObject: MyManagedObject!
override func viewDidLoad() {
super.viewDidLoad()
//1 Create the new object
let newObject = NSEntityDescription.insertNewObject(forEntityName: "MyManagedObject", into: coreDataManager.mainContext) as! MyManagedObject
newObject.name = "My First Managed Object"
//2 Save it into the context
do {
try coreDataManager.mainContext.save()
} catch {
//handle error
}
//3 Fetch it from the database
let request = NSFetchRequest<MyManagedObject>(entityName: "MyManagedObject")
do {
let saved = try coreDataManager.mainContext.fetch(request)
//4 Store it in a local variable
self.myManagedObject = saved.first
} catch {
//handle errors
}
}
}
At this point if I print the local variable's name property, I get the correct response:
print("The object's name is: \(myManagedObject.name)")
//prints: The object's name is: My First Managed Object
So, now I delete it from the database:
if let storedObject = myManagedObject {
coreDataManager.mainContext.delete(storedObject)
do {
try coreDataManager.mainContext.save()
} catch {
//handle error
}
}
But now, when I print I get the strangest output:
print("myManagedObject.name = \(myManagedObject.name)")
//prints: "myManagedObject.name = "
This is totally not the way I'm expecting memory to work. If I create a instance of a class Foo, and then pass that instance around to different objects, it's the same instance. It only goes away once no one is pointing to it.
In this case--- what is the variable, myManagedObject? It's not nil. And what is the string, name? Is it an empty string? Or is it some other weird meta-type?
The main thing what you are probably looking here for is the core data context. The context is a connection between your memory and the actual database.
Whenever you fetch the data you fetch it through context. These are managed objects which can be modified or even deleted. Still none of these really happen on the database until you save the context.
When you delete an object it is marked for deletion but it is not deleted from the memory, it must not be since if nothing else it will still be used by the context to actually delete the object from the database itself.
What happens to the managed object once you call to delete it is pretty much irrelevant, even if documented it may change as it is a part of the framework. So it is your responsibility to check these cases and to refetch the objects once needed. So you must ensure your application has a proper architecture and uses core data responsibly.
There are very many ways on how you use your database and more or less any of them has a unique way of using it optimally. You need to be more specific on what you are doing and where do you see potential issues so we can get you on the right track.
To give you an example consider data synchronization from remote server. Here you expect that data can be synchronized at any time no matter what user is doing or what part of the application he is.
For this I suggest you have a single context which operates on a separate thread. All the managed objects should be wrapped and its properties copied once retrieved from database. On most entities you would have something like:
MyEntity.findAll { items in
...the fetch happens on context thread and returns to main, items are wrappers
}
MyEntity.find(id: idString, { item in
...the fetch happens on context thread and returns to main, items are wrappers
})()
Then since you do not have any access to the managed object directly you need some kind of method to copy the data to the managed object like:
myEntityInstance.commit() // Copies all the data to core data object. The operation is done on a context thread. A callback is usually not needed
And then to save the database
MyEntity.saveDatabse {
... save happens on the context thread and callback is called on main thread
}
Now the smart part is that saveDatabse method will report to a delegate that changes have been made. A delegate is usually the current view controller so it makes sense to have a superclass like DataBaseViewController which on view did appear assigns itself as a delegate MyEntity.delegate = self, on view did load calls some method reloadData and in the databaseDidChange delegate method calls reloadData and same in viewWillAppear.
Now each of your view controllers that are subclass of DataBaseViewController will override the reloadData and in that method you will fetch the data from the database again. Either you are fetching all items or a single one. So for those singe ones you need to save the id of the object and fetch it again by that id. If the returned object is nil then item was deleted so you catch the issue you seem to be mentioning.
All of these things are oversimplified but I hope you get a basic idea about core data and how to use it. It is not easy, it never was and it most likely never will be. It is designed for speed to be able to access the data even from a very large database in shortest time possible. The result is that it might not be very safe.

Fetch data from context that is not yet committed. CoreData

I have created multiple instances of an NSManagedObject entity(for example Car: NSManagedObject) in the default NSManagedObjectContext using MagicalRecord.
I didn't save the context. Is there a way to execute a fetch request and obtain the data that is already in persistent state and the data not yet committed that was added in default context ?
Yes, it is fetched. Please check Apple docs at https://developer.apple.com/reference/coredata/nsmanagedobjectcontext:
An object that meets the criteria specified by request (it is an instance of the entity specified by the request, and it matches the request’s predicate if there is one) and that has been inserted into a context but which is not yet saved to a persistent store, is retrieved if the fetch request is executed on that context.
not as far as I know... you can fetch it all and then look at the objects objectID to decide which one was already saved.
id all = [ctx fetch..];
id savedOnly = [NSMutableArray array];
for(id o in all) {
if([[o objectID] isTemporary] == NO) {
[savedOnly addObject:o];
}
}
OR change your code to use to contexts -- that may be better :D
OR maybe use a predicate like:
savedOnly = [ctx fetchWithPredicate:#"... self.objectID.isTemporary=NO"];
MIGHT work... don't know
If we are talking about the same managed object context - yes, you should be able to get also those objects, which are not committed. This is the default behavior. The managed object context contains also the uncommitted objects, i.e. the objects which are not saved yet in the persistent store.

Use NSManagedObject in child NSManagedObjectContext instead of its parent

I have two MOCs: the first is the root context. When I save this context, the changes are saved to the persistent store coordinator. The second MOC has the first MOC as the parent. When I save the second MOC, I also have to save the first MOC in order to save the changes in the second MOC to the persistent store coordinator.
I use the second MOC to let the user edit an object. He can save or cancel the changes. When he saves the changes, all MOCs are saved. When he cancels the changes, I call rollback() of the second MOC.
Unfortunately, the object comes from the first MOC. This means, I execute an NSFetchRequest to fetch the object on the first MOC. Then I create the second MOC in which the user can edit the object. But there is a problem: when the second MOC should change something, for example delete an object that is contained in an array of the original object the user wants to edit, this is not possible, because a MOC can only delete objects that have this MOC as the context. But the object was fetched in the first MOC.
That's why I need to "transfer" somehow the object from the first MOC to the second MOC before the user edits the object. I don't want to fetch the object again with a NSFetchRequest or something, there must be a better way…
Is this possible? Or do you recommend to do this completely different, maybe without parent contexts?
This is where the objectID property of NSManagedObject will come in handy.
Ask the object for its ID
let objectID = myManagedObject.objectID
Ask the child context for a managed object with that ID
do {
let childManagedObject = try childContext.existingObjectWithID(objectID)
print("\(newObject)")
} catch {
}
I think you might be complicating your time with this. Unless it is completely necessary for proposed changes to also be saved there should be no reason to have two contexts for this. There are multiple ways for you to handle a temporary data which you can use to compare your actual record to without having it stored twice.
Why don't you just create a copy of the NSManagedObject and handle the information correction by comparison or simply replacing the original NSManagedObject with the data of the copy and then save it? I personally like this setup a bit more since all I have to do is either compare individual properties when update is desired.
When a full update is required than you can simply work directly on the one NSManagedObject without worrying about copies since you will probably be replacing the entire thing anyway. Like I said, there are other ways to handle, but if it is absolutely necessary for you to have both contexts then looking for a comparison of each property value to the replaced value and then simply save the one in the parent context.

NSManagedObject ignoring changed transformable

I have a very basic caching system for some objects that conform to NSCoding. A method takes the object, extracts some information for keys and search parameters, creates or updates an NSManagedObject, and saves it to the persistent store:
+ (void)updateQuestion:(StacManQuestion *)question site:(StacManSite *)site
{
// 1. Look up with a basic predicate.
SECachedQuestion *cachedQuestion = [SECachedQuestion
findFirstWithQuestionId:question.questionId site:site];
// 2. Create if missing.
if (!cachedQuestion) {
cachedQuestion = [SECachedQuestion MR_createEntity];
cachedQuestion.questionId = question.questionId;
cachedQuestion.site = site.apiSiteParameter;
}
// 3. Update properties.
cachedQuestion.isFavorite = question.favorited;
cachedQuestion.lastAccessTime = [NSDate date];
cachedQuestion.question = question;
// 4. Save
[[NSManagedObjectContext MR_contextForCurrentThread]
MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
/* ... */
}];
}
The first time this code runs, it works perfectly. The object gets saved to the cache and is instantly available for the next run. After that, however, assigning a new value to cachedQuestion.question does nothing.
I've added some breakpoints and found the following:
The assignment to question does not update changedValues:
(lldb) po [cachedQuestion changedValues] {
lastAccessTime = "2014-07-16 17:16:45 +0000";
}
question and cachedQuestion.question start out with the different addresses and then end with the same so an assignment is definitely occurring.
If I put a step before the assignment that sets cachedQuestion.question = nil, I get the following, but the the record for question disappears again on reassignment.
(lldb) po [cachedQuestion changedValues] {
lastAccessTime = "2014-07-16 17:16:45 +0000";
question = "<null>";
}
Here's another super up thing. I went ahead and did a test where I just set cachedQuestion.question = nil for a few builds of the app. This is working fine for subsequent lookups in a single run, but every time when the app launches the first read of cachedQuestion.question goes back to the original value! I can confirm that new cached questions are begin created and lastAccessTime has no problem updating. It's just this one field that is frozen in time. This was because of data validation. nil got it to show up in changed values but save failed as it was not optional.
UPDATE
Right now I'm doing a sad sad hack where any time there is a change I delete the old record and insert a new one. It works well enough but I'd much rather know what's going wrong.
You're not writing your changes to the correct context. First, stop using contextForCurrentThread. It's deprecated and will cause you a crash 1 time out of 100, which is one too many. You also risk crashing since this method can and will return a context that is not the default context. Making managed object assignments across contexts will also crash, for good reason.
Instead, you should explicitly create your context, write your changes to it, and then save it at the end of your method. That will likely solve the majority of the problems you're seeing.
I had to read this several times...
I think the problem here is that you do not use relationships correctly.
NSManagedObjects should be set up as an Entity.
It should not be Transformable but a relationship to an entity.
From the context you can get the first all objects in the first entity in a MutableArray, then get all of its related objects in a new MutableArray of objects through its relationship by calling this when one object is selected:
self.newArray = [[self.oldObject.relationshipName allObjects]mutableCopy];
You will then only get content related to selected object and not all objects in the related entity.
This means there should not be a question attribute but a relationship to the entity question.

CoreDataGeneratedAccessors to remove object don't seem to be deleting

I have an NSManagedObject that has a to-many relationship to another NSManagedObject.
During creation of the NSManagedObject I can use the generated accessors 'removeNotesObject' and the deletion works fine. I can create an object to add to the parent object, save the object, delete the object and then save again. When I fetch this parent object the object I created and deleted is still deleted.
However, after I add the object and then save it (but don't delete and save after) and then fetch it, I can't seem to delete the object that was previously created. I am using the generated accessors to try and remove the object, which appears to work but when I fetch it again the object hasn't been deleted.
(Note: Adding objects does work so it is not a problem with the saving)
To delete the object I retrieve the set of object and select the objects I want to delete. Then I remove the objects
NSSet *notes = summary.notes;
NSSet *oldNotes = [notes objectsPassingTest:^(id obj,BOOL *stop){
Note *oldNote = (Note *)obj;
BOOL sameRow = (oldNote.row == newNote.row);
BOOL sameColumn = (oldNote.column == newNote.column);
BOOL success = (sameRow && sameColumn);
return success;}];
[summary removeNotes:oldNotes];
I have tried making the relationship inverse to delete the objects which didn't delete them. I have also tried different delete rules (cascade and nullify) which again didn't work. Finally, I tried to remove each object separately and deleting each object from the context after I had removed it from the parent object which again unfortunately didn't work.
I assume the problem must be something to do with it being a fetched object. If anyone could help I would really appreciate it as I can't think of any other ways to test or solve this problem.
You need to do
NSManagedObjectContext * moc = .......;
[moc deleteObject:note]
edit: The core data generated accessors simply remove the object from the relationship, but do not delete the object permanently. This makes sense because you may have one NSManagedObject associated to multiple other NSManagedObjects via relationships.
edit: Deleting in the above mentioned fashion will invoke the deletion rules. I suggest you double check that they are setup correctly.
The reason the above code did not work is that == will not actually compare the NSNumber. Instead you need to call 'isEqualTo:'. I think before it was checking the address hence working before I saved it. What's more it was returning an object in the NSSet so appeared to be working. During debugging it wasn't clear what the object was but clearly wasn't the one I needed.

Resources