An attribute may be simple, derived, or flattened. A simple attribute typically corresponds to a single column in the database, and may be read or updated directly from or to the database. A simple EOAttribute may also be set as read-only with its setReadOnly: method. Read-only attributes of enterprise objects are never updated.
A derived attribute doesn't necessarily correspond to a single database column in its entity's database table, and is usually based on some other attribute, which is modified in some way. For example, if an Employee entity has a simple monthly salary attribute, you can define a derived annualSalary attribute as "salary * 12". Derived attributes, since they don't correspond to actual values in the database, are read-only; it makes no sense to write a derived value.
A flattened attribute of an entity is actually an attribute of some other entity that's fetched through a relationship with a database join. A flattened attribute's external definition is a data path ending in an attribute name. For example, if the Employee entity has the relationship toAddress and the Address entity has the attribute street, you can define streetName as an attribute of your Employee EOEntity by creating an EOAttribute for it with a definition of "toAddress.street".
Creating a Simple Attribute
A simple attribute needs at least the following characteristics:
- A name unique within its EOEntity
- The name of a column in the database table for its entity (the EOAttribute's external name)
- A declaration of the type of that column as defined by the database and adaptor (the EOAttribute's external type)
- A declaration of the Objective-C class used to represent values outside the context of an enterprise object
- For Objective-C value classes that require it, a subtype for such distinctions as between numeric types
You also have to set whether the attribute is part of its entity's primary key, is a class property, or is used for locking. See the EOEntity class description for more information on these three groups of attributes. This code excerpt gives an example of creating a simple EOAttribute and adding it to an EOEntity:
EOEntity *employeeEntity; /* Assume this exists. */ EOAttribute *salaryAttribute; NSArray *empClassProps; NSArray *empLockAttributes; BOOL result; salaryAttribute = [[EOAttribute alloc] init]; [salaryAttribute setName:@"salary"]; [salaryAttribute setColumnName:@"SALARY"]; [salaryAttribute setExternalType:@"money"]; [salaryAttribute setValueClassName:"NSDecimalNumber"]; [employeeEntity addAttribute:salaryAttribute]; [salaryAttribute release]; empClassProps = [[employeeEntity classProperties] mutableCopy]; [empClassProps addObject:salaryAttribute]; [employeeEntity setClassProperties:empClassProps]; [empClassProps release]; empLockAttributes = [[employeeEntity attributesUsedForLocking] mutableCopy]; [empLockAttributes addObject:salaryAttribute]; result = [employeeEntity setAttributesUsedForLocking:empLockAttributes]; [empLockAttributes release];
Creating a Derived Attribute
A derived attribute depends on another attribute, so you base it on a definition including that attribute's name rather than on an external name. Because a derived attribute isn't mapped directly to anything in the database, you shouldn't include it in the entity's set of primary key attributes or attributes used for locking:
EOEntity *employeeEntity; /* Assume this exists. */ EOAttribute *bonusAttribute; NSArray *empClassProps; BOOL result; bonusAttribute = [[EOAttribute alloc] init]; [bonusAttribute setName:@"bonus"]; [bonusAttribute setDefinition:@"salary * 0.5"]; [bonusAttribute setValueClassName:@"NSDecimalNumber"]; [employeeEntity addAttribute:bonusAttribute]; [bonusAttribute release]; empClassProps = [[employeeEntity classProperties] mutableCopy]; [empClassProps addObject:bonusAttribute]; result = [employeeEntity setClassProperties:empClassProps]; [empClassProps release];
Creating a Flattened Attribute
A flattened attribute depends on a relationship, so you base it on a definition including that relationship's name rather than on an external name. Because a flattened attribute doesn't correspond directly to anything in its entity's table, you don't have to specify an external name, and shouldn't include it in the entity's set of primary key attributes or attributes used for locking:
EOEntity *employeeEntity; /* Assume this exists. */ EOAttribute *deptNameAttribute; NSArray *empClassProps; BOOL result; deptNameAttribute = [[EOAttribute alloc] init]; [deptNameAttribute setName:@"departmentName"]; [deptNameAttribute setValueClassName:"NSString"]; [deptNameAttribute setExternalType:@"varchar"]; [employeeEntity addAttribute:deptNameAttribute]; [deptNameAttribute setDefinition:@"toDepartment.name"]; [deptNameAttribute release]; empClassProps = [[employeeEntity classProperties] mutableCopy]; [empClassProps addObject:deptNameAttribute]; result = [employeeEntity setClassProperties:empClassProps]; [empClassProps release];
Instead of flattening attributes in your model, a better approach is often to directly traverse the object graph through relationships.
Mapping from Database to Objects
Every EOAttribute has an external type, which is the type used by the database to store its associated data, and an Objective-C class used as the type for that data in the client application. The type used by the database is accessed with the setExternalType: and externalType methods. The class type used by the application is accessed with the valueClassName method. You can map database types to a set of standard value classes, which includes:
Database-specific adaptors automatically handle value conversions for these classes. You can also create your own custom value class, so long as you define a format that it uses to interpret data. Your value class must also implement the EOCustomClassArchiving protocol to work as a custom value; see that protocol specification for more information. For more information on using EOAttribute methods to work with custom data types, see the next section, "Working with Custom Data Types".
The handling of dates assumes by default that both the database server and the client application are running in the same, local, time zone. You can alter the server time zone with the setServerTimeZone: method. If you alter the server time zone, the adaptor automatically converts dates as they pass into and out of the server.
Working with Custom Data Types
When you create a new model, EOModeler maps each attribute in your model to one of the primitive data types the adaptor knows how to manipulate: NSString, NSNumber, NSDecimalNumber, NSData, and NSDate. For example, suppose you have a photo attribute that's stored in the database as a LONG RAW. When you create a new model, this attribute is mapped to NSData. However, NSData is just an object wrapper for binary data-for instance, it doesn't have any methods for operating on images, which would limit what you'd be able to do with the image in your application. This is a case in which you'd probably choose to use a custom data type, such as NSImage.
For a custom data type to be usable in Enterprise Objects Framework, it must supply methods for importing and exporting itself as one of the primitive types so that it can be read from and written to the database. Specifically, to use a custom data type you need to do the following:
- Set the attribute's value class using the method setValueClassName:.
- Set the factory method that will be used to create instances of your class from raw data using the method setValueFactoryMethodName:.
- Set the type of the argument that should be passed to the factory method using the method setFactoryMethodArgumentType:.
- Set the conversion method that is used to convert your data back into one of the primitive data types the adaptor can work with using the method setAdaptorValueConversionMethodName:; this enables the data to be stored in the database.
If an EOAttribute represents a binary column in the database,
the factory method argument type can be either EOFactoryMethodArgumentIsNSData or EOFactoryMethodArgumentIsBytes, indicating
that the method takes an NSData object or raw bytes as an argument.
If the EOAttribute represents a string or character column, the
factory method argument type can be either EOFactoryMethodArgumentIsNSString or
indicating that the method takes an NSString object or raw bytes as
an argument. These types apply when fetching custom values, as described
The following code excerpt demonstrates how these methods work together. The example shows two custom data types: an image that's initialized with an NSData, and a custom zip code that's initialized with a string.
[imageAttribute setValueClassName:@"NSImage"]; [imageAttribute setFactoryMethodArgumentType:EOFactoryMethodArgumentIsNSData]; [imageAttribute setValueFactoryMethodName:@"imageWithData:"]; [imageAttribute setAdaptorValueConversionMethodName:@"TIFFRepresentation"]; [zipCodeAttribute setValueClassName:@"MyZipCodeClass"]; [zipCodeAttribute setFactoryMethodArgumentType:EOFactoryMethodArgumentIsBytes]; [zipCodeAttribute setValueFactoryMethodName:@"zipCodeWithBytes:length:"]; [zipCodeAttribute setAdaptorValueConversionMethodName:@"zipCodeString"];
Instead of setting the class information programmatically, you can use the Attributes Inspector in EOModeler, which is more common. For more information, see the chapter "Advanced Enterprise Objects Modeling" in the Enterprise Objects Framework Developer's Guide.
Fetching Custom Values
Custom values are created during fetching in EOAdaptorChannel's fetchRowWithZone: method. This method fetches data in the external (server) type and converts it to a value object. For scalar database types such as numbers and dates, the EOAdaptorChannel converts the value itself. For binary and string database types, it calls upon the EOAttribute being fetched to perform the conversion, into either a standard or custom value class. EOAttribute's methods for performing this conversion are newValueForBytes:length: for binary data and newValueForBytes:length:encoding: for strings. These methods either convert the raw data directly into an NSData or NSString, or apply the custom value factory method to convert it into the custom class. Once the value is converted, the EOAdaptorChannel puts it into the dictionary for the row being fetched.
newValueForBytes:length: can handle NSData and raw bytes (void *). It converts the raw bytes into an NSData if the custom value argument type is EOFactoryMethodArgumentIsNSData, then invokes the custom value factory method with the NSData or bytes. If the EOAttribute has no custom value factory method, this method simply returns an NSData object containing the bytes.
newValueForBytes:length:encoding: can handle NSString and raw bytes. It converts the raw bytes into an NSString if the custom value argument type is EOFactoryMethodArgumentIsNSString, then it invokes the custom value factory method with the string or bytes. If the EOAttribute has no custom value factory method, this method simply returns an NSString object created from the bytes.
Converting Custom Values
Custom values are converted back to binary or character data in EOAdaptorChannel's evaluateExpression: method. For each value in the EOSQLExpression to be evaluated, the EOAdaptorChannel sends the appropriate EOAttribute an adaptorValueByConvertingAttributeValue: message to convert it. If the value is any of the standard value classes, it's returned unchanged. If the value is of a custom class, though, it's converted by applying the conversion method ( adaptorValueConversionMethod) specified in the EOAttribute.
SQL Statement Formats
In addition to mapping database values to object values, an EOAttribute can alter the way values are selected, inserted, and updated in the database by defining special format strings. These format strings allow a client application to extend its reach right down to the server for certain operations. For example, you might want to view an employee's salary on a yearly basis, without defining a derived attribute as in a previous example. In this case, you could set the salary attribute's SELECT statement format to "salary * 12" (with setReadFormat:) and the INSERT and UPDATE statement formats to "salary / 12" ( setWriteFormat:). Thus, whenever your application retrieves values for the salary attribute they're multiplied by 12, and when it writes values back to the database they're divided by 12.
Your application can use any legal SQL value expression in a format string, and can even access server-specific features such as functions and stored procedures (see EOEntity's setStoredProcedure:forOperation: method description for more information). Accessing server-specific features can offer your application great flexibility in dealing with its server, but does limit its portability. You're responsible for ensuring that your SQL is well-formed and will be understood by the database server.
Format strings for the setReadFormat: and setWriteFormat: methods should use "%P" as the substitution character for the value that is being formatted. "%@" will not work. For example:
[myAttribute setReadFormat:@"TO_UPPER(%P)"]; [myAttribute setWriteFormat:@"TO_LOWER(%P)"];
Instead of setting the read and write formats programmatically, you can set them in EOModeler, which is more common. For more information, see Enterprise Objects Framework Tools and Techniques.