< Previous PageNext Page > Hide TOC

Non-Standard Persistent Attributes

Core Data supports a range of common types for values of persistent attributes, including string, date, and number. Sometimes, however, you want an attribute's value to be a type that is not supported directly. For example, in a graphics application you might want to define a Rectangle entity that has attributes color and bounds that are an instance of NSColor and an NSRect struct respectively. This article describes the two ways in which you can use non-standard attribute types: using transformable attributes, or by using a transient property to represent the non-standard attribute backed by a supported persistent property.

Note: If you are using Mac OS X v10.4, read “Mac OS X v10.4: Non-Standard Persistent Attributes” instead.

Contents:

Introduction
Transformable Attributes
Custom Code
Type-Checking


Introduction

Persistent attributes must be of a type recognized by the Core Data framework so that they can be properly stored to and retrieved from a persistent store. Core Data provides support for a range of common types for persistent attribute values, including string, date, and number (see NSAttributeDescription for full details). Sometimes, however, you want to use types that are not supported directly, such as colors and C structures.

You can use non-standard types for persistent attributes either by using transformable attributes or by using a transient property to represent the non-standard attribute backed by a supported persistent property. The principle behind the two approaches is the same: you present to consumers of your entity an attribute of the type you want, and “behind the scenes” it’s converted into a type that Core Data can manage. The difference between the approaches is that with transformable attributes you specify just one attribute and the conversion is handled automatically. In contrast, with transient properties you specify two attributes and you have to write code to perform the conversion.

Transformable Attributes

The idea behind transformable attributes is that you access an attribute as a non-standard type, but behind the scenes Core Data uses an instance of NSValueTransformer to convert the attribute to and from an instance of NSData. Core Data then stores the data instance to the persistent store. By default, Core Data uses the NSKeyedUnarchiveFromDataTransformerName transformer, however you can specify your own transformer if you want.

To use a transformable attribute, you first simply add an attribute to your entity and specify that it is transformable. If you are using the model editor in Xcode, select Transformable in the Type popup; if you are setting the type programmatically, use setAttributeType: and pass NSTransformableAttributeType as the parameter. You then specify the name of the transformer to use: if you are using the model editor in Xcode, type the name in the Value Transformer Name text field; if you are setting the name programmatically, use setValueTransformerName:. (You don’t have to specify a name if you are using the default transformer.) If you specify a custom transformer, it must transform an instance of the non-standard data type into an instance of NSData and support reverse transformation.

In principle, you don’t have to do anything else. In practice, to suppress compiler warnings you should declare a property for the attribute as shown in the following example (notice favoriteColor):

@interface Employee :  NSManagedObject
{
}
 
@property (nonatomic, retain) NSString * firstName;
@property (nonatomic, retain) NSString * lastName;
@property (nonatomic, retain) NSSet* directReports;
@property (nonatomic, retain) Employee * manager;
@property (nonatomic, retain) Department * department;
 
@property (nonatomic, retain) NSColor * favoriteColor;
 
@end

You can also add an implementation directive, but (since the default is dynamic) this is not necessary.

@implementation Employee
 
@dynamic firstName;
@dynamic lastName;
@dynamic directReports;
@dynamic manager;
@dynamic department;
 
@dynamic favoriteColor;
 
@end

You can now use the attribute as you would any other standard attribute, as illustrated in the following code fragment:

Employee *newEmployee =
    [NSEntityDescription insertNewObjectForEntityForName:@"Employee"
        inManagedObjectContext:myManagedObjectContext];
 
newEmployee.firstName = @"Captain";
newEmployee.lastName = @"Scarlet";
newEmployee.favoriteColor = [NSColor redColor];

Custom Code

The following sections illustrate implementations for object and scalar values. Both start, however, with a common task—you must specify a persistent attribute.

Note: The example for an object value uses an instance of NSColor; if you are using Mac OS X v10.5, you should typically use a transformable attribute instead.

Basic Approach

To use non-supported types, in the managed object model you define two attributes. One is the attribute you actually want (its value is for example a color object or a rectangle struct). This attribute is transient. The other is a "shadow" representation of that attribute. This attribute is persistent.

You specify the type of the transient attribute as undefined (NSUndefinedAttributeType). Since Core Data does not need to store and retrieve transient properties, you can use any object type you want for the attribute in your implementation. Core Data does, though, track the state of transient properties so that they can participate in the object graph management (for example, for undo and redo).

The type of the shadow attribute must be one of the "concrete" supported types. You then implement a custom managed object class with suitable accessor methods for the transient attribute that retrieve the value from and store the value to the persistent attribute.

