Transient properties in Core Data have always been a bit of a mystery to me. Here is what the documentation has to say:

Transient properties are properties that you define as part of the model, but which 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.

So they are just like normal properties, except they are not saved to the persistent store. Well, almost...

The documentation doesn't talk much about transient attributes, and doesn't mention transient relationships at all, so I made some experiments. Here is what I found:

  1. Undo and redo behaves just like normal properties.
  2. Modifying a transient property will mark the object as dirty.
  3. Transient properties are cleared when an object turns into a fault, so -[refreshObject:mergeChanges:NO] will clear them.
  4. -[refreshObject:mergeChanges:YES] is funky, see below.

Refreshing Transient Properties

This is what the documentation says about refreshing objects with transient properties using -[refreshObject:mergeChanges:]:

If flag is YES, this method does not affect any transient properties; if flag is NO, transient properties are released.

The behaviour when flag is YES requires a little explanation. This is the procedure:

  1. Core Data makes a copy of all changed properties.
  2. Change processing is disabled.
  3. The equvivalent of -[willChangeValueForKey:] is invoked for all properties.
  4. The object is turned into a fault, and receives a -[didTurnIntoFault] message.
  5. The object is fetched and receives a -[awakeFromFetch] message.
  6. The changed properties are restored from the copy made earlier.
  7. The equvivalent of -[didChangeValueForKey:] is invoked for all properties.
  8. Change processing is enabled.

What are the changed properties? A little experimentation shows that it is the properties whose value differs from the current snapshot (as returned by -[commitedValuesForKeys:]). For persistent properties it means that even though you have 'changed' a property using a set accessor, it won't be consired changed if the new value is identical to the snapshot. (As determined by -[isEqual:]). For transient properties, the snapshot value is nil (since they are not stored), so any non-nil transient property is considered changed.

If a transient property is nil before -[refreshObject:mergeChanges:YES], it will retain the value you give it in -[awakeFromFetch], otherwise it is unaffected. So the mental model to use for transient properties is:

Transient properties behave like persistent properties whose value in the persistent store is always nil.

A weird side-effect of this is that after a save, a non-nil transient property is still 'changed' in the eyes of -[refreshObject:mergeChanges:YES] even though the object is not dirty.

How To Use Transient Properties

So what are transient properties good for? Well, given the mental model above, the obvious use is any property you don't need to be stored, but would like undo support for. The CoreRecipes sample code has an example of that in the SmartGroup entity. You should be aware that changing such a property will mark your object as dirty, but that is the case for any Key-Value Observation compliant key. Calling -[willChangeValueForKey:] and -[didChangeValueForKey:] marks an object as dirty, regardless of the key.

Another use that the documentation seems to recommend is caching for properties that can't be stored. Cross-store relationships and attributes with unsupported types are the typical examples. Before saving, you convert the property into something that can be stored, and write it to a binary 'shadow' attribute.

The document goes on to describe two 'getter' methods and two 'setter' methods.

  • The On-demand Get Accessor will unarchive the shadow attribute the first time it is called, and use the cached value after that.
  • The Pre-calculated Get will unarchive the shadow attribute in -[awakeFromFetch], and store it in the transient attribute.
  • The Immediate-Update Set Accessor will archive the transient attribute every time it is set.
  • The Delayed-Update Set Accessor will archive the transient attribute in -[willSave]

You can mix and match these methods, and they work well as long as you only use one managed object context. If you have multiple contexts in play, you are in all kinds of trouble. The problem is the behaviour of -[refreshObject:mergeChanges:YES]. If you implement accessors as described in 'Non-Standard Attributes', it behaves as above, so all non-nil transient properties are considered changed. This means they don't get updated if they were changed by a different managed object context. Another chapter talks about this problem. The suggested solution is to create a flag, colorIsValid, and set it to NO in -[didTurnIntoFault]. The getter method must check the flag to see if it should unarchive the shadow attribute again. It would essentially be a modified On-demand Get Accessor:

