Core data - Set many-to-one relationship with NSBatchInsertRequest - ios

I have two entities, User and Post. A User contains many Post, and each Post has only one User.
I am trying to batch insert more than 1000 posts.
private func newBatchInsertRequest(with posts: [PostData]) -> NSBatchInsertRequest {
var index = 0
let total = posts.count
let batchInsert = NSBatchInsertRequest(entity: Post.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < total else { return true }
if let post = managedObject as? Post {
let data = posts[index]
post.createdData = data.createdDate
post.identifier = data.identifier
post.text = data.text
}
index += 1
return false
}
PersistenceController.shared.container.performBackgroundTask { context in
try? context.execute(batchInsert)
try? context.save()
}
}
It inserts all the posts that I want to insert. However, I can not configure how to set their User.
I tried to use the following code, but it did not work.
let updateRequest = NSBatchUpdateRequest(entity: Post.entity())
updateRequest.resultType = .updatedObjectIDsResultType
updateRequest.propertiesToUpdate = ["user": user]
try? context.execute(updateRequest)
I get the following error.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Invalid relationship ((<NSRelationshipDescription: 0x2801045a0>), name item, isOptional 1, isTransient 0, entity Post, renamingIdentifier item
I can set their user one by one, but it is inefficient due to long processing time.
How to update the user property of Posts in a more efficient way?
Update
All of these posts belongs to one User which does not exist yet. I need to create it before or after executing the NSBatchInsertRequest.
User has three properties
1. createdDate: Date
2. identifier: UUID
3. name: String
My goal is to insert Post that belongs to one Use either using NSBatchInsertRequest or in a private context so that it does not block the main thread.

Here's what I would try:
Create the User before your batch operation. Yep, it's going to be on a different context — just make sure you grab its NSManagedObjectID (it's a property on NSManagedObject) and save it on a private property.
Inside your PersistenceController.shared.container.performBackgroundTask you have a reference to your private context. Use the managed object ID from before to fetch/register the same User in your private context:
let user = context.object(with: userManagedObjectID)
Then pass this user into the method that creates your batch insert request to set the user property on your Post objects.
I believe if you set up your inverse relationship up in the data model editor you don't need to populate User.posts. That should happen automatically for you.

I solved this problem in different way.
In my entity which needs to have relationship added placeholder attributed tmpID.
When creating batch inserts setting tmpID to particular string. As example parent objectID parent.objectID.
Executing batch insert.
Then fetching all object from CoreData with tmpID.
Then going throw everyone and setting:
Relationship
tmpID to nil, the do not waist a memory.
Saving managed object context.
To merge I am using function:
/// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date.
///
/// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute.
/// - Throws: An error if anything went wrong executing the batch deletion.
public func executeAndMergeChanges(using batchDeleteRequest: NSBatchDeleteRequest) throws {
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self])
}
// Use case:
do {
try container.viewContext.executeAndMergeChanges(using: deleteReqest)
try container.viewContext.save()
} catch {
Logger.app.e("Can not destroy/save clean table: \(name) -> \(error)")
}

Related

NSBatchInsertRequest fetch objects after its completed

I'm using a batch insert request like this:
let insertRequest = NSBatchInsertRequest(entity: EntryObject.entity(), managedObjectHandler: { object in
guard index < total else { return true }
if let entryObject = object as? EntryObject {
let entry = entries[index]
entryObject.setValues(entry: entry, shouldAttemptFetchExistingTags: true)
}
index += 1
return false
})
try context.execute(insertRequest)
Each EntryObject needs to be related to several other entities. However, NSBatchInsertRequest doesn't allow creating relationships. Each object is saved directly to the store without loading it into a context.
So my plan is to fetch each of these objects afterwards and apply the necessary relationships.
I'm using a batch insert request because EntryObject can contain several movies that are expensive as crap to create, and I'm continually getting super hard to trace crashes.
However when I go back to fetch these objects.. they don't exist.
I tried storing the objectIDs in a temporary array. That threw the same error.
Then I tried manually merging the changes into the context I performed the batch insert on, in addition to the viewContext.
insertRequest.resultType = NSBatchInsertRequestResultType.objectIDs
let result = try context.execute(insertRequest) as! NSBatchInsertResult
if let objectIDs = result.result as? [NSManagedObjectID], !objectIDs.isEmpty {
let save = [NSInsertedObjectsKey: objectIDs]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: save, into: [PersistenceController.viewContext, context])
}
And when that didn't work, I watch the 2018 best practices for CoreData and got this going..
try context.performAndWait {
let request = NSPersistentHistoryChangeRequest ()
request.resultType = .transactionsAndChanges
let result = try context.execute (request) as! NSPersistentHistoryResult
let transactions = result.result as! Array<NSPersistentHistoryTransaction>
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
}
try context.save()
It's supposed to merge all the changes to the store into the main context.
However when I go back to fetch these objects, on either context.. they don't exist. Not until I kill the app and start over.
I've been trying to figure out how to completely reinit a store.. but have had no success.
How do I fetch these objects right after I create them using an NSBatchInsertRequest?

