This chapter describes advanced techniques for use in the entity objects in your business domain layer.
This chapter includes the following sections:
Section 38.1, "Creating Custom, Validated Data Types Using Domains"
Section 38.2, "Updating a Deleted Flag Instead of Deleting Rows"
Section 38.5, "Basing an Entity Object on a PL/SQL Package API"
Section 38.6, "Basing an Entity Object on a Join View or Remote DBLink"
Section 38.7, "Using Inheritance in Your Business Domain Layer"
Section 38.8, "Controlling Entity Posting Order to Avoid Constraint Violations"
Note:
To experiment with the examples in this chapter, use theAdvancedEntityExamples
workspace in the StandaloneExamples
module of the Fusion Order Demo application, as described in Section 2.4.4, "Standalone Applications in the AdvancedEntityExamples Application Workspace." For information about how to obtain and install the Fusion Order Demo, see Section 2.2, "Setting Up the Fusion Order Demo Application."When you find yourself repeating the same sanity-checking validations on the values of similar attributes across multiple entity objects, you can save yourself time and effort by creating your own data types that encapsulate this validation. For example, imagine that across your business domain layer there are numerous entity object attributes that store strings that represent email addresses. One technique you could use to ensure that end users always enter a valid email address everywhere one appears in your business domain layer is to:
Use a basic String
data type for each of these attributes
Add an attribute-level method validator with Java code that ensures that the String value has the format of a valid email address for each attribute
However, these approaches can become tedious quickly in a large application. Fortunately, ADF Business Components offers an alternative that allows you to create your own EmailAddress
data type that represents an email address. After centralizing all of the sanity-checking regarding email address values into this new custom data type, you can use the EmailAddress
as the type of every attribute in your application that represents an email address. By doing this, you make the intention of the attribute values more clear to other developers and simplify application maintenance by putting the validation in a single place. ADF Business Components calls these developer-created data types domains.
Domains are Java classes that extend the basic data types like String
, Number
, and Date
to add constructor-time validation to insure the candidate value passes relevant sanity checks. They offer you a way to define custom data types with cross-cutting behavior such as basic data type validation, formatting, and custom metadata properties in a way that are inherited by any entity objects or view objects that use the domain as the Java type of any of their attributes.
Note:
The example in this section refers to theSimpleDomains
project in the AdvancedEntityExamples
application workspace in the StandaloneExamples
module of the Fusion Order Demo application.To create a domain, use the Create Domain wizard. This is available in the New Gallery in the ADF Business Components category.
In the Application Navigator, right-click the project for which you want to create a domain and choose New.
In the New Gallery, expand Business Tier, select ADF Business Components, and then select Domain and click OK.
This launches the Create Domain wizard.
On the Name page, specify a name for the domain and a package in which it will reside. To create a domain based on a simple Java type, leave Domain for an Oracle Object Type unchecked.
Click Next.
On the Settings page, indicate the base type for the domain and the database column type to which it will map. For example, if you were creating a domain called ShortEmailAddress
to hold eight-character short email addresses, you would set the base type to String
and the Database Column Type to VARCHAR2(8)
. You can set other common attribute settings on this panel as well.
Click Finish to create your domain.
When you create a domain, JDeveloper creates its XML component definition in the subdirectory of your project's source path that corresponds to the package name you chose. For example, if you created the ShortEmailAddress
domain in the devguide.advanced.domains
package, JDeveloper would create the ShortEmailAddress.xml
file in the ./devguide/advanced/domains
subdirectory. A domain always has a corresponding Java class, which JDeveloper creates in the common
subpackage of the package where the domain resides. This means it would create the ShortEmailAddress.java
class in the devguide.advanced.domains.common
package. The domain's Java class is automatically generated with the appropriate code to behave in a way that is identical to one of the built-in data types.
The sections that follow describe some of the things you may need to know about when working with domains.
After you've created a domain in a project, it appears among the list of available data types in the Attribute Type dropdown list in the entity object and view object wizards and dialogs. To use the domain as the type of a given attribute, just pick it from the list.
Note:
The entity-mapped attributes in an entity-based view object inherit their data type from their corresponding underlying entity object attribute, so if the entity attribute uses a domain type, so will the matching view object attribute. For transient or SQL-derived view object attributes, you can directly set the type to use a domain since it is not inherited from any underlying entity.Typically, the only coding task you need to do for a domain is to write custom code inside the generated validate()
method. Your implementation of the validate()
method should perform your sanity checks on the candidate value being constructed, and throw a DataCreationException
in the oracle.jbo
package if the validation fails.
In order to throw an exception message that is translatable, you can create a message bundle class similar to the one shown in Example 38-1. Create it in the same common
package as your domain classes themselves. The message bundle returns an array of {
MessageKeyString
,
TranslatableMessageString
}
pairs.
Example 38-1 Custom Message Bundle Class for Domain Exception Messages
package devguide.advanced.simpledomains.common; import java.util.ListResourceBundle; public class ErrorMessages extends ListResourceBundle { public static final String INVALID_SHORTEMAIL = "30002"; public static final String INVALID_EVENNUMBER = "30003"; private static final Object[][] sMessageStrings = new String[][] { { INVALID_SHORTEMAIL, "A valid short email address has no @-sign or dot."}, { INVALID_EVENNUMBER, "Number must be even."} }; /** * Return String Identifiers and corresponding Messages * in a two-dimensional array. */ protected Object[][] getContents() { return sMessageStrings; } }
Since String
is a base JDK type, a domain based on a String
aggregates a private mData String
member field to hold the value that the domain represents. Then, the class implements the DomainInterface
expected by the ADF runtime, as well as the Serializable
interface, so the domain can be used in method arguments or returns types of ADF components custom client interfaces.
Example 38-2 shows the validate()
method for a simple ShortEmailAddress
domain class. It tests to make sure that the mData
value does not contains an at-sign or a dot, and if it does, then the method throws DataCreationException
referencing an appropriate message bundle and message key for the translatable error message.
Example 38-2 Simple ShortEmailAddress String-Based Domain Type with Custom Validation
public class ShortEmailAddress extends Object implements DomainInterface, Serializable { private String mData; // . . . /**Implements domain validation logic and throws a JboException on error. */ protected void validate() { int atpos = mData.indexOf('@'); int dotpos = mData.lastIndexOf('.'); if (atpos > -1 || dotpos > -1) { throw new DataCreationException(ErrorMessages.class, ErrorMessages.INVALID_SHORTEMAIL,null,null); } } // . . . }
Other simple domains based on a built-in type in the oracle.jbo.domain
package extend the base type as shown in Example 38-3. It illustrates the validate()
method for a simple Number-based domain called EvenNumber
that represents even numbers.
Example 38-3 Simple EvenNumber Number-Based Domain Type with Custom Validation
public class EvenNumber extends Number { // . . . /** * Validates that value is an even number, or else * throws a DataCreationException with a custom * error message. */ protected void validate() { if (getValue() % 2 == 1) { throw new DataCreationException(ErrorMessages.class, ErrorMessages.INVALID_EVENNUMBER,null,null); } } // . . . }
When you create a simple domain based on one of the basic data types, it is an immutable class. This just means that once you've constructed a new instance of it like this:
ShortEmailAddress email = new ShortEmailAddress("emailaddress1");
You cannot change its value. If you want to reference a different short email address, you just construct another one:
ShortEmailAddress email = new ShortEmailAddress("emailaddress2");
This is not a new concept since it's the same way that String
, Number
, and Date
classes behave, among others.
The Oracle database supports the ability to create user-defined types in the database. For example, you could create a type called POINT_TYPE
using the following DDL statement:
create type point_type as object ( x_coord number, y_coord number );
If you use user-defined types like POINT_TYPE
, you can create domains based on them, or you can reverse-engineer tables containing columns of object type to have JDeveloper create the domain for you.
To create a domain yourself, perform the following steps in the Create Domain wizard:
In the Application Navigator, right-click the project for which you want to create a domain and choose New.
In the New Gallery, expand Business Tier, select ADF Business Components, and then select Domain and click OK.
This launches the Create Domain wizard.
On the Name page, check the Domain for an Oracle Object Type checkbox, then select the object type for which you want to create a domain from the Available Types list.
Click Next.
On the Settings page, use the Attribute dropdown list to switch between the multiple domain properties to adjust the settings as appropriate.
Click Finish to create the domain.
In addition to manually creating object type domains, when you use the Business Components from Tables wizard and select a table containing columns of an Oracle object type, JDeveloper creates domains for those object types as part of the reverse-engineering process. For example, imagine you created a table like this with a column of type POINT_TYPE
:
create table interesting_points( id number primary key, coordinates point_type, description varchar2(20) );
If you create an entity object for the INTERESTING_POINTS
table in the Business Components from Tables wizard, then you will get both an InterestingPoints
entity object and a PointType domain. The latter will have been automatically created, based on the POINT_TYPE
object type, since it was required as the data type of the Coordinates
attribute of the InterestingPoints
entity object.
Unlike simple domains, object type domains are mutable. JDeveloper generates getter and setter methods into the domain class for each of the elements in the object type's structure. After changing any domain properties, when you set that domain as the value of a view object or entity object attribute, it is treated as a single unit. ADF does not track which domain properties have changed, only that a domain-valued attribute value has changed.
Note:
Domains based on Oracle object types are useful for working programmatically with data whose underlying type is an oracle object type. They also can simplify passing and receiving structure information to stored procedures. However, support for working with object type domains in the ADF binding layer is complete, so it's not straightforward to use object domain-valued attributes in declaratively-databound user interfaces.After selecting a domain in the Application Navigator, you can quickly navigate to its implementation class using one of the following methods:
In the Application Navigator, right-click the domain and choose Go to Domain Class from the context menu.
In the Structure window, double-click the domain class.
When you create a business components archive, as described in Section 37.7, "Working with Libraries of Reusable Business Components," the domain classes and message bundle files in the *.common subdirectories of your project's source path get packaged into the *CSCommon.jar
. They are classes that are common to both the middle-tier application server and to an eventual remote-client you might need to support.
You can define custom metadata properties on a domain. Any entity object or view object attribute based on that domain inherits those custom properties as if they had been defined on the attribute itself. If the entity object or view object attribute defines the same custom property, its setting takes precedence over the value inherited from the domain.
JDeveloper enforces the declarative settings you impose at the domain definition level; they cannot be made less restrictive for the entity object or view object for an attribute based on the domain type. For example, if you define a domain to have its Updatable property set to While New, then when you use your domain as the Java type of an entity object attribute, you can set Updatable to be Never (more restrictive) but you cannot set it to be Always. Similarly, if you define a domain to be Persistent, you cannot make it transient later. When sensible for your application, set declarative properties for a domain to be as lenient as possible so you can later make them more restrictive as needed.
For auditing purposes, once a row is added to a table, sometimes your requirements may demand that rows are never physically deleted from the table. Instead, when the end user deletes the row in the user interface, the value of a DELETED
column should be updated from "N
" to "Y
" to mark it as deleted. You can use two method overrides to alter an entity object's default behavior to achieve this effect. For example, say you want to change the Products
entity from the Fusion Order Demo application to behave in this way.
To update a deleted flag when a row is removed, enable a custom Java class for your entity object and override the remove()
method to set the deleted flag before calling the super.remove()
method. Example 38-4 shows what this would look like in the custom Java class of an entity object. It is important to set the attribute before calling super.remove()
since an attempt to set the attribute of a deleted row will encounter the DeadEntityAccessException
.
This example presumes that you've altered the PRODUCTS
table to have an additional DELETED
column, and synchronized the Products
entity with the database to add the corresponding Deleted
attribute.
Example 38-4 Updating a Deleted Flag When a Products Entity Row Is Removed
// In your custom Java entity class public void remove() { setDeleted("Y"); super.remove(); }
The row will still be removed from the row set, but it will have the value of its Deleted
flag modified to "Y
" in the entity cache. The second part of implementing this behavior involves forcing the entity to perform an UPDATE
instead of an DELETE
when it is asked to perform its DML operation. You need to implement both parts for a complete solution.
To force an entity object to be updated instead of deleted, override the doDML()
method and write code that conditionally changes the operation
flag. When the operation flag equals DML_DELETE
, your code will change it to DML_UPDATE
instead. Example 38-5 shows what this would look like in the custom Java class of an entity object.
This example presumes that you've altered the PRODUCTS
table to have an additional DELETED
column, and synchronized the Products
entity with the database to add the corresponding Deleted
attribute.
Example 38-5 Forcing an Update DML Operation Instead of a Delete
// In your custom Java entity class protected void doDML(int operation, TransactionEvent e) { if (operation == DML_DELETE) { operation = DML_UPDATE; } super.doDML(operation, e); }
With this overridden doDML()
method in place to complement the overridden remove()
method described in Section 38.2.1, any attempt to remove an entity through any view object with a corresponding entity usage will update the DELETED
column instead of physically deleting the row. Of course, in order to prevent "deleted" products from appearing in your view object query results, you will need to appropriately modify their WHERE
clauses to include only products WHERE DELETED = 'N'
.
You can use update batching to reduce the number of DML statements issued with multiple entity modifications.
By default, the ADF Business Components framework performs a single DML statement (INSERT
, UPDATE
, DELETE
) for each modified entity of a given entity definition type. For example, say you have an Employee entity object type for which multiple instances are modified during typical use of the application. If two instances were created, three existing instances modified, and four existing instances deleted, then at transaction commit time the framework issues nine DML statements (2 INSERT
s, 3 UPDATE
s, and 4 DELETE
s) to save these changes.
If you will frequently be updating more than one entity of a given type in a transaction, consider using the update batching feature for that entity definition type. In the example, update batching (with a threshold of 1) causes the framework to issue just three DML statements: one bulk INSERT
statement processing two inserts, one bulk UPDATE
statement processing three updates, and one bulk DELETE
statement processing four deletes.
Note:
If the entity object has any attributes that are set to Refresh After Insert or Refresh After Update, then the batch update feature is disabled.To enable update batching for an entity
Open the appropriate entity object in the overview editor.
In the Application Navigator, double-click the appropriate entity to open it in the overview editor.
On the General page of the overview editor, expand the Tuning section, select the Use Update Batching checkbox, and specify the appropriate threshold.
This establishes a batch processing threshold beyond which ADF will process the modifications in a bulk DML operation.
This section describes several advanced techniques for working with associations between entity objects.
When you need to represent a more complex relationship between entities than one based only on the equality of matching attributes, you can modify the association's SQL clause to include more complex criteria. For example, sometimes the relationship between two entities depends on effective dates. A Product
may be related to a Supplier
, however if the name of the supplier changes over time, each row in the SUPPLIERS
table might include additional EFFECTIVE_FROM
and EFFECTIVE_UNTIL
columns that track the range of dates in which that product row is (or was) in use. The relationship between a Product
and the Supplier
with which it is associated might then be described by a combination of the matching SupplierId
attributes and a condition that the product's RequestDate
lie between the supplier's EffectiveFrom
and EffectiveUntil
dates.
You can setup this more complex relationship in the overview editor for the association. First add any additional necessary attribute pairs on the Relationship page, which in this example would include one (EffectiveFrom
, RequestDate
) pair and one (EffectiveUntil
, RequestDate
) pair. Then on the Query page you can edit the Where field to change the WHERE clause to be:
(:Bind_SupplierId = Product.SUPPLIER_ID) AND (Product.REQUEST_DATE BETWEEN :Bind_EffectiveFrom AND :Bind_EffectiveUntil)
For more information about creating associations, see Section 4.3, "Creating and Configuring Associations."
When you create a view link between two entity-based view objects, on the View Link Properties page, you have the option to expose view link accessor attributes both at the view object level as well as at the entity object level. By default, a view link accessor is only exposed at the view object level of the destination view object. By checking the appropriate In Entity Object: SourceEntityName or In Entity Object:DestinationEntityName checkbox, you can opt to have JDeveloper include a view link attribute in either or both of the source or destination entity objects. This can provide a handy way for an entity object to access a related set of related view rows, especially when the query to produce the rows only depends on attributes of the current row.
Each time you retrieve an entity association accessor row set, by default the entity object creates a new RowSet
object to allow you to work with the rows. This does not imply re-executing the query to produce the results each time, only creating a new instance of a RowSet
object with its default iterator reset to the "slot" before the first row. To force the row set to refresh its rows from the database, you can call its executeQuery()
method.
Since there is a small amount of overhead associated with creating the row set, if your code makes numerous calls to the same association accessor attributes, you can consider enabling the association accessor row set retention for the source entity object in the association. You can enable retention of the association accessor row set using the overview editor for the entity object that is the source for the association accessor. Select Retain Association Accessor Rowset in the Tuning section of the General page of the overview editor for the entity object.
Alternatively, you can enable a custom Java entity collection class for your entity object. As with other custom entity Java classes you've seen, you do this on the Select Java Options dialog that you open from the Java page of the overview editor for the entity object. In the dialog, select Generate Entity Collection Class. Then, in the YourEntityColl
Impl
class that JDeveloper creates for you, override the init()
method, and add a line after super.init()
that calls the setAssociationAccessorRetained()
method passing true
as the parameter. It affects all association accessor attributes for that entity object.
When this feature is enabled for an entity object, since the association accessor row set it not recreated each time, the current row of its default row set iterator is also retained as a side-effect. This means that your code will need to explicitly call the reset()
method on the row set you retrieve from the association accessor to reset the current row in its default row set iterator back to the "slot" before the first row.
Note, however, that with accessor retention enabled, your failure to call reset()
each time before you iterate through the rows in the accessor row set can result in a subtle, hard-to-detect error in your application. For example, say you iterate over the rows in an association accessor row set as shown in Example 38-6 to calculate some aggregate total.
Example 38-6 Iterating Over a Row Set Incorrectly
// In your custom Java entity classRowSet rs = (RowSet)getProducts(); while (rs.hasNext()) { ProductImpl r = (ProductImpl)rs.next(); // Do something important with attributes in each row }
The first time you work with the accessor row set, the code will work. However, since the row set (and its default row set iterator) are retained, the second and any subsequent time you access the row set, the current row will already be at the end of the row set and the while loop will be skipped because rs.hasNext()
will be false
. Instead, with this feature enabled, write your accessor iteration code as shown in Example 38-7.
Example 38-7 Iterating Over a Row Set and Resetting to the First Row
// In your custom Java entity class
RowSet rs = (RowSet)getProducts();
rs.reset(); // Reset default row set iterator to slot before first row!
while (rs.hasNext()) {
ProductImpl r = (ProductImpl)rs.next();
// Do something important with attributes in each row
}
If you have a PL/SQL package that encapsulates insert, update, and delete access to an underlying table, you can override the default DML processing event for the entity object that represents that table to invoke the procedures in your PL/SQL API instead. Often, such PL/SQL packages are used in combination with a companion database view. Client programs read data from the underlying table using the database view, and "write" data back to the table using the procedures in the PL/SQL package.
For example, say you want to create a Product
entity object based on such a combination of a view and a package.
Given the PRODUCTS
table in the Fusion Order Demo schema, consider a database view named PRODUCTS_V
, created using the following DDL statement:
create or replace view products_v as select product_id,name,image,description from products;
In addition, consider the simple PRODUCTS_API
package shown in Example 38-8 that encapsulates insert, update, and delete access to the underlying PRODUCTS
table.
Example 38-8 Simple PL/SQL Package API for the PRODUCTS Table
create or replace package products_api is procedure insert_product(p_prod_id number, p_name varchar2, p_supplier_id number, p_list_price number, p_min_price number, p_ship_code varchar2); procedure update_product(p_prod_id number, p_name varchar2, p_supplier_id number, p_list_price number, p_min_price number, p_ship_code varchar2); procedure delete_product(p_prod_id number); end products_api;
To create an entity object based on this combination of database view and PL/SQL package, you would perform the following tasks:
Create a view-based entity object, as described in Section 38.5.1, "How to Create an Entity Object Based on a View."
Create a base class for the entity, as described in Section 38.5.3, "Centralizing Details for PL/SQL-Based Entities into a Base Class."
Implement the appropriate stored procedure calls, as described in Section 38.5.4, "Implementing the Stored Procedure Calls for DML Operations."
Handle selecting and locking functionality, if necessary, as described in Section 38.5.5, "Adding Select and Lock Handling."
Note:
The example in these sections refers to theEntityWrappingPLSQLPackage
project of the AdvancedEntityExamples
application workspace in the StandaloneExamples
module of the Fusion Order Demo application.To create an entity object based on a view, you use the Create Entity Object wizard as described in Section 4.2.2, "How to Create Single Entity Objects Using the Create Entity Wizard," with the following exceptions:
On the Name page, give the entity a name like Product
and check the Views checkbox at the bottom of the Database Objects section.
This enables the display of the available database views in the current schema in the Schema Object list.
Select the desired database view in the Schema Object list.
On the Attribute Settings page, use the Select Attribute dropdown list to choose the attribute that will act as the primary key, then enable the Primary Key setting for that property.
Note:
When defining the entity based on a view, JDeveloper cannot automatically determine the primary key attribute since database views do not have related constraints in the database data dictionary.By default, an entity object based on a view performs all of the following directly against the underlying database view:
SELECT
statement (for findByPrimaryKey()
)
SELECT FOR UPDATE
statement (for lock()
), and
INSERT
, UPDATE
, DELETE
statements (for doDML()
)
To use stored procedure calls, you will need to override the doDML()
operations (as described in Section 38.5.3, "Centralizing Details for PL/SQL-Based Entities into a Base Class"), and possibly override the lock()
and findByPrimaryKey()
handling (as described in Section 38.5.4, "Implementing the Stored Procedure Calls for DML Operations").
If you plan to have more than one entity object based on a PL/SQL API, it's a smart idea to abstract the generic details into a base framework extension class. In doing this, you'll be using several of the concepts described in Chapter 37, "Advanced Business Components Techniques." Start by creating a PLSQLEntityImpl
class that extends the base EntityImpl
class that each one of your PL/SQL-based entities can use as their base class. As shown in Example 38-9, you'll override the doDML()
method of the base class to invoke a different helper method based on the operation.
Note:
If you are already using an extended entity implementation class for your entity, you can extend it further with thePLSQLEntityImpl
class. For example, if you have a framework extension class named zzEntityImpl
, you would create a PLSQLEntityImpl
class that extends the zzEntityImpl
class.Example 38-9 Overriding doDML() to Call Different Procedures Based on the Operation
// In PLSQLEntityImpl.java protected void doDML(int operation, TransactionEvent e) { // super.doDML(operation, e); if (operation == DML_INSERT) callInsertProcedure(e); else if (operation == DML_UPDATE) callUpdateProcedure(e); else if (operation == DML_DELETE) callDeleteProcedure(e); }
In the PLSQLEntityImpl.java
base class, you can write the helper methods so that they perform the default processing like this:
// In PLSQLEntityImpl.java /* Override in a subclass to perform non-default processing */ protected void callInsertProcedure(TransactionEvent e) { super.doDML(DML_INSERT, e); } /* Override in a subclass to perform non-default processing */ protected void callUpdateProcedure(TransactionEvent e) { super.doDML(DML_UPDATE, e); } /* Override in a subclass to perform non-default processing */ protected void callDeleteProcedure(TransactionEvent e) { super.doDML(DML_DELETE, e); }
After putting this infrastructure in place, when you base an entity object on the PLSQLEntityImpl
class, you can use the Source > Override Methods menu item to override the callInsertProcedure()
, callUpdateProcedure()
, and callDeleteProcedure()
helper methods and perform the appropriate stored procedure calls for that particular entity.
Note:
If you do not override these helper methods in a subclass, they will perform the default processing as defined in the superclass. You only need to override the operations in thedoDML()
method that you want to provide alternative processing for.To simplify the task of implementing these calls, you could add the callStoredProcedure()
helper method (described in Chapter 37, "Invoking Stored Procedures and Functions") to the PLSQLEntityImpl
class as well. This way, any PL/SQL-based entity objects that extend this class can leverage the helper method.
To implement the stored procedure calls for DML operations, you will need to create a custom Java class for the entity object and override the operations in it.
To create the custom Java class with the override methods:
In the Application Navigator, double-click the Product
entity object to open it in the overview editor.
On the Java page of the overview editor, click the Edit Java options icon.
In the Select Java Options dialog, click Classes Extend.
In the Override Base Classes dialog, in the Row field, enter the package and class of the PLSQLEntityImpl
class, or click Browse to search and select it.
Then select Generate Entity Object Class, and click OK.
In the Application Navigator, double-click ProductsImpl.java
to open it in the overview editor.
From the Source menu, choose Override Methods.
In the Override Methods dialog, select the callInsertProcedure()
, callUpdateProcedure()
, and callDeleteProcedure()
methods, and click OK.
Then enter the necessary code to override these procedures.
Example 38-10 shows the code you would write in these overridden helper methods.
Example 38-10 Leveraging a Helper Method to Invoke Insert, Update, and Delete Procedures
// In ProductsImpl.java protected void callInsertProcedure(TransactionEvent e) { callStoredProcedure("products_api.insert_product(?,?,?,?,?,?)", new Object[] { getProductId(), getProductName(), getSupplierId(), getListPrice(), getMinPrice(), getShippingClassCode() }); } protected void callUpdateProcedure(TransactionEvent e) { callStoredProcedure("products_api.update_product(?,?,?,?,?,?)", new Object[] { getProductId(), getProductName(), getSupplierId(), getListPrice(), getMinPrice(), getShippingClassCode() }); } protected void callDeleteProcedure(TransactionEvent e) { callStoredProcedure("products_api.delete_product(?)", new Object[] { getProductId() }); }
At this point, if you create a default entity-based view object called Products
for the Products
entity object and add an instance of it to a ProductsModule
application module you can quickly test inserting, updating, and deleting rows from the Products
view object instance in the Business Component Browser.
Often, overriding just the insert, update, and delete operations will be enough. The default behavior that performs the SELECT
statement for findByPrimaryKey()
and the SELECT FOR UPDATE
statement for the lock()
against the database view works for most basic kinds of views.
However, if the view is complex and does not support SELECT FOR UPDATE
or if you need to perform the findByPrimaryKey()
and lock()
functionality using additional stored procedures API's, then you can use the technique described in Section 38.5.5, "Adding Select and Lock Handling.".
You can handle the lock()
and findByPrimaryKey()
functionality of an entity object by invoking stored procedures if necessary. Imagine that the PRODUCTS_API
package were updated to contain the two additional procedures shown in Example 38-11. Both the lock_product
and select_product
procedures accept a primary key attribute as an IN
parameter and return values for the remaining attributes using OUT
parameters.
Example 38-11 Additional Locking and Select Procedures for the PRODUCTS Table
/* Added to PRODUCTS_API package */ procedure lock_product(p_prod_id number, p_name OUT varchar2, p_supplier_id OUT number, p_list_price OUT number, p_min_price OUT number, p_ship_code OUT varchar2); procedure select_product(p_prod_id number, p_name OUT varchar2, p_supplier_id OUT number, p_list_price OUT number, p_min_price OUT number, p_ship_code OUT varchar2);
You can extend the PLSQLEntityImpl
base class to handle the lock()
and findByPrimaryKey()
overrides using helper methods similar to the ones you added for insert, update, delete. At runtime, both the lock()
and findByPrimaryKey()
operations end up invoking the lower-level entity object method called doSelect(boolean lock)
. The lock()
operation calls doSelect()
with a true
value for the parameter, while the findByPrimaryKey()
operation calls it passing false
instead.
Example 38-12 shows the overridden doSelect()
method in PLSQLEntityImpl
to delegate as appropriate to two helper methods that subclasses can override as necessary.
Example 38-12 Overriding doSelect() to Call Different Procedures Based on the Lock Parameter
// In PLSQLEntityImpl.java protected void doSelect(boolean lock) { if (lock) { callLockProcedureAndCheckForRowInconsistency(); } else { callSelectProcedure(); } }
The two helper methods are written to just perform the default functionality in the base PLSQLEntityImpl
class:
// In PLSQLEntityImpl.java /* Override in a subclass to perform non-default processing */ protected void callLockProcedureAndCheckForRowInconsistency() { super.doSelect(true); } /* Override in a subclass to perform non-default processing */ protected void callSelectProcedure() { super.doSelect(false); }
Notice that the helper method that performs locking has the name callLockProcedureAndCheckForRowInconsistency()
. This reminds developers that it is their responsibility to perform a check to detect at the time of locking the row whether the newly-selected row values are the same as the ones the entity object in the entity cache believes are the current database values.
To assist subclasses in performing this old-value versus new-value attribute comparison, you can add one final helper method to the PLSQLEntityImpl
class like this:
// In PLSQLEntityImpl protected void compareOldAttrTo(int attrIndex, Object newVal) { if ((getPostedAttribute(attrIndex) == null && newVal != null) || (getPostedAttribute(attrIndex) != null && newVal == null) || (getPostedAttribute(attrIndex) != null && newVal != null && !getPostedAttribute(attrIndex).equals(newVal))) { throw new RowInconsistentException(getKey()); } }
With the additional infrastructure in place in the base PLSQLEntityImpl
class, you can override the callSelectProcedure()
and callLockProcedureAndCheckForRowInconsistency()
helper methods in the Product
entity object's ProductImpl
class. Since the select_product
and lock_product
procedures have OUT
arguments, as described in Section 37.5.4, "How to Call Other Types of Stored Procedures," you need to use a JDBC CallableStatement
object to perform these invocations.
Example 38-13 shows the code required to invoke the select_product
procedure. It's performing the following basic steps:
Creating a CallableStatement
for the PLSQL block to invoke.
Registering the OUT
parameters and types, by one-based bind variable position.
Setting the IN
parameter value.
Executing the statement.
Retrieving the possibly updated column values.
Populating the possibly updated attribute values in the row.
Closing the statement.
Example 38-13 Invoking the Stored Procedure to Select a Row by Primary Key
// In ProductsImpl.java protected void callSelectProcedure() { String stmt = "begin products_api.select_product(?,?,?,?,?,?);end;"; // 1. Create a CallableStatement for the PLSQL block to invoke CallableStatement st = getDBTransaction().createCallableStatement(stmt, 0); try { // 2. Register the OUT parameters and types st.registerOutParameter(2, VARCHAR2); st.registerOutParameter(3, NUMBER); st.registerOutParameter(4, NUMBER); st.registerOutParameter(5, NUMBER); st.registerOutParameter(6, VARCHAR2); // 3. Set the IN parameter value st.setObject(1, getProductId()); // 4. Execute the statement st.executeUpdate(); // 5. Retrieve the possibly updated column values String possiblyUpdatedName = st.getString(2); String possiblyUpdatedSupplierId = st.getString(3); String possiblyUpdatedListPrice = st.getString(4); String possiblyUpdatedMinPrice = st.getString(5); String possiblyUpdatedShipCode = st.getString(6); // 6. Populate the possibly updated attribute values in the row populateAttribute(PRODUCTNAME, possiblyUpdatedName, true, false, false); populateAttribute(SUPPLIERID, possiblyUpdatedSupplierId, true, false, false); populateAttribute(LISTPRICE, possiblyUpdatedListPrice, true, false, false); populateAttribute(MINPRICE, possiblyUpdatedMinPrice, true, false, false); populateAttribute(SHIPPINGCLASSCODE, possiblyUpdatedShipCode, true, false, false); } catch (SQLException e) { throw new JboException(e); } finally { if (st != null) { try { // 7. Closing the statement st.close(); } catch (SQLException e) { } } } }
Example 38-14 shows the code to invoke the lock_product
procedure. It's doing basically the same steps as those in Example 38-13, with just the following two interesting differences:
After retrieving the possibly updated column values from the OUT
parameters, it uses the compareOldAttrTo()
helper method inherited from the PLSQLEntityImpl
to detect whether or not a RowInconsistentException
should be thrown as a result of the row lock attempt.
In the catch (SQLException e)
block, it is testing to see whether the database has thrown the error:
ORA-00054: resource busy and acquire with NOWAIT specified
and if so, it again throws the ADF Business Components AlreadyLockedException
just as the default entity object implementation of the lock()
functionality would do in this situation.
Example 38-14 Invoking the Stored Procedure to Lock a Row by Primary Key
// In ProductsImpl.java protected void callLockProcedureAndCheckForRowInconsistency() { String stmt = "begin products_api.lock_product(?,?,?,?,?,?);end;"; CallableStatement st = getDBTransaction().createCallableStatement(stmt, 0); try { st.registerOutParameter(2, VARCHAR2); st.registerOutParameter(3, NUMBER); st.registerOutParameter(4, NUMBER); st.registerOutParameter(5, NUMBER); st.registerOutParameter(6, VARCHAR2); st.setObject(1, getProductId()); st.executeUpdate(); String possiblyUpdatedName = st.getString(2); String possiblyUpdatedSupplierId = st.getString(3); String possiblyUpdatedListPrice = st.getString(4); String possiblyUpdatedMinPrice = st.getString(5); String possiblyUpdatedShipCode = st.getString(6); compareOldAttrTo(PRODUCTNAME, possiblyUpdatedName); compareOldAttrTo(SUPPLIERID, possiblyUpdatedSupplierId); compareOldAttrTo(LISTPRICE, possiblyUpdatedListPrice); compareOldAttrTo(MINPRICE, possiblyUpdatedMinPrice); compareOldAttrTo(SHIPPINGCLASSCODE, possiblyUpdatedShipCode); } catch (SQLException e) { if (Math.abs(e.getErrorCode()) == 54) { throw new AlreadyLockedException(e); } else { throw new JboException(e); } } finally { if (st != null) { try { st.close(); } catch (SQLException e) { } } } }
With these methods in place, you have a Products
entity object that wraps the PRODUCTS_API
package for all of its database operations. Due to the clean separation of the data querying functionality of view objects and the data validation and saving functionality of entity objects, you can now leverage this Products
entity object in any way you would use a normal entity object. You can build as many different view objects that use Products
as their entity usage as necessary.
You can override the lock()
method to refresh the entity object after a RowInconsistentException
has occurred. Example 38-15 shows code that can be added to the entity object implementation class to catch the RowInconsistentException and refresh the entity object.
If you need to create an entity object based on either of the following:
Synonym that resolves to a remote table over a DBLINK
View with INSTEAD OF
triggers
Then you will encounter the following error if any of its attributes are marked as Refresh on Insert or Refresh on Update:
JBO-26041: Failed to post data to database during "Update" ## Detail 0 ## ORA-22816: unsupported feature with RETURNING clause
These types of schema objects do not support the RETURNING
clause, which by default the entity object uses to more efficiently return the refreshed values in the same database roundtrip in which the INSERT
or UPDATE
operation was executed.
To disable the use of the RETURNING clause for an entity object of this type:
Enable a custom entity definition class for the entity object.
In the custom entity definition class, override the createDef()
method to call:
setUseReturningClause(false)
If the Refresh on Insert attribute is the primary key of the entity object, you must specify some other attribute in the entity as an alternate unique key by setting the Unique Key property on it.
At runtime, when you have disabled the use of the RETURNING
clause in this way, the entity object implements the Refresh on Insert and Refresh on Update behavior using a separate SELECT
statement to retrieve the values to refresh after insert or update as appropriate.
Inheritance is a powerful feature of object-oriented development that can simplify development and maintenance when used appropriately. As shown in Section 37.9, "Creating Extended Components Using Inheritance," ADF Business Components supports using inheritance to create new components that extend existing ones in order to add additional properties or behavior or modify the behavior of the parent component. Inheritance can be useful in modeling the different kinds of entities in your reusable business domain layer.
Note:
The example in this section refers to theInheritanceAndPolymorphicQueries
project of the AdvancedEntityExamples
application workspace in the StandaloneExamples
module of the Fusion Order Demo application.Your application's database schema might contain tables where different logical kinds of business information are stored in rows of the same table. These tables will typically have one column whose value determines the kind of information stored in each row. For example, the Fusion Order Demo's PERSONS
table stores information about customers, suppliers, and staff in the same table. It contains a PERSON_TYPE_CODE
column whose value — STAFF
, CUST, or SUPP — determines what kind of PERSON
the row represents.
While the Fusion Order Demo implementation doesn't yet contain this differentiation in this release, it's reasonable to assume that a future release of the application might require:
Managing additional database-backed attributes that are specific to suppliers or specific to staff
Implementing common behavior for all users that is different for suppliers or staff
Implementing new functionality that is specific to only suppliers or only staff
Figure 38-1 shows what the business domain layer would look like if you created distinct Persons
, Staff
, and Supplier
entity objects to allow distinguishing the different kinds of business information in a more formal way inside your application. Since suppliers and staff are special kinds of persons, their corresponding entity objects would extend the base Persons
entity object. This base Persons
entity object contains all of the attributes and methods that are common to all types of users. The performPersonFunction()
method in the figure represents one of these common methods.
Then, for the Supplier
and Staff
entity objects you can add specific additional attributes and methods that are unique to that kind of user. For example, Supplier
has an additional ContractExpires
attribute of type Date
to track when the supplier's current contract expires. There is also a performSupplierFunction()
method that is specific to suppliers. Similarly, the Staff
entity object has an additional DiscountEligible
attribute to track whether the person qualifies for a staff discount. The performStaffFunction()
is a method that is specific to staff.
By modeling these different kinds of persons as distinct entity objects in an inheritance hierarchy in your domain business layer, you can simplify having them share common data and behavior and implement the aspects of the application that make them distinct.
To create entity objects in an inheritance hierarchy, you use the Create Entity Object wizard to create each entity. The example described here assumes that you've altered the FOD application's PERSONS
table by executing the following DDL statement to add two new columns to it:
alter table persons add ( discount_eligible varchar2(1), contract_expires date );
Before creating entity objects in an inheritance hierarchy based on a table containing different kinds of information, you should first identify which column in the table is used to distinguish the kind of row it is. In the FOD application's PERSONS
table, this is the PERSON_TYPE_CODE
column. Since it helps partition or "discriminate" the rows in the table into separate groups, this column is known as the discriminator column.
Next, determine the valid values that the descriminator column takes on in your table. You might know this off the top of your head, or you could execute a simple SQL statement in the JDeveloper SQL Worksheet to determine the answer. To access the worksheet:
Choose Database Navigator from the View menu.
Expand the AdvancedEntityExamples folder and select the FOD
connection.
Right-click FOD, and choose Open SQL Worksheet from the context menu.
Figure 38-2 shows the results of performing a SELECT DISTINCT
query in the SQL Worksheet on the PERSON_TYPE_CODE
column in the PERSONS
table. It confirms that the rows are partitioned into three groups based on the PERSON_TYPE_CODE
discriminator values: SUPP
, STAFF
, and CUST
.
Once you know how many different kinds of business entities are stored in the table, you will also know how many entity objects to create to model these distinct items. You'll typically create one entity object per kind of item. Next, in order to help determine which entity should act as the base of the hierarchy, you need to determine which subset of attributes is relevant to each kind of item.
For example, assume you determine that all of the attributes except ContractExpires
and DiscountEligible
are relevant to all users, that ContractExpires
is specific to suppliers, and that DiscountEligible
is specific to staff. This information leads you to determine that the Persons
entity object should be the base of the hierarchy, with the Supplier
and Staff
entity objects each extending Persons
to add their specific attributes.
To create the base entity object in an inheritance hierarchy, use the Create Entity Object wizard.
To create the base entity object
In the Application Navigator, right-click the project you want to add the entity object to, and choose New.
In the New Gallery, expand Business Tier, click ADF Business Components, select Entity Object, and click OK.
In the Create Entity Object wizard, on the Name Page, provide a name and package for the entity, and select the schema object on which the entity will be based.
For example, name the entity object Persons and base it on the PERSONS
table.
On the Attributes page, select the attributes in the Entity Attributes list that are not relevant to the base entity object (if any) and click Remove to remove them.
For example, remove the DiscountEligible
and ContractExpires
attributes from the list.
On the Attribute Settings page, use the Select Attribute dropdown list to choose the attribute that will act as the discriminator for the family of inherited entity objects and check the Discriminator checkbox to identify it as such. Importantly, you must also supply a Default Value for this discriminator attribute to identify rows of this base entity type.
For example, select the PersonTypeCode
attribute, mark it as a discriminator attribute, and set its Default Value to the value "cust
".
Note:
Leaving the Default Value blank for a discriminator attribute is legal. A blank default value means that a row with the discriminator column valueIS NULL
will be treated as this base entity type.Then click Finish to create the entity object.
To create a subtype entity object in an inheritance hierarchy, you use the Create Entity Object wizard.
Determine the entity object that will be the parent entity object from which your new entity object will extend.
For example, the parent entity for a new Manager
entity object will be the User
entity.
Ensure that the parent entity has a discriminator attribute already identified.
If it does not, use the overview editor to set the Discriminator property on the appropriate attribute of the parent entity before creating the inherited child.
To create the new subtype entity object in the hierarchy:
In the Application Navigator, right-click the project you want to add the entity object to, and choose New.
In the New Gallery, expand Business Tier, click ADF Business Components, select Entity Object, and click OK.
In the Create Entity Object wizard, on the Name Page, provide a name and package for the entity, and click the Browse button next to the Extends field to select the parent entity from which the entity being created will extend.
For example, name the new entity Staff
and select the Persons
entity object in the Extends field.
On the Attributes page, the Entity Attributes list displays the attributes from the underlying table that are not included in the base entity object. Select the attributes you do not want to include in this entity object and click Remove.
For example, since you are creating the Staff
entity remove the ContractExpires
attribute and leave the DiscountEligible
attribute.
Click Override to select the discriminator attribute so that you can customize the attribute metadata to supply a distinct Default Value for the Staff
subtype.
For example, override the PersonTypeCode
attribute.
On the Attribute Settings page, use the Select Attribute dropdown list to select the discriminator attribute. Change the Default Value field to supply a distinct default value for the discriminator attribute that defines the entity subtype being created.
For example, select the PersonTypeCode
attribute and change its Default Value to the value "staff
".
Click Finish to create the subtype entity object.
Note:
You can repeat the same steps to define theSupplier
entity object that extends Persons
to add the additional ContractExpires
attribute and overrides the Default Value of the UserRole
discriminator attribute to have the value "supp
".To add methods to entity objects in an inheritance hierarchy, enable the custom Java class for the entity object and use the source editor to add the method.
To add a method that is common to all entity objects in the hierarchy, enable a custom Java class for the base entity object in the hierarchy and add the method in the source editor. For example, if you add the following method to the PersonsImpl
class for the base User
entity object, it will be inherited by all entity objects in the hierarchy:
// In PersonsImpl.java public void performPersonFunction() { System.out.println("## performPersonFunction as Customer"); }
To override a method in a subtype entity that is common to all entity objects in the hierarchy, enable a custom Java class for the subtype entity and choose Override Methods from the Source menu to launch the Override Methods dialog. Select the method you want to override, and click OK. Then, customize the overridden method's implementation in the source editor. For example, imagine overriding the performPersonFunction()
method in the StaffImpl
class for the Staff
subtype entity object and change the implementation to look like this:
// In StaffImpl.java public void performPersonFunction() { System.out.println("## performPersonFunction as Staff"); }
When working with instances of entity objects in a subtype hierarchy, sometimes you will process instances of multiple different subtypes. Since Staff
and Supplier
entities are special kinds of Persons
, you can write code that works with all of them using the more generic PersonsImpl
type that they all have in common. When doing this generic kind of processing of classes that might be one of a family of subtypes in a hierarchy, Java will always invoke the most specific override of a method available.
This means that invoking the performPersonFunction()
method on an instance of PersonsImpl
that happens to really be the more specific StaffImpl
subtype, will the result in printing out the following:
## performPersonFunction as Staff
instead of the default result that regular PersonsImpl
instances would get:
## performPersonFunction as Customer
To add a method that is specific to a subtype entity object in the hierarchy, enable a custom Java class for that entity and add the method in the source editor. For example, you could add the following method to the SupplierImpl
class for the Supplier
subtype entity object:
// In SupplierImpl.java public void performSupplierFunction() { System.out.println("## performSupplierFunction called"); }
When using inheritance, you can also introduce a new base entity, find subtype entities using a primary key, and create view objects with polymorphic entity usages.
In the InheritanceAndPolymorphicQueries
example project, the Persons
entity object corresponded to a concrete kind of row in the PERSONS
table and it also played the role of the base entity in the hierarchy. In other words, all of its attributes were common to all entity objects in the hierarchy. You might wonder what would happen, however, if the Persons
entity required a property that was specific to customers, but not common to staff or suppliers. Imagine that customers can participate in customer satisfaction surveys, but that staff and suppliers do not. The Persons
entity would require a LastSurveyDate
attribute to handle this requirement, but it wouldn't make sense to have Staff
and Supplier
entity objects inherit it.
In this case, you can introduce a new entity object called BasePersons
to act as the base entity in the hierarchy. It would have all of the attributes common to all Persons
, Staff
, and Supplier
entity objects. Then each of the three entities the correspond to concrete rows that appear in the table could have some attributes that are inherited from BasePersons and some that are specific to its subtype. In the BasePersons
type, so long as you mark the PersonTypeCode
attribute as a discriminator attribute, you can just leave the Default Value blank (or some other value that does not occur in the PERSON_TYPE_CODE
column in the table). Because at runtime you'll never be using instances of the BasePersons
entity, it doesn't really matter what its discriminator default value is.
When you use the findByPrimaryKey()
method on an entity definition, it only searches the entity cache for the entity object type on which you call it. In the InheritanceAndPolymorphicQueries
example project, this means that if you call PersonsImpl.getDefinitionObject()
to access the entity definition for the Persons
entity object when you call findByPrimaryKey()
on it, you will only find entities in the cache that happen to be customers. Sometimes this is exactly the behavior you want. However, if you want to find an entity object by primary key allowing the possibility that it might be a subtype in an inheritance hierarchy, then you can use the EntityDefImpl
class' findByPKExtended()
method instead. In the Persons
example described here, this alternative finder method would find an entity object by primary key whether it is a customer, supplier, or staff. You can then use the Java instanceof
operator to test which type you found, and then cast the PersonsImpl
object to the more specific entity object type in order to work with features specific to that subtype.
When you create an entity-based view object with an entity usage corresponding to a base entity object in an inheritance hierarchy, you can configure the view object to query rows corresponding to multiple different subtypes in the base entity's subtype hierarchy. Each row in the view object will use the appropriate subtype entity object as the entity row part, based on matching the value of the discriminator attribute. See Section 39.6.2, "How To Create a View Object with a Polymorphic Entity Usage," for specific instructions on setting up and using these view objects.
Due to database constraints, when you perform DML operations to save changes to a number of related entity objects in the same transaction, the order in which the operations are performed can be significant. If you try to insert a new row containing foreign key references before inserting the row being referenced, the database can complain with a constraint violation. You must understand the default order for processing of entity objects during commit time and how to programmatically influence that order when necessary.
Note:
The example in this section refers to theControllingPostingOrder
project of the AdvancedEntityExamples
application workspace in the StandaloneExamples
module of the Fusion Order Demo application.By default, when you commit the transaction the entity objects in the pending changes list are processed in chronological order, in other words, the order in which the entities were added to the list. This means that, for example, if you create a new Product
and then a new Supplier
related to that product, the new Product
will be inserted first and the new Supplier
second.
When two entity objects are related by a composition, the strict chronological ordering is modified automatically to ensure that composed parent and child entity rows are saved in an order that avoids violating any constraints. This means, for example, that a new parent entity row is inserted before any new composed children entity rows.
If your related entities are associated but not composed, then you need to write a bit of code to ensure that the related entities get saved in the appropriate order.
Consider the newProductForNewSupplier()
custom method from an PostModule
application module in Example 38-16. It accepts a set of parameters and:
Creates a new Product
.
Creates a new Supplier
.
Sets the product ID to which the server request pertains.
Commits the transaction.
Constructs a Result
Java bean to hold new product ID and supplier ID.
Returns the result.
Note:
The code makes the assumption that bothProducts.ProductId
and Suppliers.SupplierId
have been set to have DBSequence
data type to populate their primary keys based on a sequence.Example 38-16 Creating a New Product, Then a New Supplier, and Returning the New IDs
// In PostModuleImpl.java public Result newProductForNewSupplier(String supplierName, String supplierStatus, String productName, String productStatus, Number listPrice, Number minPrice, String shipCode) { oracle.jbo.domain.Date today = new Date(Date.getCurrentDate()); Number objectId = new Number(0); // 1. Create a new product ProductsBaseImpl newProduct = createNewProduct(); // 2. Create a new supplier SuppliersImpl newSupplier = createNewSupplier(); newSupplier.setSupplierName(supplierName); newSupplier.setSupplierStatus(supplierStatus); newSupplier.setCreatedBy("PostingModule"); newSupplier.setCreationDate(today); newSupplier.setLastUpdatedBy("PostingModule"); newSupplier.setLastUpdateDate(today); newSupplier.setObjectVersionId(objectId); // 3. Set the supplier id to which the product pertains newProduct.setSupplierId(newSupplier.getSupplierId().getSequenceNumber()); newProduct.setProductName(productName); newProduct.setProductStatus(productStatus); newProduct.setListPrice(listPrice); newProduct.setMinPrice(minPrice); newProduct.setShippingClassCode(shipCode); newProduct.setCreatedBy("PostingModule"); newProduct.setCreationDate(today); newProduct.setLastUpdatedBy("PostingModule"); newProduct.setLastUpdateDate(today); newProduct.setObjectVersionId(objectId); // 4. Commit the transaction getDBTransaction().commit(); // 5. Construct a bean to hold new supplier id and product id Result result = new Result(); result.setProductId(newProduct.getProductId().getSequenceNumber()); result.setSupplierId(newSupplier.getSupplierId().getSequenceNumber()); // 6. Return the result return result; } private ProductsBaseImpl createNewProduct(){ EntityDefImpl productDef = ProductsBaseImpl.getDefinitionObject(); return (ProductsBaseImpl) productDef.createInstance2(getDBTransaction(), null); }private SuppliersImpl createNewSupplier(){ EntityDefImpl supplierDef = SuppliersImpl.getDefinitionObject(); return (SuppliersImpl) supplierDef.createInstance2(getDBTransaction(), null); }
If you add this method to the application module's client interface and test it from a test client program, you get an error:
oracle.jbo.DMLConstraintException: JBO-26048: Constraint "PRODUCT_SUPPLIER_FK" violated during post operation: "Insert" using SQL Statement "BEGIN INSERT INTO PRODUCTS( SUPPLIER_NAME,SUPPLIER_STATUS,PRODUCT_NAME, PRODUCT_STATUS,LIST_PRICE,MIN_PRICE, SHIPPING_CLASS_CODE) VALUES (?,?,?,?,?,?,?) RETURNING PRODUCT_ID INTO ?; END;". ## Detail 0 ## java.sql.SQLException: ORA-02291: integrity constraint (FOD.PRODUCT_SUPPILER_FK) violated - parent key not found
When the PRODUCTS
row is inserted, the database complains that the value of its SUPPLIER_ID
foreign key doesn't correspond to any row in the SUPPLIERS
table. This occurred because:
The code created the Product
before the Supplier
Products
and Suppliers
entity objects are associated but not composed
The DML operations to save the new entity rows is done in chronological order, so the new Product
gets inserted before the new Supplier
.
To remedy the problem of attempting to add a product with a not-yet-valid supplier ID, you could reorder the lines of code in the example to create the Supplier
first, then the Product
. While this would address the immediate problem, it still leaves the chance that another application developer could create things in an incorrect order.
The better solution is to make the entity objects themselves handle the posting order so it will work correctly regardless of the order of creation. To do this you need to override the postChanges()
method in the entity that contains the foreign key attribute referencing the associated entity object and write code as shown in Example 38-17. In this example, since it is the Product
that contains the foreign key to the Supplier
entity, you need to update the Product
to conditionally force a related, new Supplier
to post before the service request posts itself.
The code tests whether the entity being posted is in the STATUS_NEW
or STATUS_MODIFIED
state. If it is, it retrieves the related product using the getSupplier()
association accessor. If the related Supplier
also has a post-state of STATUS_NEW
, then first it calls postChanges()
on the related parent row before calling super.postChanges()
to perform its own DML.
Example 38-17 Overriding postChanges() in ProductsBaseImpl to Post Supplier First
// In ProductsBaseImpl.java public void postChanges(TransactionEvent e) { /* If current entity is new or modified */ if (getPostState() == STATUS_NEW || getPostState() == STATUS_MODIFIED) { /* Get the associated supplier for the product */ SuppliersImpl supplier = getSupplier(); /* If there is an associated supplier */ if (supplier != null) { /* And if it's post-status is NEW */ if (supplier.getPostState() == STATUS_NEW) { /* * Post the supplier first, before posting this * entity by calling super below */ supplier.postChanges(e); } } } super.postChanges(e); }
If you were to re-run the example now, you would see that without changing the creation order in the newProductForNewSupplier()
method's code, entities now post in the correct order — first new Supplier
, then new Product
. Yet, there is still a problem. The constraint violation still appears, but now for a different reason!
If the primary key for the Suppliers
entity object were user-assigned, then the code in Example 38-17 would be all that is required to address the constraint violation by correcting the post ordering.
Note:
An alternative to the programmatic technique discussed here, which solves the problem at the Java EE application layer, is the use of deferrable constraints at the database layer. If you have control over your database schema, consider defining (or altering) your foreign key constraints to beDEFERRABLE INITIALLY DEFERRED
. This causes the database to defer checking the constraint until transaction commit time. This allows the application to perform DML operations in any order, provided that by COMMIT
time all appropriate related rows have been saved and would alleviate the parent/child ordering. However, you would still need to write the code to cascade-update the foreign key values if the parent's primary key is assigned from a sequence, as described in Section 38.8.3.3, "Understanding Associations Based on DBSequence-Valued Primary Keys" and Section 38.8.3.4, "Refreshing References to DBSequence-Assigned Foreign Keys."In this example, however, the Suppliers.SupplierId
is assigned from a database sequence, and not user-assigned in this example. So when a new Suppliers
entity row gets posted its SupplierId
attribute is refreshed to reflect the database-assigned sequence value. The foreign key value in the Products.SupplierId
attribute referencing the new supplier is "orphaned" by this refreshing of the supplier's ID value. When the product's row is saved, its SUPPLIER_ID
value still doesn't match a row in the SUPPLIERS
table, and the constraint violation occurs again. The next two sections discuss the solution to address this "orphaning" problem.
Recall from Section 4.10.10, "How to Get Trigger-Assigned Primary Key Values from a Database Sequence" that when an entity object's primary key attribute is of DBSequence
type, during the transaction in which it is created, its numerical value is a unique, temporary negative number. If you create a number of associated entities in the same transaction, the relationships between them are based on this temporary negative key value. When the entity objects with DBSequence
-value primary keys are posted, their primary key is refreshed to reflect the correct database-assigned sequence number, leaving the associated entities that are still holding onto the temporary negative foreign key value "orphaned".
For entity objects based on a composition, when the parent entity object's DBSequence
-valued primary key is refreshed, the composed children entity rows automatically have their temporary negative foreign key value updated to reflect the owning parent's refreshed, database-assigned primary key. This means that for composed entities, the "orphaning" problem does not occur.
However, when entity objects are related by an association that is not a composition, you need to write a little code to insure that related entity rows referencing the temporary negative number get updated to have the refreshed, database-assigned primary key value. The next section outlines the code required.
When an entity like Suppliers
in this example has a DBSequence
-valued primary key, and it is referenced as a foreign key by other entities that are associated with (but not composed by) it, you need to override the postChanges()
method as shown in Example 38-18 to save a reference to the row set of entity rows that might be referencing this new Suppliers
row. If the status of the current Suppliers
row is New, then the code assigns the RowSet
-valued return of the getProduct()
association accessor to the newProductsBeforePost
member field before calling super.postChanges()
.
Example 38-18 Saving Reference to Entity Rows Referencing This New Supplier
// In SuppliersImpl.java RowSet newProductsBeforePost = null; public void postChanges(TransactionEvent TransactionEvent) { /* Only bother to update references if Product is a NEW one */ if (getPostState() == STATUS_NEW) { /* * Get a rowset of products related * to this new supplier before calling super */ newProductsBeforePost = (RowSet)getProductsBase(); } super.postChanges(TransactionEvent); }
This saved RowSet
object is then used by the overridden refreshFKInNewContainees()
method shown in Example 38-19. It gets called to allow a new entity row to cascade update its refreshed primary key value to any other entity rows that were referencing it before the call to postChanges()
. It iterates over the ProductsBaseImpl
rows in the newProductsBaseBeforePost
row set (if non-null) and sets the new supplier ID value of each one to the new sequence-assigned supplier value of the newly posted Suppliers
entity.
Example 38-19 Cascade-Updating Entity Rows with New SupplierId Value
// In SuppliersImpl.java protected void refreshFKInNewContainees() { if (newProductsBeforePost != null) { Number newSupplierId = getSupplierId().getSequenceNumber(); /* * Process the rowset of products that referenced * the new supplier prior to posting, and update their * SupplierId attribute to reflect the refreshed SupplierId value * that was assigned by a database sequence during posting. */ while (newProductsBeforePost.hasNext()) { ProductsBaseImpl svrReq = (ProductsBaseImpl)newProductsBeforePost.next(); product.setSupplierId(newSupplierId); } closeNewProductRowSet(); } }
After implementing this change, the code in Example 38-16 runs without encountering any database constraint violations.
ADF Business Components comes with a base set of built-in declarative validation rules that you can use. However, a powerful feature of the validator architecture for entity objects is that you can create your own custom validation rules. When you notice that you or your team are writing the same kind of validation code over and over, you can build a custom validation rule class that captures this common validation "pattern" in a parameterized way.
After you've defined a custom validation rule class, you can register it in JDeveloper so that it is as simple to use as any of the built-in rules. In fact, you can even bundle your custom validation rule with a custom UI panel that JDeveloper leverages to facilitate developers' using and configuring the parameters your validation rule might require.
To write a custom validation rule for entity objects, you need a Java class that implements the JboValidatorInterface
in the oracle.jbo.rules
package. You can create a skeleton class from the New Gallery.
In the Application Navigator, right-click the project where you want to create the validator, and choose New from the context menu.
In the New Gallery, expand Business Tier, click ADF Business Components, select Validation Rule and click OK.
As shown in Example 38-20, JBOValidatorInterface
contains one main validate()
method, and a getter and setter method for a Description
property.
Example 38-20 All Validation Rules Must Implement the JboValidatorInterface
package oracle.jbo.rules; public interface JboValidatorInterface { void validate(JboValidatorContext valCtx) { } java.lang.String getDescription() { } void setDescription(String description) { } }
If the behavior of your validation rule will be parameterized to make it more flexible, then add additional bean properties to your validator class for each parameter. For example, the code in Example 38-21 implements a custom validation rule called DateMustComeAfterRule
which validates that one date attribute must come after another date attribute. To allow the developer using the rule to configure the names of the date attributes to use as the initial and later dates for validation, this class defines two properties initialDateAttrName
and laterDateAttrName
.
Example 38-21 shows the code that implements the custom validation rule. It extends the AbstractValidator
to inherit support for working with the entity object's custom message bundle, where JDeveloper saves the validation error message when a developer uses the rule in an entity object.
The validate()
method of the validation rule gets invoked at runtime whenever the rule class should perform its functionality. The code performs the following basic steps:
Ensures validator is correctly attached at the entity level.
Gets the entity row being validated.
Gets the values of the initial and later date attributes.
Validate that initial date is before later date.
Throws an exception if the validation fails.
Example 38-21 Custom DateMustComeAfterRule
// NOTE: package and imports omitted public class DateMustComeAfterRule extends AbstractValidator implements JboValidatorInterface { /** * This method is invoked by the framework when the validator should do its job */ public void validate(JboValidatorContext valCtx) { // 1. If validator is correctly attached at the entity level... if (validatorAttachedAtEntityLevel(valCtx)) { // 2. Get the entity row being validated EntityImpl eo = (EntityImpl)valCtx.getSource(); // 3. Get the values of the initial and later date attributes Date initialDate = (Date) eo.getAttribute(getInitialDateAttrName()); Date laterDate = (Date) eo.getAttribute(getLaterDateAttrName()); // 4. Validate that initial date is before later date if (!validateValue(initialDate,laterDate)) { // 5. Throw the validation exception RulesBeanUtils.raiseException(getErrorMessageClass(), getErrorMsgId(), valCtx.getSource(), valCtx.getSourceType(), valCtx.getSourceFullName(), valCtx.getAttributeDef(), valCtx.getNewValue(), null, null); } } else { throw new RuntimeException("Rule must be at entity level"); } } /** * Validate that the initialDate comes before the laterDate. */ private boolean validateValue(Date initialDate, Date laterDate) { return (initialDate == null) || (laterDate == null) || (initialDate.compareTo(laterDate) < 0); } /** * Return true if validator is attached to entity object * level at runtime. */ private boolean validatorAttachedAtEntityLevel(JboValidatorContext ctx) { return ctx.getOldValue() instanceof EntityImpl; } // NOTE: Getter/Setter Methods omitted private String description; private String initialDateAttrName; private String laterDateAttrName; }
For easier reuse of your custom validation rules, you would typically package them into a JAR file for reference by applications that make use of the rules.
Since a validation rule class is a bean, you can implement a standard JavaBean customizer class to improve the design time experience of setting the bean properties. In the example of the DateMustComeAfter
rule (in Example 38-21), the two properties that the developers must configure are the initialDateAttrName
and laterDateAttrName
properties.
Figure 38-3 illustrates using JDeveloper's visual designer for Swing to create a DateMustComeAfterRuleCustomizer
using a JPanel
with a titled border containing two JLabel prompts and two JComboBox
controls for the dropdown lists. The code in the class populates the dropdown lists with the names of the Date-valued attributes of the current entity object being edited in the IDE. This will allow a developer who adds a DateMustComeAfterRule
validation to their entity object to easily pick which date attributes should be used for the starting and ending dates for validation.
To associate a customizer with your DateMustComeAfterRule
Java Bean, you follow the standard practice of creating a BeanInfo
class. As shown in Example 38-22, the DateMustComeAfterRuleBeanInfo
returns a BeanDescriptor that associates the customizer class with the DateMustComeAfter
bean class.
You would typically package your customizer class and this bean info in a separate JAR file for design-time-only use.
Example 38-22 BeanInfo to Associate a Customizer with a Custom Validation Rule
package oracle.fodemo...frameworkExt.rules; import java.beans.BeanDescriptor; import java.beans.SimpleBeanInfo; public class DateMustComeAfterRuleBeanInfo extends SimpleBeanInfo { public BeanDescriptor getBeanDescriptor() { return new BeanDescriptor(DateMustComeAfterRule.class, DateMustComeAfterRuleCustomizer.class); } }
After you've created a custom validation rule, you can add it to the project or application level in the JDeveloper IDE so that other developers can use the rule declaratively.
To register a custom validation rule in a project containing entity objects:
In the Application Navigator, right-click the desired project, and choose Project Properties from the context menu.
In the Project Properties dialog, expand Business Components, and select Registered Rules.
On the Registered Rules page, click Add.
In the Register Validation Rule dialog, browse to find the validation rule you have created (such as the one created in Section 38.9.1, "How to Create a Custom Validation Rule"), and click OK.
To register a custom validator at the IDE level:
From the Tools menu, choose Preferences.
From the Business Components > Register Rules page, you can add a one or more validation rules.
When adding a validation rule, provide the fully-qualified name of the validation rule class, and supply a validation rule name that will appear in JDeveloper's list of available validators.
History types are used to track data specific to a point in time. JDeveloper ships with a number of history types, but you can also create your own. For more information on the standard history types and how to use them, see Section 4.10.12, "How to Track Created and Modified Dates Using the History Column."
You are not limited to the history types provided, you can add or remove custom history types using the History Types page in the Preferences dialog, and then write custom Java code to implement the desired behavior. The code to handle custom history types should be written in your application-wide entity base class for reuse.
Figure 38-5 shows a custom type called last update login
with type Id of 11
. Assume that last_update_login
is a foreign key in the FND_LOGINS
table.
To create a custom history type:
From the Tools menu, choose Preferences.
In the Preferences dialog, expand Business Components, and click History Types.
On the History Types page, click New.
In the Create History Type dialog, enter a string value for the name (spaces are allowed) and a numerical Id.
The Type Id must be an integer between 11 and 126. The numerical values 0-10 are reserved for internal use. The display string is displayed in the History Column dropdown list the next time you use the Edit Attribute dialog.
Open the EntityImpl.java
file and add a definition similar to the one in Example 38-23.
Override the getHistoryContextForAttribute(AttributeDefImpl attr)
method in the EntityImpl
base class with code similar to Example 38-24.
Example 38-24 Overriding getHistoryContextForAttribute()
@Override protected Object getHistoryContextForAttribute(AttributeDefImpl attr) { if (attr.getHistoryKind() == LASTUPDATELOGIN_HISTORY_TYPE) { // Custom History type logic goes here } else { return super.getHistoryContextForAttribute(attr); } }
Because they are typically used for auditing values over the life of an application, it is rare that you would want to remove a history type. However, in the event that you need to do so, perform the following tasks:
Remove the history type from the JDeveloper history types list in the Preferences dialog.
Remove any custom code you implemented to support the history type in the base EntityImpl.getHistoryContextForAttribute
method.
Remove all usages of the history type in the entity attribute metadata. Any attribute that you have defined to use this history type must be edited.
To remove a history type from the JDeveloper history types list:
From the Tools menu, choose Preferences.
In the Preferences dialog, expand Business Components, and click History Types.
On the History Types page, select the history type that you want to remove and click Delete.