- (NSColor *)color
{
    NSColor *color = nil;
    if (colorIsValid) {
        [self willAccessValueForKey:@"color"];
        color = [self primitiveValueForKey:@"color"];
        [self didAccessValueForKey:@"color"];
    }
    else {
        NSData *colorData = [self valueForKey:@"colorData"];
        if (colorData != nil)
            color = [NSKeyedUnarchiver unarchiveObjectWithData:colorData];
        [self setPrimitiveValue:color forKey:@"color"];
        colorIsValid = YES;
    }
    return color;
}

- (void)didTurnIntoFault
{
    [super didTurnIntoFault];
    colorIsValid = NO;
}

Well, great! Now it behaves just like [refreshObject:mergeChanges:NO], discarding any local changes made to color. OK, so maybe we need another flag, say colorIsChanged, that we set when changing the color. Then we could say something like:

- (void)didTurnIntoFault
{
    [super didTurnIntoFault];
    colorIsValid = colorIsChanged;
}

- (void)didSave
{
    [super didSave];
    colorIsChanged = NO;
}

Sure, but what about [refreshObject:mergeChanges:NO]. That should always restore the attribute from the store. Also, it won't work with undo. We should be able to go: -[setColor:], undo, -[refreshObject:mergeChanges:YES] and still get a fresh color from the store (and colorIsChanged should be NO). There is also a problem with detecting changes the same way as for the persistent properties: 'A property is changed if its value is different from the snapshot'.

A Better Solution

The cause of these problems is that we are using the wrong mind-set. A transient property doesn't work if you think of it as a cache of a persistent value. You should be thinking of it as something whose value is nil in the persistent store. What would that be in the present case? How about the change made to color relative to the persistent store. Suppose we have a magic instance variable, colorSnapshot, that is always set to the value of color in the current snapshot. Then we could implement the getter/setter methods like this:

- (NSColor*)color
{
    [self willAccessValueForKey:@"color"];
    id change = [self primitiveValueForKey:@"color"];
    [self didAccessValueForKey:@"color"];
    if (change!=nil)
        return change;
    else
        return colorSnapshot;
}

- (void)setColor:(NSColor*)value
{
    [self willChangeValueForKey:@"color"];
    [self setPrimitiveValue:value forKey:@"color"];
    [self didChangeValueForKey:@"color"];
}

When the transient attribute is nil, it means that the color value is unchanged. This is the right mental model. The transient attribute always has the value nil in the persistent store.

You have probably noticed a problem with [obj setColor:nil]. Let me get back to that later.

How about the colorSnapshot instance variable? It needs to represent the current snapshot color at all times. The current snapshot changes when the object is fetched and saved, so we set it there:

- (void)awakeFromFetch
{
    [super awakeFromFetch]
    NSData *colorData = [self valueForKey:@"colorData"];
    if (colorData != nil)
        colorSnapshot = [[NSKeyedUnarchiver unarchiveObjectWithData:colorData] retain];
}

- (void)didSave
{
    [super didSave];
    id change = [self primitiveValueForKey:@"color"];
    if (change!=nil) {
        [colorSnapshot release];
        colorSnapshot = [change retain];
        [self setPrimitiveValue:nil forKey:@"color"];
    }
}

- (void)didTurnIntoFault
{
    [super didTurnIntoFault];
    [colorSnapshot release];
    colorSnapshot = nil;
}

Note that we are unarchiving in -[awakeFromFetch], so this is the pre-calculated get pattern. We are not setting any transient attributes, though. They would be overwritten during a refresh anyway.

Saving is pretty simple, but now that we know if the color has been changed, we don't have to archive if it is unchanged. This is actually the best of both worlds when comparing to the immediate-update and delayed-update patterns. We only archive once per save (delayed-update), and we don't archive at all if the value is not changed (immediate-update).

- (void)willSave
{
    id change = [self primitiveValueForKey:@"color"];
    if (change!=nil) {
        [self setPrimitiveValue:[NSKeyedArchiver archivedDataWithRootObject:change]
            forKey:@"colorData"];
    }
    [super willSave];
}