The basic approach for object and scalar values is the same—you must find a way to represent the unsupported data type as one of the supported data types—however there is a further constraint in the case of scalar values.

Scalar Value Constraints

A requirement of the accessor methods you write is that they must be key-value coding (and key-value observing) compliant. Key-value coding only supports a limited number of structures—NSPoint, NSSize, NSRect, and NSRange.

If you want to use a scalar type or structure that is not one of those supported directly by Core Data and not one of the structures supported by key-value coding, you must store it in your managed object as an object—typically an NSValue instance, although you can also define your own custom class. You will then treat it as an object value as described later in this article. It is up to users of the object to extract the required structure from the NSValue (or custom) object when retrieving the value, and to transform a structure into an NSValue (or custom) object when setting the value.

The Persistent Attribute

For any non-standard attribute type you want to use, you must choose a supported attribute type that you will use to store the value. Which supported type you choose depends on the non-standard type and what means there are of transforming it into a supported type. In many cases you can easily transform a non-supported object into an NSData object using an archiver. For example, you can archive a color object as shown in the following code sample. The same technique can be used if you represent the attribute as an instance of NSValue or of a custom class (note that your custom class would, of course, need to adopt the NSCoding protocol or provide some other means of being transformed into a supported data type).

NSData *colorAsData = [NSKeyedArchiver archivedDataWithRootObject:aColor];

You are free to use whatever means you wish to effect the transformation. For example, you could transform an NSRect structure into a string object (strings can of course be used in a persistent store).

NSRect aRect; // instance variable
NSString *rectAsString = NSStringFromRect(aRect);

You can transform the string back into a rectangle using NSRectFromString. You should bear in mind, however, that since the transformation process may happen frequently, you should ensure that it is as efficient as possible.

Typically you do not need to implement custom accessor methods for the persistent attribute. It is an implementation detail, the value should not be accessed other than by the entity itself. If you do modify this value directly, it is possible that the entity object will get into an inconsistent state.

An Object Attribute

If the non-supported attribute is an object, then in the managed object model you specify its type as undefined, and that it is transient. When you implement the entity’s custom class, there is no need to add an instance variable for the attribute—you can use the managed object's private internal store. A point to note about the implementations described below is that they cache the transient value. This makes accessing the value more efficient—it is also necessary for change management. If you define custom instance variables, you should clean up these variables in didTurnIntoFault rather than dealloc or finalize.

There are two strategies both for getting and for setting the transient value. You can retrieve the transient value either "lazily" (on demand—described in “The On-demand Get Accessor”) or during awakeFromFetch (described in “The Pre-calculated Get”). It may be preferable to retrieve it lazily if the value may be large (if for example it is a bitmap). For the persistent value, you can either update it every time the transient value is changed (described in “The Immediate-Update Set Accessor”), or you can defer the update until the object is saved (described in “The Delayed-Update Set Accessor”).

The On-demand Get Accessor

In the get accessor, you retrieve the attribute value from the managed object's private internal store. If the value is nil, then it is possible it has not yet been cached, so you retrieve the corresponding persistent value, then if that value is not nil, transform it into the appropriate type and cache it. The following example illustrates the on-demand get accessor for a color attribute.

- (NSColor *)color
{
    [self willAccessValueForKey:@"color"];
    NSColor *color = [self primitiveColor];
    [self didAccessValueForKey:@"color"];
    if (color == nil)
    {
        NSData *colorData = [self colorData];
        if (colorData != nil)
        {
            color = [NSKeyedUnarchiver unarchiveObjectWithData:colorData];
            [self setPrimitiveColor:color];
        }
    }
    return color;
}

The Pre-calculated Get

Using this approach, you retrieve and cache the persistent value in awakeFromFetch.

- (void)awakeFromFetch
{
    [super awakeFromFetch];
    NSData *colorData = [self colorData];
    if (colorData != nil)
    {
        NSColor *color;
        color = [NSKeyedUnarchiver unarchiveObjectWithData:colorData];
        [self setPrimitiveColor:color];
    }
}

In the get accessor you then simply return the cached value.

- (NSColor *)color
{
    [self willAccessValueForKey:@"color"];
    NSColor *color = [self primitiveColor];
    [self didAccessValueForKey:@"color"];
    return color;
}

This technique is useful if you are likely to access the attribute frequently—you avoid the conditional statement in the get accessor.

The Immediate-Update Set Accessor

