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 getter called
- _grid allocated
- self.pieces accessed
- fault fires
- awake called
_grid = nil
- return to grid getter
[[_grid objectAtIndex:...
access nil value, crash or at least no-op- grid getter returns nil
- crash or incorrect behavior when boardContents is expected to contain a value
On 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?
- Doesn't respond to selector, crash
- Runs my code, but my instance variables don't exist, who knows what happens when it does data++
- data exists, just modeledProperty doesn't exist because it's 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?
- I can't even call the grid getter method on a fault
- I can call it but I can't use _grid the way I want to
It seems if I understand what you're saying and it's true, the custom subclasses of NSManagedObject are very limited.
- They can't have any instance methods that aren't modeled property getters or setters, because the object can't be guaranteed to exist in a useable state when they are called. (Exception: instance methods that are just helper methods for property accessors would be fine.)
- They can't have any instance variables for any useful purpose other than temporary caches of computed values, because those instance variables could be erased at any moment. I know they won't be persisted on disk, but I thought they would at least be persisted as long as I retained the object in memory.
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?