Objects relations won't update with fetchExisting(object) #31

Closed
opened 2025-12-29 15:22:40 +01:00 by adam · 4 comments
Owner

Originally created by @Jeehut on GitHub (Jan 22, 2016).

Hi, I'm currently writing an app where I try to partition all of its different parts into modules which I can test much easier. One module is responsible for storing the data so I wrote a framework that does exactly that using CoreStore. In my tests everything seemed to work except for refetching the related set of objects of a NSManagedObject subclass.

To be more specific, here's the test that fails (I'm using Quick and Nimble for testing):

var wordPool: WordPool!
beforeEach {
    CoreDataHelper.sharedInstance.setupCoreStore()
    wordPool = WordPool.create(name: "Test")
}
describe("destroy()") {

    var wordPoolItem: WordPoolItem!
    beforeEach {
        wordPoolItem = WordPoolItem.create(clue: "Some clue for 'Test'", answer: "TEST", wordPool: wordPool)
    }

    it("destroys entry") {

        expect(WordPoolItem.all().count).to(equal(1))
        wordPool = wordPool.reload()
        expect(wordPool.items.count).to(equal(1))

        wordPoolItem.destroy()

        expect(WordPoolItem.all().count).to(equal(0))
        wordPool = wordPool.reload()
        expect(wordPool.items.count).to(equal(0)) // this line fails: expected to equal <0>, got <1>

    }

}

Now my question is basically why the items property of the WordPool subclass of NSManagedObject still contains one object after deleting the only item existing (and even checking it is deleted successfully two lines right before the failing line) when it should be empty. Am I missing something here? Did I even use CoreStore the way you intended it to? I like to have methods as simple as all(), create(..), update(...) and destroy() in usage so things are kept as simple as possible when using the framework.

I first thought about asking this on SO but it somehow feels like a bug within CoreStore to me as everything works except for the .items property update. Thanks in advance for any help or clarification.

For reference here are my classes:

WordPool

//
//  WordPool.swift
//  WordPoolStorage
//
//  Created by Cihat Gündüz on 10.01.16.
//  Copyright © 2016 Flinesoft. All rights reserved.
//

import Foundation
import CoreData

import CoreStore
import HandySwift

public class WordPool: NSManagedObject {

    // MARK: - Class Methods

    public static func all() -> [WordPool] {
        return CoreStore.fetchAll(From(WordPool), Where(true))!
    }

    public static func create(name name: String) -> WordPool {
        var wordPool: WordPool!
        CoreStore.beginSynchronous { transaction in
            wordPool = transaction.create(Into(WordPool))
            wordPool.name = name
            transaction.commit()
        }
        return wordPool
    }


    // MARK: - Instance Methods

    public func update(name name: String) -> WordPool {
        CoreStore.beginSynchronous { transaction in
            let wordPool = self.reload()
            wordPool.name = name
            transaction.commit()
        }
        return self.reload()
    }

    public func destroy() {
        CoreStore.beginSynchronous { transaction in
            transaction.delete(self)
            transaction.commit()
        }
    }

    public func reload() -> WordPool {
        return CoreStore.fetchExisting(self)!
    }

}


// MARK: - CoreDataProperties

extension WordPool {

    @NSManaged var name: String
    @NSManaged var items: NSSet

}

WordPoolItem

//
//  WordPoolItem.swift
//  WordPoolStorage
//
//  Created by Cihat Gündüz on 10.01.16.
//  Copyright © 2016 Flinesoft. All rights reserved.
//

import Foundation
import CoreData

import CoreStore
import HandySwift

public class WordPoolItem: NSManagedObject {

    // MARK: - Class Methods

    public static func all() -> [WordPoolItem] {
        return CoreStore.fetchAll(From(WordPoolItem), Where(true))!
    }

    public static func create(clue clue: String, answer: String, wordPool: WordPool) -> WordPoolItem {
        var wordPoolItem: WordPoolItem!
        CoreStore.beginSynchronous { transaction in
            wordPoolItem = transaction.create(Into(WordPoolItem))
            wordPoolItem.clue = clue
            wordPoolItem.answer = answer
            wordPoolItem.wordPool = transaction.fetchExisting(wordPool)!
            transaction.commit()
        }
        return wordPoolItem
    }


    // MARK: - Instance Methods

    public func update(clue clue: String, answer: String) -> WordPoolItem {
        CoreStore.beginSynchronous { transaction in
            let wordPoolItem = self.reload()
            wordPoolItem.clue = clue
            wordPoolItem.answer = answer
            transaction.commit()
        }
        return self.reload()
    }