In this set accessor, you set the value for both the transient and the persistent attributes at the same time. You transform the unsupported type into the supported type to set as the persistent value. You must ensure that you invoke the key-value observing change notification methods, so that objects observing the managed object—including the managed object context—are notified of the modification. The following example illustrates the set accessor for a color attribute.

- (void)setColor:(NSColor *)aColor
{
    [self willChangeValueForKey:@"color"];
    [self setPrimitiveValue:aColor forKey:@"color"];
    [self didChangeValueForKey:@"color"];
    [self setValue:[NSKeyedArchiver archivedDataWithRootObject:aColor]
                forKey:@"colorData"];
}

The main disadvantage with this approach is that the persistent value is recalculated each time the transient value is updated, which may be a performance issue.

The Delayed-Update Set Accessor

In this technique, in the set accessor you only set the value for the transient attribute. You implement a willSave method that updates the persistent value just before the object is saved.

- (void)setColor:(NSColor *)aColor
{
    [self willChangeValueForKey:@"color"];
    [self setPrimitiveValue:aColor forKey:@"color"];
    [self didChangeValueForKey:@"color"];
}
 
- (void)willSave
{
    NSColor *color = [self primitiveValueForKey:@"color"];
    if (color != nil)
    {
        [self setPrimitiveValue:[NSKeyedArchiver archivedDataWithRootObject:color]
                forKey:@"colorData"];
    }
    else
    {
        [self setPrimitiveValue:nil forKey:@"colorData"];
    }
    [super willSave];
}

If you adopt this approach, you must take care when specifying your optionality rules. If color is a required attribute, then (unless you take other steps) you must specify the color attribute as not optional, and the color data attribute as optional. If you do not, then the first save operation may generate a validation error.

When the object is first created, the value of colorData is nil. When you update the color attribute, the colorData attribute is unaffected (that is, it remains nil ). When you save, validateForUpdate: is invoked before willSave. In the validation stage, the value of colorData is still nil, and therefore validation fails.

Scalar Values

You can declare properties as scalar values, but for scalar values Core Data cannot dynamically generate accessor methods—you must provide your own implementations (see Managed Object Accessor Methods). Note that Core Data automatically synthesizes the primitive accessor methods (primitiveLength and setPrimitiveLength:), however you need to declare them to suppress compiler warnings.

For objects that will be used in either a Foundation collection or an AppKit view, you should typically allow Core Data to use its default storage instead of creating scalar instances to hold property values:

The advantages of allowing Core Data to manage its own storage usually outweigh any advantages of interacting directly with scalar values, although if you suspect that this is not true for your application you should use performance analysis tools to check.

You can declare properties as scalar values. Core Data cannot, though, dynamically generate accessor methods for scalar values—you must provide your own implementations. If you have an attribute length that is specified in the model as a double (NSDoubleAttributeType), in the interface file you declare length as:

@property double length;

In the implementation file, you implement accessors that invoke the relevant access and change notification methods, and the primitive accessors. Core Data automatically synthesizes the primitive accessor methods (primitiveLength and setPrimitiveLength:), but you need to declare them to suppress compiler warnings.

@interface MyManagedObject (PrimitiveAccessors)
- (NSNumber *)primitiveLength;
- (void)setPrimitiveLength:(NSNumber *)newLength;
@end
 
 
- (double)length
{
    [self willAccessValueForKey:@"primitiveLength"];
    NSNumber *tmpValue = [self primitiveLength];
    [self didAccessValueForKey:@"primitiveLength"];
    return (tmpValue!=nil) ? [tmpValue doubleValue] : 0.0; // or a suitable representation for nil
}
 
- (void)setLength:(double)value
{
    NSNumber* temp = [[NSNumber alloc] initWithDouble: value];
    [self willChangeValueForKey:@"primitiveLength"];
    [self setPrimitiveLength:temp];
    [self didChangeValueForKey:@"primitiveLength"];
    [temp release];
}

A Non-Object Attribute

If the non-supported attribute is one of the structures supported by key-value coding (NSPoint, NSSize, NSRect, or NSRange), then in the managed object model you again specify its type as undefined, and that it is transient. When you implement the entity’s custom class, you typically add an instance variable for the attribute. For example, given an attribute called bounds that you want to represent using an NSRect structure, your class interface might be like that shown in the following example.

@interface MyManagedObject : NSManagedObject
{
    NSRect bounds;
}
- (NSRect)bounds;
- (void)setBounds:(NSRect)aRect;
@end

Alternatively, if you want to give the instance variable a name other than the name of the attribute, you should also implement primitive get and set accessors (see “Custom Primitive Accessor Methods”), as shown in the following example.

