skip to Main Content

Currently, I have a note application, which I am using the following CoreData structure.

extension Note {

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

    @NSManaged public var heavy_body: String?
    @NSManaged public var lite_title: String?
    @NSManaged public var uuid: UUID?

}

Only fetching lite_title column to preserve memory usage

When displaying hundred to thousands of Notes simultaneously in a UICollectionView, I only need to display lite_title. This is to minimised memory usage. Here’s the code to achieve such objective.

The following code, will only load all Notes’ lite_title into memory, but NOT heavy_body.

private lazy var fetchedResultsController: NSFetchedResultsController<Note> = {
    
    let fetchRequest: NSFetchRequest<Note> = Note.fetchRequest()
    
    // We will NOT fetch "heavy_body" explicitly during app start, because it is a heavy resource.
    fetchRequest.propertiesToFetch = [
        "lite_title"
    ]
    
    fetchRequest.sortDescriptors = [
        NSSortDescriptor(key: "lite_title", ascending: false)
    ]
    
    // Create a fetched results controller and set its fetch request, context, and delegate.
    let controller = NSFetchedResultsController(
        fetchRequest: fetchRequest,
        managedObjectContext: CoreDataStack.INSTANCE.persistentContainer.viewContext,
        sectionNameKeyPath: nil,
        cacheName: nil
    )
    
    controller.delegate = self
    
    return controller
}()

Attempt to fetch heavy_body for 1 note, then hopefully able to discard heavy_body from memory

During 1 note editing, this is what I finish to achieve

  1. Attempt to read lite_title will not trigger SQLite DB operation, because lite_title is already in the memory
  2. Attempt to read heavy_body will trigger SQLite DB operation, because heavy_body is not yet fetched into the memory due to exclusion from fetchRequest.propertiesToFetch
  3. In order to remove heavy_body from the memory, call note.managedObjectContext?.refresh(note, mergeChanges: false) to turn it into fault. Hopefully couple with fetchRequest.propertiesToFetch information, CoreData is smart enough to figure out, it only need to "fault" heavy_body, but leave lite_title untouched.

We write the following code in order to achieve the above goals

@IBAction func readData(_ sender: Any) {
    guard let sections = self.fetchedResultsController.sections else { return }
    
    guard let note = sections[0].objects?[0] as? Note else { return }
    
    if note.isFault {
        print(">>>> This is a fault.n")
    } else {
        print(">>>> This is NOT a fault.n")
    }
    
    print(">>>> Read lite_titlen")
    
    let lite_title = note.lite_title
    
    print(">>>> After accessing lite_title, isFault is (note.isFault)n")
    
    print(">>>> Read heavy_bodyn")
    
    let heavy_body = note.heavy_body
    
    print(">>>> After accessing heavy_body, isFault is (note.isFault)n")
    
    // Move the object back to fault.
    note.managedObjectContext?.refresh(note, mergeChanges: false)
    
    print(">>>> After moving object back to fault, isFault is (note.isFault)n")
}

1st Attempt seems to work as expected

Our 1st execution seems to work as expected. We get the following logging

>>>> This is a fault.

>>>> Read lite_title

>>>> After accessing lite_title, isFault is true

>>>> Read heavy_body

CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZHEAVY_BODY, t0.ZLITE_TITLE, t0.ZUUID FROM ZNOTE t0 WHERE  t0.Z_PK = ? 
CoreData: details: SQLite bind[0] = (int64)1
CoreData: annotation: sql connection fetch time: 0.0003s
CoreData: annotation: fetch using NSSQLiteStatement <0x600002151040> on entity 'Note' with sql text 'SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZHEAVY_BODY, t0.ZLITE_TITLE, t0.ZUUID FROM ZNOTE t0 WHERE  t0.Z_PK = ? ' returned 1 rows
CoreData: annotation:  with values: (
    "<NSSQLRow: 0x600000c00b40>{Note 1-1-5 heavy_body="the body, which is a heavy resource" lite_title="the title" uuid=F80CE382-0345-49A0-AD34-2BAF317AB0C0 and to-manys=0x0}"
)
CoreData: annotation: total fetch execution time: 0.0006s for 1 rows.
CoreData: annotation: fault fulfilled from database for : 0xbee59927ca0b4c5b <x-coredata://DBF3953E-6ABA-4542-9F79-89822F537A93/Note/p1> with row values: <NSSQLRow: 0x600000c00b40>{Note 1-1-5 heavy_body="the body, which is a heavy resource" lite_title="the title" uuid=F80CE382-0345-49A0-AD34-2BAF317AB0C0 and to-manys=0x0}
>>>> After accessing heavy_body, isFault is false

