Business Logic

After you design and build enterprise object classes as discussed in Business Objects, you need to add business logic to some of those classes. The applications you build with Enterprise Objects derive most of their value from the business logic—or business rules—you build into them. Business rules make data relevant and the implementation of business logic is perhaps the most important task when building data-driven applications.

Many database application development environments force you to implement business logic in all the wrong places, such as in the data source itself or intermixed within user interface code. Implementing business logic in these places makes it less reusable, harder to maintain, and makes it less adaptable to changing business needs. These and other consequences of implementing business logic in the wrong places are described in Why Enterprise Objects?.

This chapter describes the best place to implement business logic—in reusable, portable, data source–independent Java objects called enterprise objects. It consists mostly of specific tasks that you commonly perform when adding business logic to Enterprise Objects applications. It includes these sections:

Custom Classes

The first step in implementing business logic in an enterprise object is assigning a custom class to an entity and generating a Java class file for that entity that will contain the custom business logic. As described in Implementing an Enterprise Object, by default all enterprise objects are assigned to the class com.webobjects.eocontrol.EOGenericRecord. For many enterprise objects, this is sufficient. However, if you want to implement business logic in an enterprise object, you must assign that enterprise object to a custom subclass of EOGenericRecord or EOCustomObject.

The assignment of classes to enterprise objects happens in EOModeler. You assign entities class names by selecting the root of the entity tree while in table mode and editing class names in the table.

It’s recommended that you supply fully qualified class names but this isn’t strictly required. So while you could specify Document as the class name for the Document entity, it’s best to specify a fully-qualified name such as com.mycompany.Document instead.

Once you assign a class to an entity, you need to generate a Java class file for that entity. To do this, simply select the entity in the tree view and choose Generate Java Files from the Property menu. Class file generation is discussed in more detail in Generating Source Files. In the save dialog that appears, choose your project directory or the directory of a business logic framework. See Building a Reusable Framework for more information on building a framework that you can use to share the same business logic classes among many applications.

Providing Initial Values

One of the most common business rules is to assign initial values to newly inserted records. For example, when a user creates a new record, you may want to immediately populate a field in that record called creationDate.

To do this, simply override the method awakeFromInsertion in an enterprise object class:

public void awakeFromInsertion(EOEditingContext context) {
    super.awakeFromInsertion(context);
    if (creationDate() == null) {
        setCreationDate(new NSTimestamp());
    }
}

This method sets the creationDate attribute to the current time. (Determining if the attribute is null is not usually necessary except in three-tier Java Client applications.)

awakeFromInsertion is automatically invoked immediately after an enterprise object class is instantiated and inserted into an editing context. You use this method to set default values in enterprise objects that represent new data.

For enterprise objects that represent existing data (data that’s already stored in a data source), you can use the method awakeFromFetch to provide additional initialization. This method is sent to an enterprise object that has just been created from a database row and initialized with database values.

You may wonder why it’s not recommended to initialize an enterprise object’s values in an object’s constructor. An enterprise object’s constructor represents the earliest state in the life of a particular object. The state of an enterprise object at the point of construction is not complete; the object is not fully initialized. It may not yet have been inserted into an editing context and it might not even have been assigned a global ID. You don’t ever want to manipulate an enterprise object that isn’t in an editing context or that doesn’t have a global ID.

Therefore, any logic you add in an enterprise object’s constructor may fail or be invalidated while the object finishes initializing. By providing custom logic in awakeFromInsertion or awakeFromFetch, however, you are guaranteed that an enterprise object is fully initialized.

Adding Validation

Another common requirement of business logic is that it validates all data that users enter, both to ensure that the data will work with other business rules and also to ensure the integrity of stored data. Enterprise Objects provides many different hooks to validate data. When you provide certain validation methods in your business logic classes, Enterprise Objects automatically invokes them to validate your data.

The EOValidation interface in the control layer defines these validation methods:

Validating Before Certain Operations

Some of these methods are invoked during certain Enterprise Objects operations, and by overriding them in your business logic classes, you can implement custom validation rules. When an application performs one of these operations on an editing context (save, delete, insert, or update objects), the editing context in which the operation is invoked sends a validation message to its enterprise objects. Based on the result returned from those enterprise objects, the operation is either allowed to continue or is refused.

