[ Team LiB ] |
10.3 Application IdentityYou can use application identity with a datastore that allows the values in an instance to determine its identity. The values of one or more persistent fields in the instance form a unique value that is referred to as the primary key; the fields are referred to as the primary-key fields. The application is responsible for generating the values of the primary-key fields to ensure they collectively have a unique value for each instance in the datastore. The primary-key fields must have a unique value for a given class and its subclasses that use the same application identity class. 10.3.1 Primary-Key FieldsYou indicate that a Java field is a component of the primary key in the metadata by setting the primary-key attribute of the field's associated field element to "true". Each field of the primary key must have this attribute set to "true"; it has a default value of "false". The primary-key fields of a persistent class must be persistent. Therefore, the persistence-modifier attribute of the field metadata element cannot be set to "transactional" or "none". The primary-key fields become a property of the persistent class that cannot be changed after the class is enhanced. If you need to change the set of fields in a primary key, you will need to enhance the class again. Read access to primary-key fields is never mediated. The type of primary-key fields must be serializable and should be one of the primitive types, String, Date, Byte, Short, Integer, Long, Float, Double, BigDecimal, or BigInteger. JDO implementations are required to support these types and might support other reference types. When a transient instance is made persistent, the implementation uses the values of the primary-key fields to construct an identity for the instance. A JDOUserException is thrown during makePersistent( ) if an instance in the PersistenceManager cache already has the same primary key, or during the flush of the new instance to the datastore if the datastore already has an instance with the same primary key. The primary-key fields of a persistent class uniquely identify an instance in the datastore. Your Java object model will likely contain references and collections of references to instances of the class. The declaration and use of these references is performed with standard Java syntax. The JDO implementation automatically maps the references used at the Java level to primary keys when things are mapped to the underlying datastore. Your application does not need to know that application identity is being used, nor does it need to know what the primary-key fields are for a particular persistent class. You simply use the Java references. 10.3.2 Persistent Class equals() and hashCode( ) MethodsIt is important for you to understand the interaction between JDO identity and equality. The equals( ) method in Object simply uses the Java identity based on the address of the instance in the JVM. The Java identity of a persistent instance is guaranteed neither between PersistenceManagers, nor across space and time. You should implement equals( ) for your persistent classes that use application identity differently from the default implementation in Object. If you store persistent instances of classes using application identity in the datastore and query them using the == query operator, or refer to them by a persistent collection that enforces equality (Set, Map), then the implementation of equals( ) should exactly match the JDO implementation of equality, using the identity value (primary-key fields). To be portable, the equals( ) and hashCode( ) methods of any persistent class using application identity should depend on all of the primary-key fields. This policy is not enforced, but if it is not correctly implemented, the semantics of standard transient collections and the persistent collections may differ. Specifically, the Set and Map collections call the equals( ) and hashCode( ) methods of their elements to enforce uniqueness constraints and manage their element look up mechanisms. The identity (represented by the primary-key fields) to identify an instance uniquely in the datastore must be used in the management of these collections in the cache. 10.3.3 The Application-Identity ClassYou need to implement an application-identity class for your classes that use application identity. You can either define it by hand or use a tool some vendors provide to generate the class for you. The identity class needs to have fields that correspond, in name and type, with the primary-key fields in the persistent class. It should also have all of the characteristics of an RMI remote object for the class that will be used as a primary-key class in EJB. Specifically, the application identity class should have the following characteristics:
These restrictions allow you to construct an instance of the application identity class by providing only the values for the primary-key fields or, alternatively, by providing the result of toString( ) from an existing application identity instance. The names and types of the primary-key fields in the persistent class must be the same as the fields in the application identity class, and the fields in the application identity class must have a public access modifier. But you can choose any access modifier that you want for the primary-key fields in the persistent class. In particular, we recommend that you declare your primary-key fields private, since changing them is dependent on the implementation supporting the optional ChangeApplicationIdentity feature, covered later in this chapter. You must specify the application identity class in the metadata with the objectid-class attribute class element of the persistent class. You should use Java's rules for naming when specifying the objectid-class value: if you do not include a package in the name, it is assumed to be in the same package as the persistent class. If you use an inner class, use the $ marker before the inner class name. An implementation is permitted to extend the application-identity class to include additional fields not provided by the application, to further identify the instance in the datastore. Thus, the identity instance returned by an implementation might be a subclass of the user-defined application identity class. An implementation must be able to use an application identity instance from any other JDO implementation. 10.3.4 A Single-Field Primary KeyLet's start with a simple example. We'll create a new version of the RentalCode class that we defined in the com.mediamania.store package and place it in a new package called com.mediamania.store.appid. The sole reason we place the RentalCode class and its application identity class in a separate package is to distinguish between the class that uses datastore identity and the class that uses application identity. Your object model would normally have one class with one type of identity. The fields and a few of the methods of the new RentalCode class are declared as follows: package com.mediamania.store.appid; import java.math.BigDecimal; public class RentalCode { private String code; [1] private int numberOfDays; private BigDecimal cost; private BigDecimal lateFeePerDay; RentalCode( ) { } // methods, etc... public boolean equals(Object obj) { [2] return obj instanceof RentalCode && ((RentalCode)obj).code.equals(code); } public int hashCode( ) { [3] return code.hashCode( ); } } The code field declared on line [1] should contain a unique String value for each RentalCode instance, providing a natural primary-key. We also define equals( ) and hashCode( ) in terms of the primary-key field code on lines [2] and [3]. We specify the following metadata for the class: <package name="com.mediamania.store.appid"> <class name="RentalCode" objectid-class="com.mediamania.store.appid.RentalCodeKey" > <field name="code" primary-key="true" /> </class> </package> The metadata specifies the code field as the one primary-key field in RentalCode. We also specify the RentalCodeKey class as the application identity class for RentalCode. Let's examine the class in detail: package com.mediamania.store.appid; import java.io.Serializable; public class RentalCodeKey implements Serializable { [1] static { [2] RentalCode code = new RentalCode( ); } public String code; [3] public RentalCodeKey(String code) { [4] this.code = code; } public RentalCodeKey( ) { [5] code = new String(""); } public String toString( ) { [6] return code; } public boolean equals(Object obj) { [7] return obj instanceof RentalCodeKey && ((RentalCodeKey)obj).code.equals(code); } public int hashCode( ) { [8] return code.hashCode( ); } } On line [1], we declare that RentalCodeKey implements Serializable. The application identity class must have public fields that correspond to the primary-key fields in the persistent class; line [3] declares the code field. The class needs to have a public, no-arg constructor, which we define on line [5]. We also define a constructor on line [4], which takes a String argument. In the case of RentalCodeKey, only a single String field corresponds to the primary-key, so we can just assign the String argument to the code field. As we will see in the next example, if there are multiple primary-key fields, you will need to parse the values in the String argument to this constructor. Having the single code field of type String also makes our required toString( ) trivial as well. We also define equals( ) and hashCode( ) on lines [7] and [8], respectively. These methods delegate to the code field and call the corresponding String methods. Class registration code is placed in the static initialization method that the enhancer adds to your persistent class. The association between a persistent class and its application identity class is established when the persistent class is registered in the JDO environment. The JDO implementation does not know the specific application identity class for a persistent class until the persistent class has been loaded into the JVM and had this static initialization method executed. Often, the first time an application accesses a persistent instance via its identity, the application has not yet used the persistent class. The application creates and initializes an application identity instance, passing it to getObjectById( ). But the persistent class may not be loaded in the JVM yet, so the registration of the persistent class and its identity class has not occurred. The JDO implementation may throw an exception, indicating that you have passed an invalid identity value. To prevent this from happening, we must make sure that the persistent class has been loaded before we use an instance of the identity class to access an instance. By placing the static initialization block at line [2] in RentalCodeKey, we force the loading of RentalCode when RentalCodeKey is loaded. The RentalCode instance created in the static initialization block is garbage-collected once the block has finished, but this has the effect of loading the RentalCode class when the identity class is loaded. 10.3.5 A Compound Primary KeyThe application identity can consist of multiple primary-key fields. Now let's cover another example that illustrates additional approaches and techniques that can be used when defining an application identity class. We will now consider the following persistent Customer class that we have placed in the com.mediamania.store.appid package. This is a simplied version of the Customer class defined in the com.mediamania.store package. To provide a unique primary key, we use a combination of the firstName, lastName, and phone fields. With this persistent class, we define the application identity class as a static inner class, named Id, on line [2]. Since there is a tight coupling between an application identity class and its persistent class, it makes sense to define it as an inner class. But the inner class must be static; you cannot use a nonstatic inner class for the application identity class. Adopting this approach across all of your persistent classes simplifies development by instituting a single consistent naming mechanism for all your application identity classes. package com.mediamania.store.appid; import java.io.Serializable; import java.util.StringTokenizer; public class Customer { private String firstName; // primary-key field private String lastName; // primary-key field private String phone; // primary-key field private String email; // other fields removed for brevity in the example Customer( ) { } public Customer(String firstName, String lastName, String phone, String email) { this.firstName = firstName; this.lastName = lastName; this.phone = phone; this.email = email; } public String getFirstName( ) { return firstName; } public String getLastName( ) { return lastName; } public String getPhone( ) { return phone; } public String getEmail( ) { return email; } public boolean equals(Object obj) { [1] if(!(obj instanceof Customer)) return false; Customer c = (Customer)obj; Id id1 = new Id(firstName, lastName, phone); Id id2 = new Id(c.firstName, c.lastName, c.phone); return id1.equals(id2); } public int hashCode( ) { Id id = new Id(firstName, lastName, phone); return id.hashCode( ); } public static class Id implements Serializable { [2] static { Customer customer = new Customer( ); } public String firstName; public String lastName; public String phone; public Id(String fname, String lname, String phone) { [3] firstName = fname; lastName = lname; this.phone = phone; } public Id( ) { [4] firstName = ""; lastName = ""; phone = ""; } public Id(String val) { [5] StringTokenizer tokenizer = new StringTokenizer(val, "|"); firstName = tokenizer.nextToken( ); lastName = tokenizer.nextToken( ); phone = tokenizer.nextToken( ); } public String toString( ) { [6] StringBuffer buffer = new StringBuffer( ); buffer.append(firstName); buffer.append("|"); buffer.append(lastName); buffer.append("|"); buffer.append(phone); return buffer.toString( ); } public boolean equals(Object obj) { [7] if (!(obj instanceof Id)) return false; Id id = (Id) obj; if (!phone.equals(id.phone)) return false; if (!lastName.equals(id.lastName)) return false; return firstName.equals(id.firstName); } public int hashCode( ) { [8] return toString().hashCode( ); } } } We need to define equals( ) and hashCode( ) in Customer, and they must be based on the values of the primary-key fields. Line [1] defines these methods. Since the functionality that manages the composite value of the three primary-key fields is defined in the Id class, equals( ) and hashCode( ) delegate to temporary Id instances already in Id, instead of duplicating the code. This strategy also makes sure they implement the same functionality. This may or may not always make sense for your persistent classes. The Id class provides three constructors. The constructors defined on lines [4] and [5] are required of all application identity classes. Since this persistent class has multiple primary-key fields, the constructor defined on line [5] must parse the String to initialize each component of the primary key. An application identity class does not require the constructor defined on line [3], but it provides a useful means of initializing all the primary-key components. We define the required application identity method toString( ) on line [6]; its result can be used by the String method on line [5] to initialize a new Id instance. We need to define equals( ) and hashCode( ) in our application identity classes, and they should be based on the values of all the primary-key fields. On line [7], we define equals( ) for Id. We define hashCode( ) on line [8], and it uses Id's toString( ) method to construct a String containing all the primary-key field values and then calls String's hashCode( ) to compute the hash code for Id. Let's examine the metadata for Customer: <package name="com.mediamania.store.appid"> <class name="Customer" identity-type="application" objectid-class="Customer$Id" > <field name="firstName" primary-key="true" /> <field name="lastName" primary-key="true" /> <field name="phone" primary-key="true" /> </class> </package> We provide field elements to specify each of the primary-key fields. Since we provide a value for objectid-class, inclusion of the identity-type attribute is optional. We let the package of the objectid-class attribute value default to the same package as the persistent class, since we do not include the package name. Since Id is an inner class, we use $ between the class name and inner class name to denote Id. 10.3.6 A Compound Primary Key That Contains a Foreign KeyIt is common in relational schemas to have a compound primary key that includes a foreign key column. For example, assume you have a table in your relational database, called Order, to represent an order placed by a customer. The Order table has a primary-key column containing a unique order number. A separate table, called LineItem, contains the individual items in the customer's order. There is a one-to-many relationship between Order and LineItem, represented by the LineItem table having a foreign key reference to a row in the Order table. To identify a particular LineItem row uniquely, we define a primary key for LineItem that consists of the order number, which is a foreign key reference to Order, and a line-item number that is unique within the particular order. A primary key, like the one defined for the LineItem table, is very common in relational schemas. Let's examine the Java classes and metadata necessary to represent such a model. An Order class could be defined as follows: package com.mediamania.store; import java.io.Serializable; public class Order { private int orderNumber; // primary-key field private Customer customer; public Order( ) { orderNumber = 0; } public Order(Customer cust, int orderNum) { customer = cust; orderNumber = orderNum; } public boolean equals(Object obj) { return obj instanceof Order && ((Order)obj).orderNumber == orderNumber; } public int hashCode( ) { return orderNumber; } public static class Id implements Serializable { static { Order order = new Order( ); } public int orderNumber; public Id( ) { orderNumber = 0; } public Id(int orderNum) { orderNumber = orderNum; } public Id(String orderNum) { orderNumber = 0; try { Integer.parseInt(orderNum); } catch(NumberFormatException e) { } } public String toString( ) { return Integer.toString(orderNumber); } public boolean equals(Object obj) { return obj instanceof Id && ((Id)obj).orderNumber == orderNumber; } public int hashCode( ) { return orderNumber; } } } In a real application, the class would likely have more fields and methods, but we primarily want to describe the application identity classes that are appropriate for this model. The orderNumber field in Order has a unique value that uniquely identifies an Order instance. We define the application identity class for Order as a static inner class named Id. The Id class has a corresponding orderNumber field. The application needs to have a means of acquiring a unique value for orderNumber. JDO does not currently provide a facility for generating unique application values, but it is being considered for a future release. Some JDO implementations provide such a facility now. The Order.Id class implements all the functionality necessary in an application identity class. Now let's examine the LineItem class. As in the Order class, we do not provide all the fields and functionality a real application would have, but we include fields and methods relevant to our discussion. package com.mediamania.store; import java.io.Serializable; import java.math.BigDecimal; public class LineItem { private int orderNumber; // primary-key field private int itemNumber; // primary-key field private String description; private BigDecimal price; // other fields LineItem( ) { orderNumber = 0; itemNumber = 0; } public LineItem(int orderNum, int itemNum, String desc, BigDecimal price) { orderNumber = orderNum; itemNumber = itemNum; description = desc; this.price = price; } // other methods public static class Id implements Serializable { static { LineItem item = new LineItem( ); } public int orderNumber; public int itemNumber; public Id( ) { orderNumber = 0; itemNumber = 0; } public Id(int orderNum, int itemNum) { orderNumber = orderNum; itemNumber = itemNum; } public Id(String val) { int separatorIndex = val.indexOf('|'); orderNumber = 0; itemNumber = 0; try { orderNumber = Integer.parseInt(val.substring(0,separatorIndex)); } catch (NumberFormatException e) { } try { itemNumber = Integer.parseInt(val.substring(separatorIndex+1)); } catch (NumberFormatException e) { } } public String toString( ) { return Integer.toString(orderNumber) + "|" + Integer.toString(itemNumber); } public boolean equals(Object obj) { if (!(obj instanceof Id)) return false; Id id = (Id) obj; return orderNumber == id.orderNumber && itemNumber == id.itemNumber; } public int hashCode( ) { return orderNumber*1000 + itemNumber; } } } LineItem has two primary-key fields: orderNumber and itemNumber. Again, we define the application identity class as a static inner class Id. It contains the two fields of the primary key: orderNumber and itemNumber. You may consider it more appropriate to declare the primary-key fields as follows: private Order order; // primary-key field private int itemNumber; // primary-key field Since the LineItem table in the database has a foreign-key reference to the Order table, this would seem to be the natural mapping. But the type of primary-key fields in JDO should be one of the primitive, String, Date, or Number types. The fields in the application identity class and the application identity class itself must be serializable. But if we use the preceding order field, when the identity instance is serialized it will also serialize the Order and possibly other persistent instances. You may still want to have a reference to Order that you can use to navigate to the instance. You could declare the following fields in the LineItem class: private int orderNumber; // primary-key field private int itemNumber; // primary-key field private Order order; private String description; private BigDecimal price; How this gets mapped to the underlying datastore depends on the capabilities of the JDO implementation you are using. Some implementations would require the underlying datastore to have a redundant orderNumber field, since the order field declared in this example would be represented in the datastore by the primary key of Order, declared to be an order number. There are some implementations that would allow the orderNumber and order fields to be mapped onto the same column in a relational database. These implementations also ensure that these two fields are always kept in sync, as a change to one of the fields necessitates a change to the other. Here is the metadata for the Order and LineItem classes: <package name="com.mediamania.store" > <class name="Order" objectid-class = "Order$Id" > <field name="orderNumber" primary-key="true" /> </class> <class name="LineItem" objectid-class="LineItem$Id" > <field name="orderNumber" primary-key="true" /> <field name="itemNumber" primary-key="true" /> </class> </package> 10.3.7 Application Identity in an Inheritance HierarchyThere are special considerations when using application identity for persistent classes in an inheritance hierarchy. Only certain persistent classes in the inheritance hierarchy can have primary-key fields, and there are restrictions on the definition and metadata specification of their associated application identity classes. Every class in the hierarchy must have exactly one nonabstract (concrete) application identity class. A least-derived (topmost), concrete persistent class must have an associated application identity class, specified either in the objectid-class attribute of its own persistent class's metadata, or in the objectid-class attribute of one of its abstract superclasses. The persistent class and all its subclasses use this concrete application identity class. The subclasses must not specify a value for the objectid-class attribute. You can declare primary-key fields only in abstract superclasses and in the topmost, concrete classes in an inheritance hierarchy. You need to define an application identity class for each persistent class in the hierarchy that has a primary-key field. Each of these application identity classes must declare fields that correspond to the primary-key fields in their respective persistent class. Within an inheritance hierarchy, you can have intermediate classes between two persistent classes that have primary-key fields, in which the intermediate classes do not have any primary-key fields. The simplest design is to define one application identity class for the entire inheritance hierarchy, specified at the least-derived persistent class in the hierarchy, regardless of whether it is concrete or abstract. If you require multiple application identity classes for the persistent classes in an inheritance hierarchy, the application identity classes form an inheritance hierarchy that corresponds to the inheritance hierarchy of their associated persistent classes. Let's look at an example, illustrated in Figure 10-1. If a Component abstract class declares a masterId primary-key field, the ComponentKey application identity class (which should be abstract as well) must also declare a field of the same name and type. Figure 10-1. Inheritance of application identity classes in inheritance hierarchiesThe following code declares a subset of the Component class: package productdesign; public abstract class Component { private String masterId; // primary-key field private int x; private int y; // other fields protected Component( ) { } protected Component(String id) { masterId = id; x = 0; y = 0; } // other methods } We define the ComponentKey class as follows: package productdesign; import java.io.Serializable; public abstract class ComponentKey implements Serializable { static { Component comp = new Component( ); } public String masterId; public ComponentKey( ) { masterId = ""; } public ComponentKey(String id) { masterId = id; } public String toString( ) { return masterId; } public boolean equals(Object obj) { return obj instanceof ComponentKey && ((ComponentKey)obj).masterId.equals(masterId); } public int hashCode( ) { return masterId.hashCode( ); } } A concrete Part class that extends Component must declare a concrete application identity class (for example, PartKey) that extends ComponentKey. Part might not have its own primary-key fields, as we illustrate in this example. Persistent subclasses of Part must not have their own application identity class. We define the Part class as follows: package productdesign; public class Part extends Component { private String designer; // other fields protected Part( ) { } public Part(String assemId, String designer) { super(assemId); this.designer = designer; } public String getDesigner( ) { return designer; } // other methods } Here is a portion of the associated PartKey class: package productdesign; public class PartKey extends ComponentKey { static { Part part = new Part( ); } public PartKey(String id) { super(id); } public PartKey( ) { } // other identity methods } The concrete Assembly class that extends Component must declare a concrete application identity class (for example, AssemblyKey) that extends ComponentKey. If Assembly has a assemblyId primary-key field, the assemblyId field must also be declared in AssemblyKey with the same name and type. Here is a part of the Assembly class declaration: package productdesign; import java.util.HashSet; public class Assembly extends Component { private int assemblyId; // primary-key field private HashSet components; private Assembly( ) { } public Assembly(String componentId, int aid) { super(componentId); assemblyId = aid; components = new HashSet( ); } public int getAssemblyId( ) { return assemblyId; } } We define the AssemblyKey class as follows: package productdesign; public class AssemblyKey extends ComponentKey { static { Assembly assembly = new Assembly( ); } public int assemblyId; public AssemblyKey( ) { assemblyId = 0; } public AssemblyKey(String id) { super(id.substring(0, id.indexOf('|'))); assemblyId = 0; try { assemblyId = Integer.parseInt(id.substring(id.indexOf('|')+1)); } catch(Exception e) { } } public AssemblyKey(String master, int id) { super(master); assemblyId = id; } public String toString( ) { return super.toString( ) + "|" + Integer.toString(assemblyId); } public boolean equals(Object obj) { if (!(obj instanceof AssemblyKey)) return false; AssemblyKey assemKey = (AssemblyKey) obj; if (assemblyId != assemKey.assemblyId) return false; return super.equals(assemKey); } public int hashCode( ) { return assemblyId * super.hashCode( ); } } Persistent subclasses of Assembly must not have their own application identity class. There might be other abstract or nonpersistent classes in the inheritance hierarchy between Component and Part, or between Component and Assembly. The application identity classes and primary-key fields ignore these classes. Here is the metadata for these classes: <jdo>
<package name="productdesign" >
<class name="Component" objectid-class="ComponentKey" >
<field name="masterId" primary-key="true" />
</class>
<class name="Part" objectid-class="PartKey"
persistence-capable-superclass="Component"/>
<class name="Assembly" objectid-class="AssemblyKey"
persistence-capable-superclass="Component" >
<field name="assemblyId" primary-key="true" />
<field name="components" >
<collection element-type="Part" /> [1]
</field>
</class>
</package>
</jdo>
There is an interesting modeling issue to consider in the Assembly class. It contains a collection named components. An Assembly abstraction models a set of components that should be treated as a single design unit in a product design. On line [1] in the metadata we declare that components contains Part instances. We may also want to allow an Assembly to contain references to Component instances, which could include references to other Assembly instances. But in the object model we have defined here, Component introduces only a partial primary key. Though the Part class is the first concrete class in its branch of the inheritance hierarchy and it does not add any additional fields to identify a Part instance, the Assembly class does introduce additional fields that are necessary to reference an Assembly instance. Many other classes may extend Component and introduce their own additional primary-key fields. In general, you should not rely on support of partial primary keys to represent references when using application identity (though some implementations may support it). If your model needs support of such references, you should either have the persistent class at the root of the inheritance hierarchy completely define the primary key for its class and all subclasses, or you should use datastore identity, which does not have this issue. |
[ Team LiB ] |