How to use func assign(_ object: Any, to store: NSPersistentStore) of a NSManagedObjectContext?

Setup:
My app uses 2 persistent stores managed by a single persistent store coordinator that is used by several managed object contexts. Both persistent stores store the same entities.
It is required that any inserts, updates and deletes of managed objects are only done in one of both persistent stores, and the app can select the persistent store.
Problem:
In order to store a managed object only in one persistent store, I want to use func assign(_ object: Any, to store: NSPersistentStore) of a NSManagedObjectContext. The docs say
Specifies the store in which a newly inserted object will be saved.
Question:
What means „newly inserted“?
Can I assign a persistent store as long as the object has never been saved by its context?
Could I use, e.g., a willSaveObjectsNotification to loop though all registeredObjects of the context and assign the required persistent store?
When the app switches the persistent store, I would then reset the contexts so that all managed objects would be fetched newly, and the required persistent store could be set again in the willSaveObjectsNotification notification.
Is this the right way to go?
That function can only be used for a managed object that does not belong to any managed object context yet. It’s possible to create an instance using an entity description, that’s not part of any context. For example you can use init(entity:,insertInto:) with a nil for the second argument. If you do that, you can later use the `assigns function to insert it into a context.
If a managed object is already associated with a context, you can’t use this function to move it to a different one. You would need to create a new instance in the new context and copy all the property values.
Based on Tom Harrington’s answer, I did the following tests.
The result is:
As long as a managed object has never been saved to a persistent store, one can use assign(_:to:) to define one of several persistent stores into which the object will be saved later.
My code:
// Setup:
// privateStore and sharedStore are persistent stores to which the Item entity is assigned.
// Item is a managed object whose entity is assigned to the persistent stores privateStore and sharedStore.
// It has an attribute "name" to distibguish it from other objects.
// Create for privateStore and sharedStore one fetch request respectively for an Item with name "item"
let namePredicate = NSPredicate(format: "name == %#", "item")
let privateStoreItemFetchRequest = NSFetchRequest<Item>(entityName: Item.entityName)
privateStoreItemFetchRequest.predicate = namePredicate
privateStoreItemFetchRequest.affectedStores = [privateStore]
let sharedStoreItemFetchRequest = NSFetchRequest<Item>(entityName: Item.entityName)
sharedStoreItemFetchRequest.predicate = namePredicate
sharedStoreItemFetchRequest.affectedStores = [sharedStore]
// 1st test: Create an Item without a context, assign it to a store, insert it into a context and try to fetch it from both stores
let item1 = Item(entity: itemEntity, insertInto: nil)
item1.name = "item"
viewContext!.assign(item1, to: privateStore)
viewContext!.insert(item1)
let items1InPrivateStore = try? viewContext!.fetch(privateStoreItemFetchRequest)
print("Test 1: privateStore: \(items1InPrivateStore ?? [])") // Prints item1
let items1InSharedStore = try? viewContext!.fetch(sharedStoreItemFetchRequest)
print("Test 1: sharedStore: \(items1InSharedStore ?? [])") // Prints []
// 2nd test: Create an Item and insert it into a context, assign it to a store and try to fetch it from both stores
let item2 = Item(entity: itemEntity, insertInto: viewContext!)
item2.name = "item"
viewContext!.assign(item2, to: privateStore)
let items2InPrivateStore = try? viewContext!.fetch(privateStoreItemFetchRequest)
print("Test 2: privateStore: \(items2InPrivateStore ?? [])") // Prints item1 and item2
let items2InSharedStore = try? viewContext!.fetch(sharedStoreItemFetchRequest)
print("Test 2: sharedStore: \(items2InSharedStore ?? [])") // Prints []
// Save the context and try to fetch both items from both stores
do {
try viewContext!.save()
} catch { fatalError() }
let items3InPrivateStore = try? viewContext!.fetch(privateStoreItemFetchRequest)
print("Test 3: privateStore: \(items3InPrivateStore ?? [])") // Prints item1 and item2
let items3InSharedStore = try? viewContext!.fetch(sharedStoreItemFetchRequest)
print("Test 3: sharedStore: \(items3InSharedStore ?? [])") // Prints []
// item1 has been assigned above to the privateStore. Try now to assign it to the sharedStore
viewContext!.assign(item1, to: sharedStore) // Run time error: "Can't reassign an object to a different store once it has been saved."

How to fetch relationship entities when performing a core data migration?

I am performing a core data migration. I have this subclass for my migration:
class TagtoSkillMigrationPolicyV1toV2: NSEntityMigrationPolicy {
override func createDestinationInstances( forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
// 1 - create destination instance of skill and skillmetadata
let skillEntityDescription = NSEntityDescription.entity( forEntityName: "Skill",in: manager.destinationContext)
let newSkill = Skill(entity: skillEntityDescription!, insertInto: manager.destinationContext)
let skillMetaDataDescription = NSEntityDescription.entity(forEntityName: "SkillMetaData", in: manager.destinationContext)
newSkill.metaData = SkillMetaData(entity: skillMetaDataDescription!, insertInto: manager.destinationContext)
var posts = sInstance.value(forKey: "posts")
posts.sort {
($0.timeStamp! as Date) < ($1.timeStamp! as Date)
}
if let mostRecentThumbnail = posts.first?.postImage {
let thumbnail = Utils.makeThusmbnail(url: URL(string: mostRecentThumbnail)!, for: 150.0)
newSkill.metaData?.thumbnail = thumbnail?.pngData()
}
if let name = sInstance.value(forKey: "name"){
newSkill.setValue(name, forKey: "name")
}
manager.associate(sourceInstance: sInstance, withDestinationInstance: newSkill, for: mapping)
}
}
This custom migration is from my old Tag entity to my new Skill entity. For each Tag, it creates a skill entity and a skillmetaData entity that it attaches to it. There are no entries in the .xcmappingmodel file, as I am doing everything manually - please let me know if I still need entries there.
Right now, when it runs, it gives a signal SIGABRT:
Could not cast value of type '_NSFaultingMutableOrderedSet' (0x7fff87c44e80) to 'NSArray' (0x7fff87c51fb0).
2020-04-14 15:55:40.108927-0700 SweatNetOffline[28046:3645007] Could not cast value of type '_NSFaultingMutableOrderedSet' (0x7fff87c44e80) to 'NSArray' (0x7fff87c51fb0).
When I set a breakpoint and inspect sInstance.value(forKey: "posts"), I get
▿ Optional<Any>
- some : Relationship 'posts' fault on managed object (0x6000010dcf00) <NSManagedObject: 0x6000010dcf00> (entity: Tag; id: 0xdf4b05c2e82b2c4e <x-coredata://A6586C18-60C3-40E7-B469-293A50EB5728/Tag/p1>; data: {
latestUpdate = "2011-03-13 00:17:25 +0000";
name = flowers;
posts = "<relationship fault: 0x600002607f40 'posts'>";
uuid = "4509C1B3-7D0F-4BBD-AA0B-7C7BC848DA80";
})
so its not entirely loading those posts - its giving that fault there.
The goal is to get the thumbnail from the latest of those posts. How can I access this relationship item?
I found a way to do it using
let lastThumbnail = sInstance.mutableOrderedSetValue(forKeyPath: "posts.postImage").lastObject
Reading this helped a lot. My understanding is at this point in the migration, we are dealing with NSManagedObjects and never the actual Model which would have attributes like "posts" that it usually does in core data.
In Objective-C especially and swift to some extent, NSObjects are accessed with key-value coding. They are accessed indirectly - meaning instead of reading the object itself, you are reading what the NSMutableOrderedSet wants to show you for that key - so it can do stuff to it before presenting it. Thus you can't for instance, fetch just "forKey: posts" and then expect posts to have the attributes your post usually does. Anyone is welcome to flesh out this explanation if they know more :).
One thing I was trying to do is to fetch by that key-path in a sortable way - so that I can find the post that has the most recent timestamp and then use that associated thumbnail. Instead I am using .lastObject which works because this is an NSMutableOrderedSet and not an NSMutableSet because my original model specified this relationship to be an ordered relationship. This means that it will pick the last inserted item which is ok for me for now. Still interested in a way to filter that result though if anyone has advice.

Updating CoreData Entity or saving a new one

I'm fairly new to CoreData, and I'm trying to make a game. I have a couple of questions I was hoping you guys could help me out with some guidance:
- does GameKit already have some sort of CoreData integrated in it? I am not sure if I am overthinking this CoreData stuff if there's already something that replaces it in GameKit.
. . .
Anyways, assuming the answer to the above question is "no. GameKit has nothing to save your game". I will proceed with my current "Save game" code which is the following:
func saveCurrentMatch()
{
/* CORE DATA STUFF:
FIRST NEED TO VERIFY IF THIS GAME HAS BEEN PREVIOUSLY SAVED, IF SO THEN UPDATE, ELSE JUST SAVE
Entity: MatchData
Attributes: scoreArray (String), playerArray (String), myScore (Int), matchID (Int), isWaiting (Bool), isRealTime (Bool), gameLog (String)
*/
let context = myAppDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "MatchData")
request.returnsObjectsAsFaults = false
do
{
let gamesInProgress = try context.fetch(request)
print (gamesInProgress.count)
if gamesInProgress.count > 0 //HERE CHANGE THIS TO LOOK FOR THE MATCH ID OF THIS GAME!!
{
gameExistsinCD = true
}
else
{
gameExistsinCD = false
}
}
catch
{
print ("Error Reading Data: \(error.localizedDescription)")
}
if gameExistsinCD
{
//CODE TO UPDATE MATCH INSTEAD OF SAVING NEW ONE
}
else
{
// CODE TO SAVE A NEW MATCH FOR THE FIRST TIME
let matchData = NSEntityDescription.insertNewObject(forEntityName: "MatchData", into: context)
matchData.setValue(isRealTime, forKey: "isRealTime")
matchData.setValue(currentScore?[0], forKey: "myScore")
matchData.setValue(currentScore?.map{String($0)}.joined(separator: "\t"), forKey: "scoreArray") // IS THIS CODE CORRECT? I'M TRYING TO SAVE AN ARRAY OF INTS INTO A SINGLE STRING
matchData.setValue(currentPlayers?.joined(separator: "\t"), forKey: "playerArray")
matchData.setValue(true, forKey: "isWaiting") //will change later to update accordingly.
matchData.setValue(matchID, forKey: "matchID")
matchData.setValue(gameLog, forKey: "gameLog")
do
{
try context.save()
print ("CoreData: Game Saved!")
}
catch
{
print ("Error Saving Data: \(error.localizedDescription)")
}
}
}
My main concern is on the fetch request, how do I check all the core-data if this match has already been saved? and if so, whats the code for updating an Entity instead of inserting a new one?
Any guidance is appreciated, thanks!
Don't let Core Data scare you. It can be a fine way to save local data and despite some comments, it is not slow when done right. In fact, Core Data can be quite fast.
You can simplify your code a lot by using your Object class in a more normal fashion instead of using setValue calls. Your create code can be changed to this:
// CODE TO SAVE A NEW MATCH FOR THE FIRST TIME
if let matchData = NSEntityDescription.insertNewObject(forEntityName: "MatchData", into: context) as? MatchData {
matchData.isRealTime = isRealTime
matchData.myScore = currentScore?[0]
matchData.scoreArray = currentScore?.map{String($0)}.joined(separator: "\t") // IS THIS CODE CORRECT? I'M TRYING TO SAVE AN ARRAY OF INTS INTO A SINGLE STRING
// You can certainly save it this way and code it in and out. A better alternative is to have a child relationship to another managed object class that has the scores.
matchData.playerArray = currentPlayers?.joined(separator: "\t")
matchData.isWaiting = true
matchData.matchID = matchID
matchData.gameLog = gameLog
}
This is a much more readable and normal way to set your object properties. Any time you change a property on a core data managed object then it will get saved the next time you save the context.
As far as finding a current record that matches the ID, I like to add classes like that to my Managed Object class itself:
class func findByID(_ matchID: String) -> MatchData? {
let myAppDelegate = UIApplication.shared.delegate as! AppDelegate
let context = myAppDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "MatchData")
let idPredicate = NSPredicate(format: "matchID = \(matchID)", argumentArray: nil)
request.predicate = idPredicate
var result: [AnyObject]?
var matchData: MatchData? = nil
do {
result = try context.fetch(request)
} catch let error as NSError {
NSLog("Error getting match: \(error)")
result = nil
}
if result != nil {
for resultItem : AnyObject in result! {
matchData = resultItem as? MatchData
}
}
return matchData
}
Then any place you need the match data by ID you can call the class function:
if let matchData = MatchData.findByID("SomeMatchID") {
// Match exists
}
Core data is basically wrapper around SQL database. It is very efficient when you are working with high volume of data that need to be stored. So please consider either you had such requirements, otherwise perhaps it can be wise to store data in user defaults, or settings.
If it is, there is few things you need to know.
It is very useful to create you own model classes. Open core data model file, open "Editor/Create NSManagedObject subclass". It will allow you to refer direct properties, instead of KVC(setValue:forKey:).
Alway mind what thread you are working in. It is unsafe to work with objects, created in other threads.
Your gamesInProgress contains array of objects you fetched from your database.
So basically instead of
if gameExistsinCD
{
//CODE TO UPDATE MATCH INSTEAD OF SAVING NEW ONE
}
else
{
// CODE TO SAVE A NEW MATCH FOR THE FIRST TIME
let matchData = NSEntityDescription.insertNewObject(forEntityName: "MatchData", into: context)
matchData.setValue(isRealTime, forKey: "isRealTime")
<...>
you can do
let matchData = (gamesInProgress.first ??
NSEntityDescription.insertNewObject(forEntityName: "MatchData", into: context)) as! <YouEntityClass>
matchData.isRealTime = isRealTime
<...>
PS: https://www.raywenderlich.com/173972/getting-started-with-core-data-tutorial-2

Realm: Why when updating an object, it loses its children references?

I'm writing some Unit tests for my Realm model but I'm getting a problem when updating saved Realm objects. Th problem is about losing the one-to-many references after an update.
Please, see the example below:
(I'm using Quick, but it should be readable even if you don't know it)
// Create a test User.
let user = User.init()
user.account_id = 1
user.email = "foo#bar.com"
user.cards.append(Car(value: ["id": 1]))
user.cards.append(Car(value: ["id": 2]))
user.boxes.append(Box(value: ["id": 5]))
user.boxes.append(Box(value: ["id": 6]))
// Save User.
self.testRealm.write { () -> Void in
self.testRealm.add(user, update: true)
}
// Get User.
let testUser = self.testRealm.objectForPrimaryKey(User.self, key: 1)
expect(testUser).toNot(beNil())
// Update the user by creating a new one.
let newuser = User.init()
newuser.account_id = 1
newuser.email = "fooBar#bar.com"
// Update a test User.
self.testRealm.write { () -> Void in
self.testRealm.add(newuser, update: true)
}
// Get User again.
let testUpdatedUser = self.testRealm.objectForPrimaryKey(User.self, key: testUserId)
expect(testUpdatedUser?.email).to(equal("fooBar#bar.com"))
expect(testUpdatedUser?.boxes.count).to(equal(2)) <--- FAILS
expect(testUpdatedUser?.cards.count).to(equal(2)) <--- FAILS
And my Model:
import RealmSwift
class User: Object {
override static func primaryKey() -> String? {
return "account_id"
}
dynamic var email = ""
// The list of Boxes that user owns.
let boxes = List<Box>()
// The list of Cars that user owns.
let cars = List<Car>()
}
That's the expected behavior. Properties, which have nil values, are merged with the existing values if you set the optional parameter update: true. List properties may not be nil. An empty list could be also a legitimate more up-to-date value than a list of some objects.
You'd need to retrieve the object and manually merge the properties according to your will.

Resources