2

In my GitHub project, I'm using Codeable with CoreData so I don't need a mediating struct to convert data. I get the data from file, and dump them in CoreData. However, whenever I run my project, the number of items I'm consuming from the original file is different from the total number of items I'm fetching from CoreData. And the number of items fetched are different for every build.

enter image description here

enter image description here

I want to save 1000 items in my Core Data. However, when I build my project, at least 1 out of 5 times, I see that I have fewer than 1000 items. I checked the data I'm getting from my back end, and I'm also checking the data that I'm fetching from Core Data, and the numbers don't match.

On my viewDidLoad, I call retrieveData()

        guard let url = URL(string: <URL here>)
            else { return }
        
        let task = URLSession.shared.dataTask(
             with: url as URL,
             completionHandler: { [weak self] (data, response, error) in
                 guard let dataResponse = data, error == nil else {
                     print("error!!!")
                     return
                 }
                self?.decodeAndSaveAndFetch(dataResponse: dataResponse)
             }
         )
         task.resume()
    }

This is my decodeAndSaveAndFetch() where I decode the json from my service call, save the items in my CoreData and fetch it to display:

    func decodeAndSaveAndFetch(dataResponse: Data) {
        let decoder = JSONDecoder()
        guard let codingUserInfoKeyContext = CodingUserInfoKey.context else { return }
        decoder.userInfo[codingUserInfoKeyContext] = container?.viewContext
        
        container?.performBackgroundTask { [weak self] context in
            do {
                let filePath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
                guard let fileName = filePath?.appendingPathComponent("COREDATA.txt") else { return }
                
                try dataResponse.write(to: fileName)
                _ = try decoder.decode([Ingredient].self, from: dataResponse)
                
                try context.save()
                
                let request: NSFetchRequest<Ingredient> = Ingredient.fetchRequest()
                do {
                    try print(self?.container?.viewContext.count(for: request))
                    self?.data = try self?.container?.viewContext.fetch(request)
                    DispatchQueue.main.async { [weak self] in
                        self?.tableView.reloadData()
                    }
                } catch {
                    print("fetch failed")
                }
            } catch {
                print("decode and save failed")
            }
        }
    }

In the COREDATA.txt, I always get 1000 items, but when I print the count for the request, I sometimes get fewer than 1000 items.

Here is my Ingredient class with the fetch method:

extension CodingUserInfoKey {
    static let context = CodingUserInfoKey(rawValue: "context")
}

@objc(Ingredient)
public class Ingredient: NSManagedObject, Codable {
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        do {
            try container.encode(id ?? "blank", forKey: .id)
        } catch {
            print("error")
        }
    }
    
    required convenience public init(from decoder: Decoder) throws {
        // return the context from the decoder userinfo dictionary
        guard let contextUserInfoKey = CodingUserInfoKey.context,
        let managedObjectContext = decoder.userInfo[contextUserInfoKey] as? NSManagedObjectContext,
        let entity = NSEntityDescription.entity(forEntityName: "Ingredient", in: managedObjectContext)
        else {
            fatalError("decode failure")
        }
        // Super init of the NSManagedObject
        self.init(entity: entity, insertInto: managedObjectContext)
        let values = try decoder.container(keyedBy: CodingKeys.self)
        do {
            id = try values.decode(String.self, forKey: .id)
            desc = try values.decode(String.self, forKey: .desc)
            ingredientType = try values.decode(Set<IngredientType>.self, forKey: .ingredientType)
        } catch {
            print ("error")
        }
    }
    
    enum CodingKeys: String, CodingKey {
        case id = "id"
        case desc = "desc"
        case ingredientType = "ingredientType"
    }
}

extension Ingredient {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Ingredient> {
        return NSFetchRequest<Ingredient>(entityName: "Ingredient")
    }

    @NSManaged public var id: String?
    @NSManaged public var desc: String?
    @NSManaged public var ingredientType: Set<IngredientType>?

}

Here is the relationship between Ingredient and IngredientType: enter image description here

Please advise. Thank you!

