On Mac OS X v10.5, Core Data provides transformable attributes to make it easy to use non-standard attribute types. For the benefit of developers who need to develop for earlier releases, this article describes how to support non-standard attribute types on Mac OS X v10.4.
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 how you can use non-standard attribute types by using a transient property to represent the non-standard attribute backed by a supported persistent property.
Introduction
The Persistent Attribute
An Object Attribute
A Non-Object Attribute
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 by using a transient property to represent the non-standard attribute backed by a supported persistent property. 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. You use a transient property to represent the non-standard attribute and write code to convert it to a standard persistent property.
The following sections illustrate implementations for object and scalar values. Both start, however, with a common task—you must specify a persistent attribute.
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
). 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.
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.
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.
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.
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”).
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 primitiveValueForKey:@"color"]; |
[self didAccessValueForKey:@"color"]; |
if (color == nil) { |
NSData *colorData = [self valueForKey:@"colorData"]; |
if (colorData != nil) { |
color = [NSKeyedUnarchiver unarchiveObjectWithData:colorData]; |
[self setPrimitiveValue:color forKey:@"color"]; |
} |
} |
return color; |
} |
Using this approach, you retrieve and cache the persistent value in awakeFromFetch
.
- (void)awakeFromFetch |
{ |
[super awakeFromFetch]; |
NSData *colorData = [self valueForKey:@"colorData"]; |
if (colorData != nil) { |
NSColor *color; |
color = [NSKeyedUnarchiver unarchiveObjectWithData:colorData]; |
[self setPrimitiveValue:color forKey:@"color"]; |
} |
} |
In the get accessor you then simply return the cached value.
- (NSColor *)color |
{ |
[self willAccessValueForKey:@"color"]; |
NSColor *color = [self primitiveValueForKey:@"color"]; |
[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.
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.
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.
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.
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; |
} |
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"]; } |
© 2004, 2009 Apple Inc. All Rights Reserved. (Last updated: 2009-03-04)