A fetched property is essentially a stored NSFetchRequest attached to a specific entity. Instead of being backed by a physical foreign key in the database like a standard relationship, it is evaluated dynamically at runtime whenever it is accessed.
Conceptually, you can think of it as a "soft link" or a lazy, read-only relationship. It is always represented as an NSArray (unlike standard To-Many relationships, which are NSSets).
How they differ from standard relationships
- No Schema Impact: They require no foreign keys, no join tables, and no SQLite triggers. They exist entirely in the model graph and at runtime.
- No Inverse: Because they are just dynamic queries, they cannot have an inverse relationship.
- Read-Only: You cannot add or remove objects directly from a fetched property array. To change the results, you must modify the underlying objects so they either match or fail the predicate, and then refresh the property.
- Cross-Store Linking (Legacy): Historically, CoreData allowed fetched properties to query across different persistent stores (e.g., an XML store to a SQLite store), which is impossible with standard relationships.
- Because fetched properties execute a discrete
NSFetchRequest against the store, they cannot be pre-fetched using relationshipKeyPathsForPrefetching.
- Fetched properties are completely ignored during
-[NSManagedObject validateForUpdate:] or -[NSManagedObject validateForInsert:]. Because they are read-only and don't persist foreign keys, the context doesn't care about their state when saving.
How it works at runtime
When a user accesses a fetched property (e.g., NSArray *results = myObject.recentInvoices;), the NSManagedObject intercepts the call via Key-Value Coding or dynamic method resolution.
The framework then performs the following steps:
- Retrieves the Request: It looks up the
NSFetchedPropertyDescription in the model to get the underlying NSFetchRequest.
- Variable Substitution: Fetched properties use a special variable substitution in their predicates. The framework injects the current object into the predicate using the
$FETCH_SOURCE variable. (e.g., A predicate of customerId == $FETCH_SOURCE.id becomes customerId == 123).
- Execution: It executes the fetch request against the object's
NSManagedObjectContext.
- Caching: CoreData caches the resulting
NSArray. Subsequent accesses will return the cached array without hitting the database again, unless the object is turned back into a fault or the context is explicitly refreshed.
Implementation Strategy
To implement this without touching the SQLite store, need to handle the lazy-loading at the NSManagedObject level.
NSFetchedPropertyDescription: new subclass of NSPropertyDescription. It needs an NSFetchRequest property.
- Dynamic Access: In the
NSManagedObject implementation of valueForKey:
- Check if the requested key corresponds to an
NSFetchedPropertyDescription.
- If it does, check the internal value of the property to see if it has already been evaluated.
- If cached, return it.
- Evaluation: If not cached:
- Copy the
NSFetchRequest from the description.
- Use
[NSPredicate predicateWithSubstitutionVariables:@{@"FETCH_SOURCE": self}] to bind the predicate.
- Call
[self.managedObjectContext executeFetchRequest:error:].
- Store the resulting array as the internal property value and return it.
- Cache Invalidation: When
[self refreshObject:mergeChanges:] is called, clear the property value, so it re-evaluates on the next access.
A fetched property is essentially a stored
NSFetchRequestattached to a specific entity. Instead of being backed by a physical foreign key in the database like a standard relationship, it is evaluated dynamically at runtime whenever it is accessed.Conceptually, you can think of it as a "soft link" or a lazy, read-only relationship. It is always represented as an NSArray (unlike standard To-Many relationships, which are NSSets).
How they differ from standard relationships
NSFetchRequestagainst the store, they cannot be pre-fetched usingrelationshipKeyPathsForPrefetching.-[NSManagedObject validateForUpdate:]or-[NSManagedObject validateForInsert:]. Because they are read-only and don't persist foreign keys, the context doesn't care about their state when saving.How it works at runtime
When a user accesses a fetched property (e.g.,
NSArray *results = myObject.recentInvoices;), theNSManagedObjectintercepts the call via Key-Value Coding or dynamic method resolution.The framework then performs the following steps:
NSFetchedPropertyDescriptionin the model to get the underlyingNSFetchRequest.$FETCH_SOURCEvariable. (e.g., A predicate ofcustomerId == $FETCH_SOURCE.idbecomescustomerId == 123).NSManagedObjectContext.NSArray. Subsequent accesses will return the cached array without hitting the database again, unless the object is turned back into a fault or the context is explicitly refreshed.Implementation Strategy
To implement this without touching the SQLite store, need to handle the lazy-loading at the
NSManagedObjectlevel.NSFetchedPropertyDescription: new subclass ofNSPropertyDescription. It needs anNSFetchRequestproperty.NSManagedObjectimplementation ofvalueForKey:NSFetchedPropertyDescription.NSFetchRequestfrom the description.[NSPredicate predicateWithSubstitutionVariables:@{@"FETCH_SOURCE": self}]to bind the predicate.[self.managedObjectContext executeFetchRequest:error:].[self refreshObject:mergeChanges:]is called, clear the property value, so it re-evaluates on the next access.