For example, if you implement validateForSave in a particular enterprise object class, when a user attempts to save changes to a particular enterprise object (thereby invoking saveChanges on the object’s editing context), that object’s editing context first asks the enterprise object if it’s in a consistent state to save. It does this by invoking validateForSave. If validateForSave doesn’t throw an NSValidation.ValidationException, the operation is allowed to continue. However, if validateForSave throws an NSValidation.ValidationException, the save operation is not allowed to continue.

It’s up to you to decide what constitutes a valid enterprise object. In this example, you could write custom business logic in validateForSave that makes sure the enterprise object’s data is valid based on your application’s business rules.

All classes that implement the EOEnterpriseObject interface include a default implementation of the validateForOperation methods. The default implementation of validateForDelete, for example, validates that an enterprise object’s relationships conform to their specified referential integrity rules.

The default implementations of validateForSave, validateForInsert, and validateForUpdate invoke validateValueForKey for each of an object’s class properties. See Validating Individual Properties for more information on validateValueForKey.

Validating Individual Properties

In addition to the validation that occurs before certain operations are allowed to proceed, you can also validate the individual properties of an enterprise object. The EOValidation interface defines another validation method, validateValueForKey. The default implementation of validateValueForKey searches for and invokes methods of the form validateKey in enterprise object classes. You implement a validateKey method for each property of an enterprise object you want to validate.

For example, it’s common to want to validate a zip code field to make sure that a user entered five numbers (for zip codes in the United States). In an enterprise object class in which the zipcode field exists, you add this method:

public Object validateZipcode(Object zipcode) throws        NSValidation.ValidationException {
    if ((Number)zipcode.length() > 5) {
        throw new NSValidation.ValidationException(“Invalid zipcode.”);
}
    else return zipcode;
}

When a user tries to assign a value to the zipcode property of an enterprise object, the default implementation of validateValueForKey invokes validateZipcode. Based on the result of that method, the user-entered value is either allowed to be assigned to the property or refused. Immediately before invoking validateKey methods, the default implementation of validateValueForKey validates the property against constraints specified in the data model, such as null constraints and referential integrity rules.

Another common use of validateValueForKey is to coerce user-entered data and return the coerced value. For example, users may enter a phone number in a text field in a form. That text field is bound to the phoneNumber attribute of an enterprise object. Rather than force users to enter the phone number in a particular format, such as with dash or period separators, you can let them enter a number in any format. Then, in the validatePhoneNumber method, you can coerce that number into the format you want and return the coerced string rather than the user-entered string.

Writing Business Methods

The properties of a given enterprise object (its attributes and relationships) do not usually provide all the values or data an application needs to be useful. To be of any value to your business, you usually need to add custom business logic in an application. The data in database tables usually stores raw business data. In order to make meaningful results from that data, you need to write business logic, usually in the form of business methods.

A business method in an enterprise object class returns a value based on data in the enterprise object’s properties. In the Real Estate model, you could write a business method to return the number of listings above a certain selling price that were sold by a particular agent.

This business method seeks information regarding a particular agent, so it should be implemented in the Agent business logic class. The method requires an Agent enterprise object and uses an Agent’s listings relationship and two attributes of a Listing enterprise object, isSold and sellingPrice, to determine the desired business information. Examples of this method appear in Listing 4-1, which assumes Listing is an EOGenericRecord subclass and in Listing 4-2, which assumes Listing is an EOCustomObject subclass.

Listing 3-1  Business method to determine information about the properties sold by a particular agent (assuming EOGenericRecord)

public int listingsSoldAbovePrice(String targetSellingPrice) {
     int hitCount = 0;
 
     NSArray listings = listings();
     java.util.Enumeration enum = listings.objectEnumerator();
 
     while (enum.hasMoreElements()) {
       webobjectsexamples.realestate.common.Listing listing = (webobjectsexamples.realestate.common.Listing)enum.nextElement();
       int isSold = ((Integer)listing.valueForKey("isSold")).intValue();
       int sellingPrice = ((Integer)listing.valueForKey("sellingPrice")).intValue();
       if ((isSold == 1) && (targetSellingPrice >= sellingPrice)) {
        hitCount++;
      }
    }
    return hitCount;
}

Listing 3-2  Business method to determine information about the properties sold by a particular agent (assuming EOCustomObject)

