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
- Attempt to read
lite_title
will not trigger SQLite DB operation, becauselite_title
is already in the memory - Attempt to read
heavy_body
will trigger SQLite DB operation, becauseheavy_body
is not yet fetched into the memory due to exclusion fromfetchRequest.propertiesToFetch
- In order to remove
heavy_body
from the memory, callnote.managedObjectContext?.refresh(note, mergeChanges: false)
to turn it into fault. Hopefully couple withfetchRequest.propertiesToFetch
information, CoreData is smart enough to figure out, it only need to "fault"heavy_body
, but leavelite_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
- Read
lite_title
will not trigger SQLite DB operation. - 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
CoreData has underlying caching.
ManagedObjectContext
has a propertystalenessInterval
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:It seems like all of this additional work can be avoided if
Note
stores theheavy_body
text into a file separately and it needs to know thatfilePath
only.Whenever app needs to load the full
heavy_body
, (say opening aNote
details screen), read/load the contents of the file located atNote.filePath
.As soon as you are done dealing with the
heavy_body
, move away from theNote
details screen and system takes care of cleaning that up automatically.In case you want to show some metadata about the
heavy_body
in theUICollectionViewCell
itself, (say size/length), you could store additional metadata on theNote
object and keep it in sync withheavy_body
data by setting/updating appropriate values in thewillSave()
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.