@interface MyManagedObject : NSManagedObject
{
    NSRect myBounds;
}
- (NSRect)primitiveBounds;
- (void)setPrimitiveBounds:(NSRect)aRect;
- (NSRect)bounds;
- (void)setBounds:(NSRect)aRect;
@end

The primitive methods simply get and set the instance variable—they do not invoke key-value observing change or access notification methods—as shown in the following example.

- (NSRect)primitiveBounds
{
    return myBounds;
}
- (void)setPrimitiveBounds:(NSRect)aRect
    myBounds = aRect;
}

Whichever strategy you adopt, you then implement accessor methods mostly as described for the object value. For the get accessor you can adopt either the lazy or pre-calculated technique, and for the set accessor you can adopt either the immediate update or delayed update technique. The following sections illustrate only the former versions of each.

The Get Accessor

In the get accessor, you retrieve the attribute value from the managed object's private internal store. If the value has not yet been set, then it is possible it has not yet been cached, so you retrieve the corresponding persistent value, then if that value is not nil, transform it into the appropriate type and cache it. The following example illustrates the get accessor for a rectangle (this example makes a simplifying assumption that the bounds width cannot be 0, so if the value is 0 then the bounds has not yet been unarchived).

- (NSRect)bounds
{
    [self willAccessValueForKey:@"bounds"];
    NSRect aRect = bounds;
    [self didAccessValueForKey:@"bounds"];
    if (aRect.size.width == 0)
    {
        NSString *boundsAsString = [self boundsAsString];
        if (boundsAsString != nil)
        {
            bounds = NSRectFromString(boundsAsString);
        }
    }
    return bounds;
}

The Set Accessor

In the set accessor, you must set the value for both the transient and the persistent attributes. You transform the unsupported type into the supported type to set as the persistent value. You must ensure that you invoke the key-value observing change notification methods, so that objects observing the managed object—including the managed object context—are notified of the modification. The following example illustrates the set accessor for a rectangle.

- (void)setBounds:(NSRect)aRect
{
    [self willChangeValueForKey:@"bounds"];
    bounds = aRect;
    [self didChangeValueForKey:@"bounds"];
    NSString *rectAsString = NSStringFromRect(aRect);
    [self setValue:rectAsString forKey:@"boundsAsString"]; }

Type-Checking

If you define an attribute to use a non-standard type, you can also specify the name of the class used to represent the value, using setAttributeValueClassName:. If you do, Core Data automatically checks any value set and throws an exception if it is an instance of the wrong class.

You can only set the value class name in code. The following example shows how you can modify the managed object model of a subclass of NSPersistentDocument to include a value class name for a non-standard attribute (favoriteColor) represented in this case by a an instance of a custom class, MyColor. Notice the subsequent programming error in setting the Captain Scarlet’s favorite color to an instance of NSColor.

- (NSManagedObjectModel *)managedObjectModel
{
    if (myManagedObjectModel == nil)
    {
        NSBundle *bundle = [NSBundle bundleForClass:[self class]];
        NSString *path = [bundle pathForResource:@"MyDocument" ofType:@"mom"];
        NSURL *url = [NSURL fileURLWithPath:path];
        myManagedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:url];
 
        NSEntityDescription *employeeEntity =
            [[myManagedObjectModel entitiesByName] objectForKey:@"Employee"];
        NSAttributeDescription *favoriteColorAttribute =
            [[employeeEntity attributesByName] objectForKey:@"favoriteColor"];
 
        // set the attribute value class to MyColor
        [favoriteColorAttribute setAttributeValueClassName:@"MyColor"];
    }
 
    return myManagedObjectModel;
}
 
 
- (void)windowControllerDidLoadNib:(NSWindowController *)windowController
{
    [super windowControllerDidLoadNib:windowController];
 
    Employee *newEmployee =
        [NSEntityDescription insertNewObjectForEntityForName:@"Employee"
            inManagedObjectContext:[self managedObjectContext]];
 
    newEmployee.firstName = @"Captain";
    newEmployee.lastName = @"Scarlet";
    newEmployee.favoriteColor = [NSColor redColor]; // exception thrown here
}
 

Note that the attribute value class must actually exist at runtime. If you misspell the class name itself (for example, MyColour instead of MyColor), the check succeeds silently.



< Previous PageNext Page > Hide TOC


© 2004, 2009 Apple Inc. All Rights Reserved. (Last updated: 2009-03-04)


Did this document help you?
Yes: Tell us what works for you.
It’s good, but: Report typos, inaccuracies, and so forth.
It wasn’t helpful: Tell us what would have helped.