I have a Roof object in a Realm DB, and I use it to show some data on the screen. After the user logs out, I delete the Roof object and then update the screen. Inside this update method the app crashes with the message: "Object has been deleted or invalidated."
Should the object become invalidated, or is this not supposed to do? Should I just check in the update method if the roof is invalidated, or is there a better way to handle a non-existent object?
Here is the basic code that I use:
class Roof: Object {
dynamic var info: String?
}
let roof = Roof()
let realm = try! Realm()
try! realm.write {
realm.add(roof)
}
try! realm.write {
realm.delete(roof)
}
Following my understand, this is the basic flow: RealmDB -> Container -> View.
When your container (may be an array) holds references to the objects in the DB, but if one was deleted before, then when you update the View, it can take nothing. Because your container is older than the DB.
My answer is query again (update the container), then update your view.
When you delete object from Realm all its instances become invalid and you can't use it anymore. You can check if the object has been invalidate with isInvalidated property.
Related
I’m trying to get my head around background threads and manipulating CoreData via a background MOC, but I’ve ran into a bit of a snag whilst deleting a large number of records from CoreData
My setup is, I have a number of (Main) records, each of which can have 0 to several thousand (Secondary) records associated to them via a one to many relationship
This is represented by 2 view controllers in a master/detail style setup.
ViewController A has a table view that lists all of the main records.
ViewController B then shows the associated records when a cell on viewController A is tapped
Both tables are populated on the main thread and the data is fetched using the persistentContainer.viewContext.
ViewController B gives the user the option to delete all of the associated records in bulk, so I would like this to be done on a background thread, so as not to block the main thread.
The problem I am having is, If I delete the records and close viewController B before the process is finished, it doesn’t delete the records.
However, it deletes them fine if I leave viewController B open until the delete has finished.
One thing I have noticed, is if I do close ViewController B before the delete thread has finished, the view doesn’t deinitialize until the background delete process ends, so it seems to still be deleting after the view is closed, but it doesn’t seem to save to the persistent store unless I leave the view open until the process has finished
Here is the code i use to delete the records in the background:
class GlobalChanges....
static func deleteRecords(records: [NSManagedObject], managedContext: NSManagedObjectContext, finished: () -> Void){
//create a new background MOC
let coreDataManager = CoreDataStack.shared
let backgroundContext = coreDataManager.persistentContainer.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
backgroundContext.performAndWait {
//remove the records from the managed context
for record in records{
let backgroundContextRecord = backgroundContext.object(with: record.objectID) as NSManagedObject
//delete the record
backgroundContext.delete(backgroundContextRecord)
}
do {
//update core data
try backgroundContext.save()
managedContext.refreshAllObjects()
finished()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
}
And this is called when user taps delete by:
#IBAction func deleteButton(_ sender: UIButton) {
let deleteQueue = DispatchQueue(label: "deleteQueue")
deleteQueue.async {
GlobalChanges.deleteRecords(records: self.selectedRows, managedContext: self.managedContext){
DispatchQueue.main.async{
//update UI...
}
}
}
}
How would I get the data to persist when closing ViewController B before the delete process has finished?
Thank in advance
What I would like to contribute to your question is not the solution of your problem per se. But I want to help you improve your approach to "delete all of the associated records in bulk".
If indeed you want to delete all the secondary records of a Primary (Main) records at once, the best approach in CoreData is to edit the to-Many relationship using the Relationship Inspector of the .xcmodeld file and set the Delete Rule to cascade.
Then you delete the primary record itself. Core Data will automatically cascade your delete to all the secondary records. They will be deleted as well without you doing anything else.
If you need the keep the primary record an only clear the relationship, you might consider having a to-One relationship to an intermediate entity that will hold the to-Many secondary records. This way you can delete that intermediate entity an assign a new one to the to-One relationship.
Ok, I’ve sussed it
Each time View controller B is closed, it calculates totals from the remaining (Secondary) records and writes the data back to the (Main) record on the ViewContext.
This was causing a merge conflict due to the backgroundContext and the ViewContext both trying to update the persistent store at the same time.
Update:
To get around this, I queued all context writes, so that only one write was being performed on the store at any given time
let persistentContainerQueue = OperationQueue()
persistentContainerQueue.maxConcurrentOperationCount = 1
func SaveBackgroundContext(backgroundContext: NSManagedObjectContext) {
//add the save operation to the back of the queue
persistentContainerQueue.addOperation(){
backgroundContext.performAndWait{
do {
//update core data
try backgroundContext.save()
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
}
}
}
}
I'm using Realm for my project and I need to query a list of results in a non-UI-blocking thread (ie. background), read only; I consulted Realm's doc, it seems that I need to create the Realm instance in the same thread where it's been queried, so I wonder how expensive it is if I re-create Realm object every time?
#IBAction func scoreAction(_ sender: Any?) {
DispatchQueue.global(qos: .background).async }
let scores = loadScore()
DispatchQueue.main.async {
display(scores)
}
}
}
then:
func loadScore() -> [Score] {
let realm = try! Realm(configuration: config)
return realm.objects(Score.self).filter("some criteria")
}
Calling the initializer of Realm doesn't actually create a new database, it simply creates a new reference to the existing Realm database at the location specified in the RealmConfiguration used in the initializer of Realm. This means that in general, once the database is open, creating a new reference to it by calling Realm() or Realm(configuration: config) isn't expensive computationally. So in general, it can often make more sense to create a new reference to your Realm when switching between threads.
Of course, to know for sure which is the more optimal way for your specific use case, you'll actually need to run tests on a real device, but as long as you're not switching between threads frequently (say several times in a single second), you should be fine with creating a new reference to Realm on both threads after switching between them.
I noticed many problems with accessing realm object, and I thought that my solution would be solving that.
So I have written simple helping method like this:
public func write(completion: #escaping (Realm) -> ()) {
DispatchQueue(label: "realm").async {
if let realm = try? Realm() {
try? realm.write {
completion(realm)
}
}
}
}
I thought that completion block will be fine, because everytime I write object or update it, I use this method above.
Unfortunately I'm getting error:
libc++abi.dylib: terminating with uncaught exception of type realm::IncorrectThreadException: Realm accessed from incorrect thread.
Instances of Realm and Object are thread-contained. They cannot be passed between threads or that exception will occur.
Since you're passing the completion block itself to the background queue at the same time the queue is being created (As Dave Weston said), any Realm objects inside that block will most certainly not have been created on the same thread, which would explain this error.
Like Dave said, you're creating a new dispatch queue every time you call that method. But to expand upon that, there's also no guarantee by iOS that a single queue will be consistently called on the same thread.
As such, best practice with Realm is to recreate your Realm objects on the same thread each time you want to perform a new operation on that thread. Realm internally caches instances of Realm on a per-thread basis, so there's very little overhead involved with calling Realm() multiple times.
To update a specific object, you can use the new ThreadSafeReference feature to re-access the same object on a background thread.
let realm = try! Realm()
let person = Person(name: "Jane") // no primary key required
try! realm.write {
realm.add(person)
}
let personRef = ThreadSafeReference(to: person)
DispatchQueue(label: "com.example.myApp.bg").async {
let realm = try! Realm()
guard let person = realm.resolve(personRef) else {
return // person was deleted
}
try! realm.write {
person.name = "Jane Doe"
}
}
Your method creates a new DispatchQueue every time you call it.
DispatchQueue(name:"") is an initializer, not a lookup. If you want to make sure you're always on the same queue, you'll need to store a reference to that queue and dispatch to it.
You should create the queue when you setup the Realm, and store it as a property of the class that does the setup.
Perhaps it helps someone (as I spent a few hours looking for a solution)
In my case, I had a crash in background mapping of JSON to a model (which imported ObjectMapper_Realm). At the same time there was an instance of realm allocated on main thread.
Generally it happens when you initialised it in different thread and trying to access or modify from different thread. Just put a debugger to see which thread it was initialised and try to use same thread.
I am using Realm in My project, and I want to know whether the realm.write() method is synchronous or not.
My example is here:
let realm = try! Realm()
try! realm.write {
realm.delete(message)
}
realm.invalidate()
In the above example, I am deleting a realm object and outside braces I am writing invalidate()
Here is my confusion:
If write() is synchronous, then invalidate() is ok
And if Async than before write invalidate will call, and realm will release but operation is running in background
Thanks
Realm.write is synchronous. It just calls realm.beginWrite()/realm.commitWrite() with some error handling:
public func write(_ block: (() throws -> Void)) throws {
beginWrite()
do {
try block()
} catch let error {
if isInWriteTransaction { cancelWrite() }
throw error
}
if isInWriteTransaction { try commitWrite() }
}
The method you write is synchronous method as you did not specify the background queue for it.
Purpose of Invalidate() method
func invalidate()
Description
Invalidates all Objects, Results, LinkingObjects, and Lists managed by the Realm.
A Realm holds a read lock on the version of the data accessed by it, so that changes made to the Realm on different threads do not modify or delete the data seen by this Realm. Calling this method releases the read lock, allowing the space used on disk to be reused by later write transactions rather than growing the file. This method should be called before performing long blocking operations on a background thread on which you previously read data from the Realm which you no longer need.
All Object, Results and List instances obtained from this Realm instance on the current thread are invalidated. Objects and Arrays cannot be used. Results will become empty. The Realm itself remains valid, and a new read transaction is implicitly begun the next time data is read from the Realm.
Calling this method multiple times in a row without reading any data from the Realm, or before ever reading any data from the Realm, is a no-op. This method may not be called on a read-only Realm.
Both on simulator and my real device, an array of strings is saved upon app termination. When I restart the app and fetchRequest for my persisted data (either from a viewDidLoad or a manual button action), I get an empty array on the first try. It isn't until the second time I fetchRequest that I finally get my data.
The funny thing is that there doesn't seem to be a time discrepancy involved in this issue. I tried setting various timeouts before trying to fetch the second time. It doesn't matter whether I wait 10 seconds to a minute -- or even immediately after the first fetch; the data is only fetched on the second try.
I'm having to use this code to fetch my data:
var results = try self.context.fetch(fetchRequest) as! [NSManagedObject]
while (results.isEmpty) {
results = try self.context.fetch(fetchRequest) as! [NSManagedObject]
}
return results
For my sanity's sake, here's a checklist:
I'm initializing the Core Data Stack using boilerplate code from Apple: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/InitializingtheCoreDataStack.html#//apple_ref/doc/uid/TP40001075-CH4-SW1
I'm putting my single DataController instance in a static variable at the top of my class private static let context: NSManagedObjectContext = DataController().managedObjectContext
I'm successfully saving my context and can retrieve the items without any issue in a single session; but upon trying to fetch on the first try in a subsequent session, I get back an empty array (and there lies the issue).
Note** I forgot to mention that I'm building a framework. I am using CoreData with the framework's bundle identifier and using the model contained in the framework, so I want to avoid having to use logic outside of the framework (other than initalizing the framework in the appDelegate).
The Core Data stack should be initialized in applicationDidFinishLaunchingWithOptions located in appDelegate.swift because the psc is added after you're trying to fetch your data.
That boilerplate code from Apple includes:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
/* ... */
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
} catch {
fatalError("Error migrating store: \(error)")
}
}
The saved data isn't available until the addPersistentStoreWithType call finishes, and that's happening asynchronously on a different queue. It'll finish at some point but your code above is executing before that happens. What you're seeing isn't surprising-- you're basically looping until the async call finishes.
You need to somehow delay your fetch until the persistent store has been loaded. There are a couple of possibilities:
Do something sort of like what you're already doing. I'd prefer to look at the persistent store coordinator's persistentStores property to see if any stores have been loaded rather than repeatedly trying to fetch.
Post a notification after the persistent store is loaded, and do your fetch when the notification happens.