0

I have close to 7K items stored in a relation called Verse. I have another relation called Translation that needs to load 7K related items with a single call from a JSON file.

Verse has a one to many relationship to Translation

Here is my code:

let container = getContainer()
    container.performBackgroundTask() { (context) in
        autoreleasepool {


        for row in translations{
            let t = Translation(context: context)
            t.text = (row["text"]! as? String)!
            t.lang = (row["lang"]! as? String)!
            t.contentType = "Verse"
            t.verse = VerseDao.findById(row["verse_id"] as! Int16, context: context) 
// this needs to make a call to the database to retrieve the approparite Verse instance. 
        }

        }
        do {
           try context.save()
        } catch {
            fatalError("Failure to save context: \(error)")
        }
        context.reset()
    }

Code for the findById method.

static func findById(_ id: Int16, context: NSManagedObjectContext) -> Verse{

    let fetchRequest: NSFetchRequest<Verse>
    fetchRequest = Verse.fetchRequest()
    fetchRequest.predicate = NSPredicate(format: "verseId == %@", id)
    fetchRequest.includesPropertyValues = false
    fetchRequest.fetchLimit = 1

    do {
        let results =
            try context.fetch(fetchRequest)
        return results[0]
    } catch let error as NSError {
        print("Could not fetch \(error), \(error.userInfo)")
        return Verse()

    }
}

This works fine until I add the VerseDao.findById, which makes the whole process really slow because it has to make a request for each object to the Coredata database.

I did everything I could by limiting the number of fetched properties and using NSFetchedResultsController for data fetching but no luck.

I wonder if there's any way to insert child records in a more efficient way? Thanks.

Noah
  • 71
  • 7

1 Answers1

0

Assuming your store type is persistent store type is sqlite (NSSQLiteStoreType):

The first thing you should check is whether you have an Core Data fetch index on the Verse objects verseId property. See this stack overflow answer for some introductory links on fetch indexes. Without that, the fetch in your VerseDao.findById function may be scanning the whole database table every time. To see if your index is working properly you may inspect the SQL queries generated by adding -com.apple.CoreData.SQLDebug 1 to the launch arguments in your Xcode scheme.

Other improvements:

  • Use NSManagedObjectContext.fetch or NSFetchRequest.execute (equivalent) instead of NSFetchedResultsController. The NSFetchedResultsController is typically used to bind results to a UI. In this case using it just adds overhead.
  • Don't set fetchRequest.propertiesToFetch, instead set fetchRequest.includesPropertyValues = false. This will avoid fetching the Verse object property values which you don't need to establish the relation to the Translation object.
  • Don't specify a sortDescriptor on the fetch request, this just complicates the query
guru_meditator
  • 549
  • 6
  • 11
  • Thanks for the answer. I already have indexes and made the other changes you suggested. It still seems to be slow. I wonder if there's a way to create the Verse object with verseID without making a call to the coredata database? I just need a reference. In a normal RDBMs, it would be just inserting the associated primary key in the second table. – Noah Jul 23 '18 at 09:18
  • Do you have any timings on how slow it is so we can judge if normal or not? You can't create a relation without a lookup in the way you mention, Core Data is an object graph, not a relational database. Do you need to update your data (for example from a web service) or do you only do this import from the json files once on the first app launch? If only once, you can consider shipping a prebuilt store as an application resource, see https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/FrequentlyAskedQuestions.html#//apple_ref/doc/uid/TP40001075-CH29-SW1 – guru_meditator Jul 24 '18 at 02:46
  • It takes 1 minute and 40 seconds on an iPhone 7. For the second part, I need to download this from a web service and is not a one time download. – Noah Jul 24 '18 at 09:34
  • Sounds slower than expected for 7k lookups and inserts. Since you say above that most of the time is spent in the Verse fetch requests, I suggest you do a EXPLAIN QUERY PLAN in sqlite for that query (you can see the query with -com.apple.CoreData.SQLDebug 1). This article explains how to do the EXPLAIN: https://www.objc.io/issues/4-core-data/core-data-fetch-requests/ – guru_meditator Jul 26 '18 at 07:48