It is also possible to use the on-demand get pattern for loading the snapshot. Just make sure to use -[committedValuesForKeys:] to get colorData, since [valueForKey:@"colorData"] will return the value set by -[willSave] above. If the save failed, that won't be the snapshot value. Note that if you have any KVO observers registered with the NSKeyValueObservingOptionNew option, the get accessor will always be called right after -[awakeFromFetch], and you have gained nothing.

Picking Nits

OK, so we have a problem with storing nil as our color. As he code stands, [obj setColor:nil] acts as a kind of 'revert to saved' function, color gets the value it had in the snapshot, and is no longer considered changed when refreshing. While that may be practical sometimes, it is not how persistent properties work. We want to be able to store nil the same way as any other value. We have to distinguish the two meanings of nil: 1. The value is unchanged, and 2. The value has been set to nil. The trick is to use a placeholder object for the meaning 'the value has been set to nil'. [NSNull null] is exactly meant to act as a placeholder for nil, but we can't use that here. There is a tiny bug in -[refreshObject:mergeChanges:YES] that will change a transient attribute from [NSNull null] to nil. No biggie, we just use some other unique object. I use a constant string below. That is not perfect since compile-time constants are interned. Never mind that for now, it is not a problem as long as we are not dealing with string values.

With the set-to-nil placeholder, we get these get/set methods:

static const id SetNil = @"<set-nil>";

- (NSColor*)color
{
    [self willAccessValueForKey:@"color"];
    id change = [self primitiveValueForKey:@"color"];
    [self didAccessValueForKey:@"color"];
    if (change!=nil)
        return change==SetNil ? nil : change;
    else
        return colorSnapshot;
}

- (void)setColor:(NSColor*)value
{
    [self willChangeValueForKey:@"color"];
    [self setPrimitiveValue:(value==nil ? SetNil : value) forKey:@"color"];
    [self didChangeValueForKey:@"color"];
}

We need to update saving as well:

- (void)willSave
{
    id change = [self primitiveValueForKey:@"color"];
    if (change!=nil) {
        if (change!=SetNil)
            [self setPrimitiveValue:[NSKeyedArchiver archivedDataWithRootObject:change]
                forKey:@"colorData"];
        else
            [self setPrimitiveValue:nil forKey:@"colorData"];
    }
    [super willSave];
}

- (void)didSave
{
    [super didSave];
    id change = [self primitiveValueForKey:@"color"];
    if (change!=nil) {
        [colorSnapshot release];
        colorSnapshot = change==SetNil ? nil : [change retain];
        [self setPrimitiveValue:nil forKey:@"color"];
    }
}

Cool, now the value is considered changed from the time it is set until it is saved successfully. This makes a lot of sense to me, but it is not the way persistent properties work. A persistent property is considered changed if its value is different from the snapshot. Let's say color is red in the persistent store, and I say [obj setColor:red]. In one model that is a changed attribute, in the other it isn't.

To create that behaviour, we need this method:

- (void)reconcileChanges
{
    id change = [self primitiveValueForKey:@"color"];
    if (change!=nil) {
        if ((colorSnapshot==nil && change==SetNil) || 
            (colorSnapshot!=nil && [colorSnapshot isEqual:change]))
            [self setPrimitiveValue:nil forKey:@"color"];
    }
}

That will mark our attribute as unchanged if the changed value is the same as the snapshot. You need to call -[reconcileChanges] just before -[refreshObject:mergeChanges:] or in -[setColor:].

Conclusion

It is better to imagine transient properties as representing 'something that is nil in the persistent store', than the common 'fancy instance variables with undo'. When dealing with a single managed object context both metaphors work, but when dealing with multiple managed object contexts, merging changes fails miserably for the 'instance variable with undo' way of thinking.

The pattern I suggest here for handling non-standard attributes with a binary 'shadow attribute' is:

  • Store the unarchived value corresponding to the current snapshot data in an instance variable.
  • Store the change relative to the snapshot in a transient variable.

The same pattern could be modified to implement a counter that can be edited simultaneously in multiple contexts (store a delta in the transient attribute). It could also be made to handle sets, including to-many cross-store relationships (store inserted and removed objects in two transient attributes).