EDIT: Link to GitHub project To reproduce: build and run in XCode - observe the counts.

  • 1
    Unrelated but never print pointless literal strings in `catch` blocks. Print the actual `error`. – vadian Jul 30 '20 at 18:44
  • Can you confirm what relationship you are using in your SQLite attributes? Sometimes if you chose many to many instead of one to one in your relations or something like that then this can happen. – Parth Jul 30 '20 at 19:05
  • @ParthTamane, That's correct, Ingredient has a many-to-many relationship with IngredientType. Ingredients can have many types, and each type belongs to multiple Ingredients. How can this be resolved? – Isiah Malit Jul 30 '20 at 19:50
  • Try making it one to many. Since `Ingredient` is your base attribute. I think that should work. – Parth Jul 30 '20 at 19:52
  • @ParthTamane Unfortunately, it didn't work. I even removed the inverse relationship, but the issue is still happening intermittently. – Isiah Malit Jul 30 '20 at 20:09
  • Try changing the direction to many `Ingredient ` to one `IngredientType `? – Parth Jul 30 '20 at 20:10
  • What is the item you are trying to store? – Parth Jul 30 '20 at 20:11
  • @ParthTamane, I updated the original post with the image depicting the initial data structure I'm storing in CoreData. – Isiah Malit Jul 30 '20 at 20:32
  • Did you try changing the direction to many `Ingredient` to one `IngredientType`. I think it should definitely not be 2 way. – Parth Jul 30 '20 at 20:35
  • @ParthTamane I'll try that. Thanks! The concept is that I can store 1000 ingredients with different types. For example, in a handsoap, there is an ingredient Benzoic Acid. Benzoic Acid is both preservative and anti-corrosive agent. The user can then filter by types, and it will list all ingredients with those types. In my UITableView, I will display each ingredient and the different types it has. Hence in my opinion, it needs to be at least Ingredient <--->> IngredientType. I tried this, and it didn't work. – Isiah Malit Jul 30 '20 at 20:45
  • Yeah that's what I thought you were trying to do. I'm pretty sure the problem is with the type of assocation. This has happened with me to. I think the flipping to many to one should fix it. – Parth Jul 30 '20 at 20:47
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/218990/discussion-between-isiah-malit-and-parth-tamane). – Isiah Malit Jul 31 '20 at 14:49

1 Answers1

0

Try changing the direction to many Ingredient to one IngredientType. I think it should definitely not be many-to-many. I faced a similar issue when making a list of to-dos. This method of association allows one item type from a fixed set of item types to be associated with multiple items.

Check out this StackOverflow post for how you can have a set of item types: Array of Objects in Core Data Entity?. They propose using To Many for toItemType relation.

core data model

This is how my relationship is setup:

The items are accessed like this:

func configureCell(item: Item) {
        
    title.text = item.title
    price.text = "$\(item.price)"
    details.text = item.details
    type.text = item.toItemType?.type
    thumb.image = item.toImage?.image as? UIImage
}
Parth
  • 2,682
  • 1
  • 20
  • 39
  • If your TODO item has multiple types, how are you able to display that data? For example, `TODO: Jogging with friends` is of types `SOCIAL` and `EXERCISE`. – Isiah Malit Jul 30 '20 at 21:19
  • @IsiahMalit Good question. I think you will have to set a list of items. What this relation does is allow multiple to-dos to be associated with the same fixed type of item eg `SOCIAL`. See how it's accessed above. – Parth Jul 30 '20 at 21:26
  • Hi Parth! I read the link you provided. It's not different from what I have. In my initial post, I posted the Ingredient class that has `@NSManaged public var ingredientType: Set?`. I already have a `to-many` for `Type`. Yours is the opposite of mine and the provided link which doesn't really work for my model, unfortunately. I tried both `Ingredient <<--->> IngredientType` and `Ingredient <-->> IngredientType`, but neither worked. `Ingredient <<---> IngredientType` doesn't work because for each ingredient, there are multiple ingredient types. – Isiah Malit Jul 31 '20 at 01:40
  • I don't think you need to manually define a set but I could be wrong. According to that link as long as you have many to one relationship it should let you add multiple, right? It says that you need to rebuild before you get that option. Also, if your code is on GitHub, then I can clone and take a look at it over the weekend. Might be quicker that way. – Parth Jul 31 '20 at 02:01
  • Hi Parth! Here is the link to GitHub: https://github.com/imalit/TestNestedCoreData. I'm using Codeable with CoreData so I don't need a mediating struct to convert data. I originally am using data from Firebase, but since I want to keep my data private, I created a json file. The result is the same: I'm expecting 1000 items, but getting less. – Isiah Malit Jul 31 '20 at 13:58
  • Nice! Thanks. Please update your original question with this link and also add steps to reproduce the issue. – Parth Jul 31 '20 at 14:15
  • I've updated the original question with the link to my GitHub project (MCVE) and steps to reproduce. – Isiah Malit Jul 31 '20 at 14:49