public int listingsSoldAbovePrice(BigDecimal targetSellingPrice) {
     int hitCount = 0;
 
     NSArray listings = listings();
     java.util.Enumeration enum = listings.objectEnumerator();
 
     while (enum.hasMoreElements()) {
       webobjectsexamples.realestate.common.Listing listing = (webobjectsexamples.realestate.common.Listing)enum.nextElement();
       Boolean isSold = listing.isSold();
       int sellingPrice = (listing.sellingPrice()).intValue();
       if ((isSold) && (targetSellingPrice.intValue() >= sellingPrice)) {
        hitCount++;
      }
    }
    return hitCount;
}

Within the Enterprise Objects frameworks, there are a number of other mechanisms you can use to derive business data. At the model level, you can specify custom read and write formats for particular attributes to coerce their values when they are read from the database and written back to the database. Also at the model level, you can define derived attributes that use custom SQL you write to derive business data. Both of these techniques are discussed in EOModeler User Guide.

The Foundation framework also provides mechanisms that help you write business logic. The class NSArray.Operator provides a number of operators that provide common information about a set of business data. These operators are listed in Table 4-1.

Table 3-1  NSArray operators

Operator

Operator description

count

Returns the number of elements in an array.

max

Returns the element in the array with the highest value.

min

Returns the element in the array with the lowest value.

avg

Returns the average of the values in the array.

sum

Returns the sum of the values in the array.

In the Real Estate business logic framework (located in /Developer/Examples/JavaWebObjects/Frameworks/JavaRealEstate), the webobjectsexamples.realestate.server.Agent class includes a method that uses the @avg operator. It is shown in Listing 4-3.

Listing 3-3  A business method using an NSArray operator

public Number averageRating() {
    if (_averageRating == null) {
    _averageRating = (Number)(ratings().valueForKey("@avg.rating.rating"));
    }
    return _averageRating;
}

Each of the operators requires an array of objects whose data type is a java.lang.Number (which includes the concrete classes java.lang.Integer and java.lang.BigDecimal). As shown in Listing 4-3, you use the operators within an invocation of the key-value coding method valueForKey. All the operators except @count require you to specify both an array of objects and the element within the array on which to apply the operator.

Manipulating Relationships

Enterprise Objects makes working with relationships rather simple. All you need is two enterprise objects in the same editing context to manipulate relationships programmatically. Were you to perform the same kind of task in other database development environments, you’d likely have to write many lines of SQL to relate a record in one table with a record in another table. With Enterprise Objects, however, a single method invocation does this for you.

Say you have an enterprise object representing a Document entity and a relationship in that entity called writers, which represents the authors of the document. To associate a new Writer record with the Document record, you simply create an enterprise object for the writer and then add it to the relationship with this code:

document.addObjectToBothSidesOfRelationshipWithKey(writer, "writers");

The first argument in the method invocation represents the Writer object. The second argument corresponds to the name of the relationship in the Document entity. If the relationship (in this case writers) is modeled with an inverse relationship, this method also adds the object to the other side of the relationship.

In addition to adding records in a relationship, you’ll likely also need to remove them. Fortunately, Enterprise Objects provides another method that does all the work for you:

document.removeObjectFromBothSidesOfRelationshipWithKey(writer, "writers");

Building a Reusable Framework

Well-designed enterprise objects are reusable. That is, if you do not use SQL, access layer classes (besides EOUtilities), or WebObjects classes (com.webobjects.appserver) in enterprise object classes, your enterprise objects will be reusable.

The best way for multiple applications to share the same business logic is to build a framework. This framework holds custom Java classes generated by EOModeler and .eomodeld files. By using a framework, changes to enterprise object classes in that framework and to the model are picked up by all applications that use the framework.

To build a framework, make a new project of type WebObjects Framework (see the document Project Builder for WebObjects Developers for more information on project types). Then, add business logic classes and other resources to it.

The only tricky part is assigning classes and resources to the correct targets. Model files and server-side business logic classes should be assigned to the Application Server target, while client-side business logic classes should be assigned to the Web Server target. If you’re not building a three-tier desktop application, you’ll only have server-side business logic classes. See the document Project Builder for WebObjects Developers for more information on targets in WebObjects projects.