I read the documentation on transient properties but I can't really understand their purpose. Can someone tell me the difference between having and not having a transient property if I have a custom subclass of NSManagedObject like this?
@interface Board : NSManagedObject
{
NSMutableArray *_grid;
}
// Core Data to-many relationship
@property (nonatomic, retain) NSSet *pieces;
@property (nonatomic, readonly) NSArray *grid;
-(void)awake;
-(void)movePiece:(PieceState *)piece to_x:(int)x y:(int)y;
@end
@implementation Board
@dynamic pieces;
-(void)awakeFromInsert {
[super awakeFromInsert];
[self awake];
}
-(void)awakeFromFetch {
[super awakeFromFetch];
[self awake];
}
-(void)awake {
_grid = nil; // probably not necessary
}
-(NSArray *)grid {
if (!_grid) {
_grid = [[NSMutableArray alloc] initWithCapacity:10];
for (int i = 0; i < 10; i++) {
NSMutableArray *column = [[NSMutableArray alloc] initWithCapacity:10];
[_grid addObject:column];
for (int j = 0; j < 10; j++)
[column addObject:[NSNull null]];
[column release];
}
for (PieceState *piece in self.pieces)
if (piece.x >= 0 && piece.y >= 0)
[[_grid objectAtIndex:piece.x] replaceObjectAtIndex:piece.y withObject:piece];
}
return _grid;
}
-(void)movePiece:(PieceState *)piece to_x:(int)x y:(int)y {
if (x >= 0 && y >= 0) {
NSObject *capturedPieceObject = [[self.grid objectAtIndex:x] objectAtIndex:y];
if ([capturedPieceObject isKindOfClass:[PieceState class]]) {
PieceState *capturedPiece = (PieceState *)capturedPieceObject;
[self removePiecesObject:capturedPiece];
[[self managedObjectContext] deleteObject:capturedPiece];
capturedPiece = nil;
}
}
if (_grid) {
if (piece.x >= 0 && piece.y >= 0)
[[_grid objectAtIndex:piece.x] replaceObjectAtIndex:piece.y withObject:[NSNull null]];
if (x >= 0 && y >= 0)
[[_grid objectAtIndex:x] replaceObjectAtIndex:y withObject:piece];
}
[piece setX:x];
[piece setY:y];
}
- (void)didTurnIntoFault {
[_grid release];
_grid = nil;
[super didTurnIntoFault];
}
@end
So pieces and grid present two ways to access the same data. pieces is the actual Core Data relationship property, and is a dense list of all the pieces. grid is a way to find the contents of a particular space on the board addressed by (x, y) coordinates. grid is built lazily and updated (as long as it exists) when a piece changes location.
I'm not declaring grid as a transient property and everything is working fine. I'm just wondering if there is some unusual condition that could arise that would cause a bug if I don't declare a transient property.
I think I read transient properties are needed to get proper undo behavior if you're doing a derived property like this. I'm not using undo, and in any case I don't see how it could work in this case. If a piece move is undone, the undo manager can assign the old value of _grid back to it (maybe assuming I didn't make it readonly), but the old value is the same as the new value. It is a pointer to the same NSMutableArray instance, only the contents have changed. Anyway I don't use undo.
So do I get any benefit if I declare grid to be a transient property?
Additional question. What if I have code like this:
Board *board = someOtherManagedObject.board;
NSObject *boardContents = [[board.grid objectAtIndex:5] objectAtIndex:5];
Is it possible board is a fault after accessing someOtherManagedObject.board? I'm having trouble understanding faulting too. I think in that case my code would crash. I noticed awake sets _grid to nil. I think the sequence would be like this:
_grid = nil
[[_grid objectAtIndex:...
access nil value, crash or at least no-opOn the other hand, maybe if I declare grid to be a transient property, then the fault fires before my grid getter is called?
From TechZen:
Faults are placeholder objects that define an object graph with relationships but don't load attribute values. They will log as instances of either an NSManagedObject or of a private _NSFault... class.
Because unmodeled properties are only attributes of the custom NSManagedObject subclass and not the entity, the fault objects know nothing about them. Fault objects are initialized from the data model so that all the keys they respond to must be in the data model. This means faults will not reliably respond to request for unmodeled properties.
Wait what? I'm starting to realize my objects can be faults at any time but are you telling me they might not even be instances of my class!? Or if you use a custom subclass are they guaranteed to be the sort of faults that are instances of NSManagedObject (specifically my subclass)?
If they aren't instances of the custom class then what happens with something like this:
@interface Foo : NSManagedObject {
int data;
}
@property (nonatomic, retain) NSString *modeledProperty;
-(void)doSomething;
@end
@implementation Foo
@dynamic modeledProperty;
-(void)doSomething {
data++;
}
@end
What happens if I call doSomething on a fault?
Transient properties fix this problem. The transient property provides a key that the context can observe without saving. If you have a fault, sending it a key-value message for a transient property will trigger the context to "fire" the fault and load the complete managed object.
Okay, but what if I have an instance method that's not a property accessor, like doSomething above? How do I make sure I have a real object before I call it? Or can I call it, and first thing in the method body make sure I have a real object (for example by accessing a modeled property)?
In your case, you want to use a transient property for grid if the value of grid depends on the values of any modeled properties of the Board class. That is the only way to guarantee that grid will always be populated when you access it.
I thought if it depended on the values of modeled properties, then it would fire the fault when it depended on them, i.e. the line for (PieceState *piece in self.pieces)
fires the fault because it accesses self.pieces, which is a modeled property. But you are telling me which?
It seems if I understand what you're saying and it's true, the custom subclasses of NSManagedObject are very limited.
If that's the case then are you not intended to put application logic in your custom NSManagedObject subclasses? Should application logic reside in other classes that have references to the managed objects, and the managed objects are only dumb objects that you read from and write to (just a little bit smart, with some capabilities to maintain data consistency)? Is the only point of subclassing NSManagedObject to do some "tricks" with non-standard data types?
Transient attributes are properties that you define as part of the model, but that are not saved to the persistent store as part of an entity instance's data. Core Data does track changes you make to transient properties, so they are recorded for undo operations.
The transient flag specifies whether or not a property's value is ignored when an object is saved to a persistent store. Transient properties are not saved to the persistent store, but are still managed for undo, redo, validation, and so on.
When you declare a property as Transformable Core Data converts your custom data type into binary Data when it is saved to the persistent store and converts it back to your custom data type when fetched from the store. It does this through a value transformer.
To implement a Transformable attribute, configure it by setting its type to Transformable and specifying the transformer and custom class name in Data Model Inspector, then register a transformer with code before an app loads its Core Data stack.
The advantage of transient properties comes from the difference between modeled/observed properties and unmodeled/unobserved properties.
The managed object context uses key-value observing (KVO) to monitor modeled properties. Based on the information provided in the data model, it knows what properties must have values, what default, minimum and max values are, when the property is changed and, most importantly, whether the managed object has a key name for a property. All this provides the "managed" part of managed objects.
Modeled properties do not require a custom NSManagedObject subclass but can use a generic NSManagedObject instance initialized to an entity. Accessing a modeled property of a fault (see below) causes the fault to load fully.
The managed object context doesn't observe unmodeled properties and unmodeled properties require a custom NSManagedObject subclass. The unmodeled properties are attributes of the class only and do not show up in the entity and they are never persisted in Core Data. Changes to unmodeled properties go unnoticed by the context.
Faults are placeholder objects that define an object graph with relationships but don't load attribute values. You can think of them as "ghost" objects. They will log as instances of either an NSManagedObject or of a private _NSFault... class. If it is a NSManagedObject the attributes are all empty. When a fault "fires" or is "faulted in" the placeholder object is replaced with a fully populated NSManagedObject instance whose attributes can be read.
Because unmodeled properties are only attributes of the custom NSManagedObject subclass and not the entity, the fault objects know nothing about them. Fault objects are initialized from the data model so that all the keys they respond to must be in the data model. This means faults will not reliably respond to request for unmodeled properties.
Transient properties fix this problem. The transient property provides a key that the context can observe without saving. If you have a fault, sending it a key-value message for a transient property will trigger the context to "fire" the fault and load the complete managed object.
It is important to note that although the data model has a key name for a transient property, the property only has a value when the managed object is fully instantiated and loaded. This means that when you do any fetches, which operate solely in the persistent store, the transient properties will have no values.
In your case, you want to use a transient property for grid
if the value of grid
depends on the values of any modeled properties of the Board
class. That is the only way to guarantee force Core Data to guarantee that grid
will always be populated when you access it.
[Edit: That last is highly theoretical. Using a transient property ensures that Core Data tracks the property such that the accessing the property will cause a fault to fire and provide the data. However, in practice accessing any modeled property will reliably fire the fault and unmodeled methods are always available (see below.)
You can also use:
+[NSManagedObject contextShouldIgnoreUnmodeledPropertyChanges:]
… to force a context to watch unmodeled properties. However, that can cause unanticipated and unmanaged behavior if the unmodeled properties have side effects.
I think that it is good practice to use transient properties whenever possible to make sure everything is covered.]
Okay, but what if I have an instance method that's not a property accessor, like doSomething above? How do I make sure I have a real object before I call it?
I think you're over thinking this and my cumbersome explanation didn't help any.
Core Data manages all these issues for you. I've been using Core Data as long as there has been a Core Data and I have never run into any problems. Core Data wouldn't be much use if you had to constantly stop and check if the objects were faults or not.
For example, I set up a simple model with classes like so:
Alpha:
@class Beta;
@interface Alpha : NSManagedObject {
@private
}
@property (nonatomic, retain) NSNumber * num;
@property (nonatomic, retain) NSString * aString;
@property (nonatomic, retain) NSSet *betas;
-(NSString *) unmodeledMethod;
@end
@interface Alpha (CoreDataGeneratedAccessors)
- (void)addBetasObject:(Beta *)value;
- (void)removeBetasObject:(Beta *)value;
- (void)addBetas:(NSSet *)values;
- (void)removeBetas:(NSSet *)values;
@end
@implementation Alpha
@dynamic num;
@dynamic aString;
@dynamic betas;
-(NSString *) unmodeledMethod{
return @"Alpha class unmodeledMethod return value";
}
@end
Beta:
@class Alpha;
@interface Beta : NSManagedObject {
@private
}
@property (nonatomic, retain) NSNumber * num;
@property (nonatomic, retain) NSSet *alphas;
-(NSString *) unmodeledMethod;
-(NSString *) accessModeledProperty;
@end
@interface Beta (CoreDataGeneratedAccessors)
- (void)addAlphasObject:(Alpha *)value;
- (void)removeAlphasObject:(Alpha *)value;
- (void)addAlphas:(NSSet *)values;
- (void)removeAlphas:(NSSet *)values;
@end
@implementation Beta
@dynamic num;
@dynamic alphas;
-(NSString *) unmodeledMethod{
return [NSString stringWithFormat:@"%@ isFault=%@", self, [self isFault] ? @"YES":@"NO"];
}
-(NSString *) accessModeledProperty{
return [NSString stringWithFormat:@"\n isFault =%@ \n access numValue=%@ \n isFault=%@", [self isFault] ? @"YES":@"NO", self.num,[self isFault] ? @"YES":@"NO"];
}
@end
Then I created an object graph of Alpha
object with a related Beta
object. Then I restarted the app and ran a fetch of all Alpha
objects. Then I logged the following:
id aa=[fetchedObjects objectAtIndex:0];
id bb=[[aa valueForKey:@"betas"] anyObject];
NSLog(@"aa isFault= %@",[aa isFault] ? @"YES":@"NO");
//=> aa isFault= NO
NSLog(@"\naa = %@",aa);
//=> aa = <Alpha: 0x63431b0> (entity: Alpha; id: 0x6342780 <x-coredata://752A19D9-2177-45A9-9722-61A40973B1BC/Alpha/p1> ; data: {
//=> aString = "name 2";
//=> betas = (
//=> "0x63454c0 <x-coredata://752A19D9-2177-45A9-9722-61A40973B1BC/Beta/p7>"
//=> );
//=> // ignore fetchedProperty = "<relationship fault: 0x6153300 'fetchedProperty'>";
//=> num = 0;
//=> })
NSLog(@"\nbb isFault= %@",[bb isFault] ? @"YES":@"NO");
//=> bb isFault= YES
NSLog(@"\nany beta = %@",[[bb class] description]);
//=> any beta = Beta
NSLog(@"\n-[Beta unmodeledMethod] =\n \n %@",[bb unmodeledMethod]);
//=> -[Beta unmodeledMethod] =
//=> <Beta: 0x639de70> (entity: Beta; id: 0x639dbf0 <x-coredata://752A19D9-2177-45A9-9722-61A40973B1BC/Beta/p7> ; ...
//=>...data: <fault>) isFault=YES
NSLog(@"\n-[Beta accessModeledProperty] = \n %@",[bb accessModeledProperty]);
-[Beta accessModeledProperty] =
//=> isFault =NO
//=> access numValue=2
//=> isFault=YES
NSLog(@"\nbb = %@",bb);
//=>bb = <Beta: 0x6029a80> (entity: Beta; id: 0x6029460 <x-coredata://752A19D9-2177-45A9-9722-61A40973B1BC/Beta/p7> ; data: {
//=> alphas = "<relationship fault: 0x60290f0 'alphas'>";
//=> num = 2;
//=>})
Note that:
aa
and bb
are set to the expected class even though I did a generic object assignment. The context ensures that the fetch returns the proper class.bb
's class is Beta
it reports as a fault meaning that the object represents an instance of the Beta
class but that none of it's modeled properties are populated.bb
object responds to the unmodeledMethod
selector even though within the method it still reports as a fault.Beta.num
converts bb
from a fault even before the call is made (the compiler sets it to trigger) but as soon as the access is done it reverts back to a fault.Alpha.betas
the Beta
object has the address of 0x63454c0
whereas bb
has the address of 0x639de70>
while it is a fault. After it converts from a fault and then back again, it's a address is 0x6029a80
. However, the managedObjectID of all three objects is the same.The morals here are:
_NSFault…
classes. From the coders perspective, all these different objects are interchangeable.In short don't worry about unmodeled properties and methods. They should work transparently. It's best practice to use transient properties especially if those properties have side effects with modeled properties. You can force a context to track unmodeled properties but that can cause unnecessary complexity.
If you have doubts, just perform test yourself on faults to ensure that your class works.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With