    public func destroy() {
        CoreStore.beginSynchronous { transaction in
            transaction.delete(self)
            transaction.commit()
        }
    }

    public func reload() -> WordPoolItem {
        return CoreStore.fetchExisting(self)!
    }

}


// MARK: - CoreDataProperties

extension WordPoolItem {

    @NSManaged var clue: String
    @NSManaged var answer: String
    @NSManaged var wordPool: WordPool

}

Here's a screenshot of my data model:

datamodel

The delete rules are 'Cascade' on the side of WordPool and 'Nullify' on the side of WordPoolItem.

I'm also willing to invite you to the project so you can investigate the issue directly within the project if you like. Just send me an email in that case (see my profile).

Originally created by @Jeehut on GitHub (Jan 22, 2016). Hi, I'm currently writing an app where I try to partition all of its different parts into modules which I can test much easier. One module is responsible for storing the data so I wrote a framework that does exactly that using CoreStore. In my tests everything seemed to work except for **refetching the related set of objects of a `NSManagedObject` subclass**. To be more specific, here's the test that fails (I'm using [Quick](https://github.com/Quick/Quick) and [Nimble](https://github.com/Quick/Nimble) for testing): ``` Swift var wordPool: WordPool! beforeEach { CoreDataHelper.sharedInstance.setupCoreStore() wordPool = WordPool.create(name: "Test") } describe("destroy()") { var wordPoolItem: WordPoolItem! beforeEach { wordPoolItem = WordPoolItem.create(clue: "Some clue for 'Test'", answer: "TEST", wordPool: wordPool) } it("destroys entry") { expect(WordPoolItem.all().count).to(equal(1)) wordPool = wordPool.reload() expect(wordPool.items.count).to(equal(1)) wordPoolItem.destroy() expect(WordPoolItem.all().count).to(equal(0)) wordPool = wordPool.reload() expect(wordPool.items.count).to(equal(0)) // this line fails: expected to equal <0>, got <1> } } ``` Now my question is basically why the `items` property of the `WordPool` subclass of `NSManagedObject` still contains one object after deleting the only item existing (and even checking it is deleted successfully two lines right before the failing line) when it should be empty. Am I missing something here? Did I even use CoreStore the way you intended it to? I like to have methods as simple as `all()`, `create(..)`, `update(...)` and `destroy()` in usage so things are kept as simple as possible when using the framework. I first thought about asking this on SO but it somehow feels like a bug within CoreStore to me as everything works except for the `.items` property update. Thanks in advance for any help or clarification. For reference here are my classes: **WordPool** ``` Swift // // WordPool.swift // WordPoolStorage // // Created by Cihat Gündüz on 10.01.16. // Copyright © 2016 Flinesoft. All rights reserved. // import Foundation import CoreData import CoreStore import HandySwift public class WordPool: NSManagedObject { // MARK: - Class Methods public static func all() -> [WordPool] { return CoreStore.fetchAll(From(WordPool), Where(true))! } public static func create(name name: String) -> WordPool { var wordPool: WordPool! CoreStore.beginSynchronous { transaction in wordPool = transaction.create(Into(WordPool)) wordPool.name = name transaction.commit() } return wordPool } // MARK: - Instance Methods public func update(name name: String) -> WordPool { CoreStore.beginSynchronous { transaction in let wordPool = self.reload() wordPool.name = name transaction.commit() } return self.reload() } public func destroy() { CoreStore.beginSynchronous { transaction in transaction.delete(self) transaction.commit() } } public func reload() -> WordPool { return CoreStore.fetchExisting(self)! } } // MARK: - CoreDataProperties extension WordPool { @NSManaged var name: String @NSManaged var items: NSSet } ``` **WordPoolItem** ``` Swift // // WordPoolItem.swift // WordPoolStorage // // Created by Cihat Gündüz on 10.01.16. // Copyright © 2016 Flinesoft. All rights reserved. // import Foundation import CoreData import CoreStore import HandySwift public class WordPoolItem: NSManagedObject { // MARK: - Class Methods public static func all() -> [WordPoolItem] { return CoreStore.fetchAll(From(WordPoolItem), Where(true))! } public static func create(clue clue: String, answer: String, wordPool: WordPool) -> WordPoolItem { var wordPoolItem: WordPoolItem! CoreStore.beginSynchronous { transaction in wordPoolItem = transaction.create(Into(WordPoolItem)) wordPoolItem.clue = clue wordPoolItem.answer = answer wordPoolItem.wordPool = transaction.fetchExisting(wordPool)! transaction.commit() } return wordPoolItem } // MARK: - Instance Methods public func update(clue clue: String, answer: String) -> WordPoolItem { CoreStore.beginSynchronous { transaction in let wordPoolItem = self.reload() wordPoolItem.clue = clue wordPoolItem.answer = answer transaction.commit() } return self.reload() } public func destroy() { CoreStore.beginSynchronous { transaction in transaction.delete(self) transaction.commit() } } public func reload() -> WordPoolItem { return CoreStore.fetchExisting(self)! } } // MARK: - CoreDataProperties extension WordPoolItem { @NSManaged var clue: String @NSManaged var answer: String @NSManaged var wordPool: WordPool } ``` Here's a screenshot of my data model: <img width="338" alt="datamodel" src="https://cloud.githubusercontent.com/assets/6942160/12498078/1e2a9d68-c0a0-11e5-922b-56c27fb78812.png"> The **delete rules** are 'Cascade' on the side of `WordPool` and 'Nullify' on the side of `WordPoolItem`. I'm also willing to invite you to the project so you can investigate the issue directly within the project if you like. Just send me an email in that case (see my profile).
adam added the question label 2025-12-29 15:22:40 +01:00
adam closed this issue 2025-12-29 15:22:40 +01:00
Author
Owner

@JohnEstropia commented on GitHub (Jan 22, 2016):

@Dschee I believe this is because you are extracting objects out of their transaction scope:

     public static func create(name name: String) -> WordPool {
        var wordPool: WordPool!
        CoreStore.beginSynchronous { transaction in
            wordPool = transaction.create(Into(WordPool))
            wordPool.name = name
            transaction.commit()
        }
        return wordPool // dead instance
    }

The MOC for the wordPool you returned already expired, and the behavior from then on is undefined.

Also, I don't think this does what you expect it to:

    public func reload() -> WordPoolItem {
        return CoreStore.fetchExisting(self)!
    }

This fetches an object from the main context, so you can't use this object instance inside transactions like so:

    public func update(clue clue: String, answer: String) -> WordPoolItem {
        CoreStore.beginSynchronous { transaction in
            let wordPoolItem = self.reload()    // Should be transaction.edit(self) or transaction.fetchExisting(self)
            wordPoolItem.clue = clue
            wordPoolItem.answer = answer
            transaction.commit()
        }
        return self.reload() // It's actually safe to just return self, as long as you used the proper instance within the transaction
    }

These mistakes are easy to make when doing operations via the NSManagedObject instead of executing from the data stack/transactions directly, so CoreStore doesn't encourage the pattern. I suggest avoiding methods that are vague in read/write usage. Otherwise, just create a transaction with CoreStore.beginUnsafe() and use it globally (never use CoreStore.xxx methods).

@JohnEstropia commented on GitHub (Jan 22, 2016): @Dschee I believe this is because you are extracting objects out of their transaction scope: ``` swift public static func create(name name: String) -> WordPool { var wordPool: WordPool! CoreStore.beginSynchronous { transaction in wordPool = transaction.create(Into(WordPool)) wordPool.name = name transaction.commit() } return wordPool // dead instance } ``` The MOC for the wordPool you returned already expired, and the behavior from then on is undefined. Also, I don't think this does what you expect it to: ``` swift public func reload() -> WordPoolItem { return CoreStore.fetchExisting(self)! } ``` This fetches an object from the main context, so you can't use this object instance inside transactions like so: ``` swift public func update(clue clue: String, answer: String) -> WordPoolItem { CoreStore.beginSynchronous { transaction in let wordPoolItem = self.reload() // Should be transaction.edit(self) or transaction.fetchExisting(self) wordPoolItem.clue = clue wordPoolItem.answer = answer transaction.commit() } return self.reload() // It's actually safe to just return self, as long as you used the proper instance within the transaction } ``` These mistakes are easy to make when doing operations via the NSManagedObject instead of executing from the data stack/transactions directly, so CoreStore doesn't encourage the pattern. I suggest avoiding methods that are vague in read/write usage. Otherwise, just create a transaction with `CoreStore.beginUnsafe()` and use it globally (never use CoreStore.xxx methods).
Author
Owner

@Jeehut commented on GitHub (Jan 23, 2016):

Thank you for your answer, @JohnEstropia. I actually like how this library is designed (which is why I'm using it in favor of others) but my goal is to abstract away from the implementation of how my data is saved within my framework so that I can change the implementation at all times without the need to change the usage of it in apps. So that's why I opted for the above solution which is both easy to understand and use (e.g. WordPool.create(name: "NewWordPool")) but still abstracts away from the fact that CoreStore or even CoreData is used at all.

I'm not sure though what you were referring to when you said:

These mistakes are easy to make when doing operations via the NSManagedObject instead of executing from the data stack/transactions directly, so CoreStore doesn't encourage the pattern. I suggest avoiding methods that are vague in read/write usage.

Is there another way of abstracting away from the implementation + keeping usage simple and at the same time not use NSManagedObject instances and passing them around? If there is, I'd be glad to rethink my approach. But passing a stack / transaction to the app that is using my data storage framework and calling methods like commit from there is not an option as this would disclose unnecessary implementation details (in my opinion).

In any case, I just changed my code to fix the issues you mentioned, now not only my tests for the destroy method fail, but also the ones for update(...). Here's my new create and update methods:

WordPool

    public static func create(name name: String) -> WordPool {
        var wordPool: WordPool!
        CoreStore.beginSynchronous { transaction in
            let transactionWordPool = transaction.create(Into(WordPool))
            transactionWordPool.name = name
            transaction.commit()
            wordPool = CoreStore.fetchExisting(transactionWordPool)
        }
        return wordPool   
    }

    public func update(name name: String) {
        CoreStore.beginSynchronous { transaction in
            let editableSelf = transaction.edit(self)!
            editableSelf.name = name
            transaction.commit()
        }
    }

WordPoolItem

    public static func create(clue clue: String, answer: String, wordPool: WordPool) -> WordPoolItem {
        var wordPoolItem: WordPoolItem!
        CoreStore.beginSynchronous { transaction in
            let transactionItem = transaction.create(Into(WordPoolItem))
            transactionItem.clue = clue
            transactionItem.answer = answer
            transactionItem.wordPool = transaction.fetchExisting(wordPool)!
            transaction.commit()
            wordPoolItem = CoreStore.fetchExisting(transactionItem)
        }
        return wordPoolItem
    }

    public func update(clue clue: String, answer: String) {
        CoreStore.beginSynchronous { transaction in
            let editableSelf = transaction.edit(self)!
            editableSelf.clue = clue
            editableSelf.answer = answer
            transaction.commit()
        }
    }

Seems like I'm still not using this correctly. I thought the issue was that I called fetchExisting after the beginSynchronous closure was closed. Also I removed returning anything from update as you stated that returning self would be sufficient (and as returning self would be redundant, I just removed it completely). What is my misconception?

@Jeehut commented on GitHub (Jan 23, 2016): Thank you for your answer, @JohnEstropia. I actually like how this library is designed (which is why I'm using it in favor of others) but my goal is to **abstract away from the implementation** of how my data is saved within my framework so that I can change the implementation at all times without the need to change the usage of it in apps. So that's why I opted for the above solution which is both **easy to understand and use** (e.g. `WordPool.create(name: "NewWordPool")`) but still abstracts away from the fact that CoreStore or even CoreData is used at all. I'm not sure though what you were referring to when you said: > These mistakes are easy to make when doing operations _via the NSManagedObject_ instead of executing from the data stack/transactions directly, so CoreStore doesn't encourage the pattern. I suggest avoiding methods that are _vague in read/write usage_. Is there another way of abstracting away from the implementation + keeping usage simple and at the same time **not** use NSManagedObject instances and passing them around? If there is, I'd be glad to rethink my approach. But passing a stack / transaction to the app that is using my data storage framework and calling methods like `commit` from there is not an option as this would disclose unnecessary implementation details (in my opinion). In any case, I just changed my code to fix the issues you mentioned, now not only my tests for the `destroy` method fail, but also the ones for `update(...)`. Here's my **new create and update methods**: **WordPool** ``` Swift public static func create(name name: String) -> WordPool { var wordPool: WordPool! CoreStore.beginSynchronous { transaction in let transactionWordPool = transaction.create(Into(WordPool)) transactionWordPool.name = name transaction.commit() wordPool = CoreStore.fetchExisting(transactionWordPool) } return wordPool } public func update(name name: String) { CoreStore.beginSynchronous { transaction in let editableSelf = transaction.edit(self)! editableSelf.name = name transaction.commit() } } ``` **WordPoolItem** ``` Swift public static func create(clue clue: String, answer: String, wordPool: WordPool) -> WordPoolItem { var wordPoolItem: WordPoolItem! CoreStore.beginSynchronous { transaction in let transactionItem = transaction.create(Into(WordPoolItem)) transactionItem.clue = clue transactionItem.answer = answer transactionItem.wordPool = transaction.fetchExisting(wordPool)! transaction.commit() wordPoolItem = CoreStore.fetchExisting(transactionItem) } return wordPoolItem } public func update(clue clue: String, answer: String) { CoreStore.beginSynchronous { transaction in let editableSelf = transaction.edit(self)! editableSelf.clue = clue editableSelf.answer = answer transaction.commit() } } ``` Seems like I'm still not using this correctly. I thought the issue was that I called fetchExisting after the `beginSynchronous` closure was closed. Also I removed returning anything from `update` as you stated that returning self would be sufficient (and as returning `self` would be redundant, I just removed it completely). What is my misconception?
Author
Owner

@JohnEstropia commented on GitHub (Jan 26, 2016):

First, about your code:

WordPool.create()

    public static func create(name name: String) -> WordPool {
        var wordPool: WordPool!
        CoreStore.beginSynchronous { transaction in
            wordPool = transaction.create(Into(WordPool))
            wordPool.name = name
            wordPool.commit()
        }
        return CoreStore.fetchExisting(wordPool)   
    }

I suggest you only call fetchExisting() on the same queue you'll use the instance on. Use the same pattern for your WordPoolItem.create()

WordPool.update()

    public func update(name name: String) {
        CoreStore.beginSynchronous { transaction in
            let editableSelf = transaction.edit(self)!
            editableSelf.name = name
            transaction.commit()
        }
        self.refreshAsFault()
    }

Your WordPool and WordPoolItem's update() looks fine, but you might need to re-fault the object right after beginSynchronous(). This might also fix your destroy() method:

    public func destroy() {
        var wordPool: WordPool?
        CoreStore.beginSynchronous { transaction in
            wordPool = transaction.edit(self)?.wordPool
            transaction.delete(self)
            transaction.commit()
        }
        wordPool?.refreshAsFault()
    }

About the architecture, it was a conscious decision to avoid adding utility methods that abstract away the separation between DataStack and Transaction operations. I used to use MagicalRecord before, which implements such methods as your WordPool.create(), but I found it's too easy to make mistakes such as mixing objects from different MOCs, or using objects from the wrong threads.

Since you want to bypass transactions (you're just doing things synchronously, which will cause you deadlocks at some point), I suggest you just use a global UnsafeDataTransaction and do everything from there. I'll think about letting UnsafeDataTransactions create their own ListMonitors and ObjectMonitors.

@JohnEstropia commented on GitHub (Jan 26, 2016): First, about your code: _WordPool.create()_ ``` swift public static func create(name name: String) -> WordPool { var wordPool: WordPool! CoreStore.beginSynchronous { transaction in wordPool = transaction.create(Into(WordPool)) wordPool.name = name wordPool.commit() } return CoreStore.fetchExisting(wordPool) } ``` I suggest you only call `fetchExisting()` on the same queue you'll use the instance on. Use the same pattern for your WordPoolItem.create() _WordPool.update()_ ``` swift public func update(name name: String) { CoreStore.beginSynchronous { transaction in let editableSelf = transaction.edit(self)! editableSelf.name = name transaction.commit() } self.refreshAsFault() } ``` Your WordPool and WordPoolItem's `update()` looks fine, but you might need to re-fault the object right after `beginSynchronous()`. This might also fix your `destroy()` method: ``` swift public func destroy() { var wordPool: WordPool? CoreStore.beginSynchronous { transaction in wordPool = transaction.edit(self)?.wordPool transaction.delete(self) transaction.commit() } wordPool?.refreshAsFault() } ``` About the architecture, it was a conscious decision to avoid adding utility methods that _abstract_ away the separation between DataStack and Transaction operations. I used to use MagicalRecord before, which implements such methods as your `WordPool.create()`, but I found it's too easy to make mistakes such as mixing objects from different MOCs, or using objects from the wrong threads. Since you want to bypass transactions (you're just doing things synchronously, which will cause you deadlocks at some point), I suggest you just use a global `UnsafeDataTransaction` and do everything from there. I'll think about letting `UnsafeDataTransaction`s create their own `ListMonitor`s and `ObjectMonitor`s.
Author
Owner

@JohnEstropia commented on GitHub (Feb 2, 2016):

I'm closing this as it's more of an architecture problem and not a bug in CoreStore (the unit test includes object deletion). If you have any other questions feel free to message anytime :)

@JohnEstropia commented on GitHub (Feb 2, 2016): I'm closing this as it's more of an architecture problem and not a bug in CoreStore (the unit test includes object deletion). If you have any other questions feel free to message anytime :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/CoreStore#31