>>>> After moving object back to fault, isFault is true

Seems to achieve our goal

  1. Read lite_title will not trigger SQLite DB operation.
  2. Read heavy_body will trigger SQLite DB operation.

However, we aren’t sure after note.managedObjectContext?.refresh(note, mergeChanges: false), is heavy_body discarded from the memory? Even though isFault is returning true, we can only confirm such by running the same function for the 2nd time


2nd Attempt doesn’t work

We run the same function for the 2nd time. This is our logging

>>>> This is a fault.

>>>> Read lite_title

>>>> After accessing lite_title, isFault is false

>>>> Read heavy_body

>>>> After accessing heavy_body, isFault is false

>>>> After moving object back to fault, isFault is true

Although isFault is true, but no SQLite DB operation observed, when we read heavy_body. It looks like, heavy_body is still staying in the memory cache of CoreData!


We would like to remove heavy_body from the memory, and leave lite_title untouched for this case.

Or, even if lite_title is removed from memory, when accessing note.lite_title and triggering fault firing, only lite_title should be SQLite fetched (As stated in propertiesToFetch, we onyl want lite_title to be fetched). heavy_body should NOT be SQLite fetched along the side.

May I know, how I can achieve so?

The complete sample code to demonstrate the issue is at https://github.com/yccheok/faulting-example

Reference: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/FaultingandUniquing.html (Turning object back to fault)


Update regarding turning off caching mechanism in CoreData

Based on suggestion from @Eugene Dudnyk, I was able to turn off the caching mechanism of CoreData by using

container.viewContext.stalenessInterval = 0.1

With such code, I am able to observe SQLite fetch in 2nd attempt.

>>>> This is a fault.

>>>> Read lite_title

CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZHEAVY_BODY, t0.ZLITE_TITLE, t0.ZUUID FROM ZNOTE t0 WHERE  t0.Z_PK = ? 
CoreData: details: SQLite bind[0] = (int64)1
CoreData: annotation: sql connection fetch time: 0.0002s
CoreData: annotation: fetch using NSSQLiteStatement <0x600002b80320> on entity 'Note' with sql text 'SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZHEAVY_BODY, t0.ZLITE_TITLE, t0.ZUUID FROM ZNOTE t0 WHERE  t0.Z_PK = ? ' returned 1 rows
CoreData: annotation:  with values: (
    "<NSSQLRow: 0x6000006991a0>{Note 1-1-6 heavy_body="the body, which is a heavy resource" lite_title="the title" uuid=F80CE382-0345-49A0-AD34-2BAF317AB0C0 and to-manys=0x0}"
)
CoreData: annotation: total fetch execution time: 0.0008s for 1 rows.
CoreData: annotation: fault fulfilled from database for : 0x9aff7ac6eceb6a90 <x-coredata://DBF3953E-6ABA-4542-9F79-89822F537A93/Note/p1> with row values: <NSSQLRow: 0x6000006991a0>{Note 1-1-6 heavy_body="the body, which is a heavy resource" lite_title="the title" uuid=F80CE382-0345-49A0-AD34-2BAF317AB0C0 and to-manys=0x0}
>>>> After accessing lite_title, isFault is false

>>>> Read heavy_body

>>>> After accessing heavy_body, isFault is false

>>>> After moving object back to fault, isFault is true

However, there is still a pitfall.

When reading lite_title, I was expecting SQLite data fetching will respect fetchRequest.propertiesToFetch, by only fetching lite_title.

However, it seems that SQLite data is fetching the entire Note object, by fetching heavy_body as well.

How can I avoid from fetching heavy_body, if I am only interested in lite_title?

Thanks.

2

Answers


  1. CoreData has underlying caching. ManagedObjectContext has a property stalenessInterval to control for how long the cached informtation should be considered fresh. You can read more about it here.

    The default value of the property defines infinite staleness.

    To test that it makes a difference, add to CoreDataStack.swift:30 this line:

    container.viewContext.stalenessInterval = 0.1
    
    Login or Signup to reply.
  2. It seems like all of this additional work can be avoided if Note stores the heavy_body text into a file separately and it needs to know that filePath only.

    Whenever app needs to load the full heavy_body, (say opening a Note details screen), read/load the contents of the file located at Note.filePath.

    As soon as you are done dealing with the heavy_body, move away from the Note details screen and system takes care of cleaning that up automatically.


    In case you want to show some metadata about the heavy_body in the UICollectionViewCell itself, (say size/length), you could store additional metadata on the Note object and keep it in sync with heavy_body data by setting/updating appropriate values in the willSave() callback.


    CoreData manages it’s caching for valid reasons and fighting it to make it work for this case might create more bookkeeping work than necessary.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search