Business object instances are typically java object representations of rows of a database table.
The addition of following columns to each database table is strongly suggested:
Object ID
Version number
The Object ID is used as a globally unique identifier (or GUID) of each row across all database tables. That is, every row in every table should have a different Object ID value. It is typically defined as a VARCHAR field of 36 characters, and should be named "OBJ_ID" in the database. A unique constraint should be applied to the object ID column, but must NOT be part of the primary key. The KNS system will assume that each row has a unique value.
The object ID value is automatically stored by the framework and/or the database layer.
KFS/Rice uses optimistic locking to provide concurrency control. Optimistic locking requires the use of a version number field, named "VER_NBR". On Oracle, the field is defined as a NUMBER(8,0). On MySQL, the field is defined as a DECIMAL(8). This column should NOT be part of the primary key.
Optimistic locking helps to prevent updates to stale data and consists of two steps:
Retrieval of a row from a database, including the value of the version number column
Updating/deleting a row from the database with the same primary key and version number criteria. If updating the table, the version number will be incremented by one.
The following series of steps demonstrates how optimistic locking works:
User A retrieves the row for chart code "BL". The row has version number of 3.
User A performs an update of the "BL" record. The SQL query that updates the record would read something like "UPDATE CA_CHART_T SET <some updates>, VER_NBR = 4 WHERE FIN_COA_CD = "BL" and VER_NBR = 3. (The "4" refers to the incremented version number.)
User B retrieves the row for chart code "BL". The version number is now 4.
User B performs an update of the "BL" record. The SQL query that updates the record would read something like "UPDATE CA_CHART_T SET <some updates>, VER_NBR = 5 WHERE FIN_COA_CD = "BL" and VER_NBR = 4. (The "5" refers to the incremented version number.)
The following series of steps demonstrates how optimistic locking prevents concurrency problems.
User A retrieves the row for chart code "BL". The row has version number of 3.
User B retrieves the row for chart code "BL". Like user A, the version number is 3.
User A performs a update of the "BL" record. The SQL query that updates the record would read something like "UPDATE CA_CHART_T SET <some updates>, VER_NBR = 4 WHERE FIN_COA_CD = "BL" and VER_NBR = 3. (The "4" refers to the incremented version number.)
User B performs a update of the "BL" record. The SQL query that updates the record would read something like what User A executed above (notice the version numbers). However, the previous step already updated the version number to 4 from 3, so this update does nothing (i.e. update row count = 0) because it was trying to update the BL chart with a version number of 3. The system detects the 0 update row count, and throws an OptimisticLockingException. This exception indicates that the system tried to update stale data.
The default mapping library used by the KNS for this release is OJB from Apache. More information can be found on the OJB website: http://db.apache.org/ojb/.
OJB repository files map the following information:
The BusinessObject (BO) mapped to a given database table
The getter/setter method in the BO mapped to a given database column
The fields(s) comprising foreign keys between a business object and its reference(s)
Currently, OJB is used as the underlying persistence layer. It converts database rows into java objects upon retrieval, and vice versa upon updates/deletes. This section assumes that the reader is familiar with the basic mapping constructs/principles described on these pages:
http://db.apache.org/ojb/docu/guides/repository.html#class-descriptor-N104E3
http://db.apache.org/ojb/docu/guides/repository.html#field-descriptor-N105C6
http://db.apache.org/ojb/docu/guides/repository.html#field-descriptor-N105C6
http://db.apache.org/ojb/docu/guides/repository.html#collection-descriptor-N10770
http://db.apache.org/ojb/docu/guides/repository.html#foreignkey
http://db.apache.org/ojb/docu/guides/repository.html#inverse-foreignkey
http://db.apache.org/ojb/docu/guides/basic-technique.html
OJB provides a way to convert data before they are persisted to and retrieved from the database. This is accomplished by specifying a class that implements org.apache.ojb.broker.accesslayer.conversions.FieldConversion in the <field-descriptor> element.
The following are the more often used converters in KFS/Rice:
org.kuali.core.util.OjbCharBooleanConversion: since boolean flags are typically stored as "Y" or "N" (i.e. strings) in the database but represented as booleans within business objects, this converter automatically allows converts between the string and the boolean representation
org.kuali.core.util.OjbKualiEncryptDecryptFieldConversion: provides seamless encryption of values when persisting, and decryption when retrieving from the database. Beware that the business object itself holds an unencrypted value, and as such, care should be taken to ensure that unencrypted sensitive data are not exposed to unauthorized parties.
Both OJB and the KNS offer a number of FieldConversion implementations beyond these two for use in client applications.
Example converter declaration for a sample Business Object
<field-descriptor name="bankAccountNbr" column="BNK_ACCT_NBR" jdbc-type="VARCHAR" conversion="org.kuali.core.util.OjbKualiEncryptDecryptFieldConversion"/>
OJB relationships should be used to define relationships between tables that are guaranteed to exist within the same database.
For example, assume a sample Business Object class “Bank”. The Bank class contains a BankType reference object. Typically a BankType class table would exist in the same database as the Bank class table. In this example the relationship between Bank and BankType can be defined by OJB. However, a “User” business object table typically will exist in an external system since it will likely be referenced by more than one Rice client application. If a BO had a relationship with a “User” BO, the mapping would require that the relationship be set up via the data dictionary files (which will be discussed in detail later in this document). Any business object implementing the org.kuali.rice.kns.bo.ExternalizableBusinessObject interface needs to be related to via the data dictionary.
Here is an example directly from Rice in the file OJB-repository-kns.xml:
<class-descriptor class="org.kuali.rice.kns.bo.StateImpl" table="KR_STATE_T"> <field-descriptor name="postalCountryCode" column="POSTAL_CNTRY_CD" jdbc-type="VARCHAR" primarykey="true" index="true" /> <field-descriptor name="postalStateCode" column="POSTAL_STATE_CD" jdbc-type="VARCHAR" primarykey="true" index="true" /> <field-descriptor name="postalStateName" column="POSTAL_STATE_NM" jdbc-type="VARCHAR" /> <field-descriptor name="objectId" column="OBJ_ID" jdbc-type="VARCHAR" index="true" /> <field-descriptor name="versionNumber" column="VER_NBR" jdbc-type="BIGINT" locking="true" /> <field-descriptor name="active" column="ACTV_IND" jdbc-type="VARCHAR" conversion="org.kuali.rice.kns.util.OjbCharBooleanConversion"/> <reference-descriptor name="country" class-ref="org.kuali.rice.kns.bo.CountryImpl" auto-retrieve="true" auto-update="none" auto-delete="none"> <foreignkey field-ref="postalCountryCode" /> </reference-descriptor> </class-descriptor>
In this OJB mapping, we can determine the following information:
The KR_STATE_T table is mapped to the org.kuali.rice.kns.bo.StateImpl business object
The POSTAL_CNTRY_CD column is mapped to the "postalCountryCode" property of the BO (i.e. accessed using the getPostalCountryCode and setPostalCountryCode methods), is a VARCHAR, is indexed, and is one of the fields in the primary key
The POSTAL_STATE_CD column is mapped to the "postalStateCode" property of the BO, is a VARCHAR, is indexed, and is one of the fields in the primary key
The OBJ_ID column is mapped to the "objectId" property, is indexed, and is a VARCHAR
The VER_NBR column is mapped to the "verionNumber" property, is a BIGINT, and is used for locking
The ACTV_IND column is mapped to the “active” property, is a VARCHAR, and uses the conversion class org.kuali.rice.kns.util.OjbCharBooleanConversion
We can determine the following information about the "country" reference object:
It is of type org.kuali.rice.kns.bo.CountryImpl
the auto-retrieve attribute is true: When the StateImpl is retrieved from OJB, the CountryImpl object will behave like it was retrieved as well (the proxy attribute of the ‘field-descriptor’ tag can be set to true or false to determine whether the CountryImpl is really retrieved when the account is retrieved or not)
the auto-update attribute is none: When the StateImpl is updated using OJB, the CountryImpl object will not be updated even if changes have been made to it
the auto-delete attribute is none: When the StateImpl is deleted using OJB, the CountryImpl object will not be deleted
The <foreignkey> tag specifies the fields in the StateImpl BO that are in a foreign key relationship and their order with the primary key fields in the CountryImpl BO. The CountryImpl BO has one primary key field, and the value from StateImpl's “postalCountryCode” property is used as the value for CountryImpl’s primary key value.
A mapping may also define a collection-descriptor tag as follows:
<class-descriptor class="org.kuali.rice.kns.test.document.bo.AccountManager" table="TRV_ACCT_FO"> <field-descriptor name="id" column="acct_fo_id" jdbc-type="BIGINT" primarykey="true" autoincrement="true" sequence-name="TRV_FO_ID_S" /> <field-descriptor name="userName" column="acct_fo_user_name" jdbc-type="VARCHAR" /> <collection-descriptor name="accounts" collection-class="org.apache.ojb.broker.util.collections.ManageableArrayList" element-class-ref="org.kuali.rice.kns.test.document.bo.Account" auto-retrieve="true" auto-update="object" auto-delete="object" proxy="true" > <orderby name="accountNumber" sort="ASC" /> <inverse-foreignkey field-ref="amId" /> </collection-descriptor> </class-descriptor>
We can determine the following information about the "accounts" collection reference:
The collection itself is of type org.apache.ojb.broker.util.collections.ManageableArrayList, which keeps track of which elements have been removed from the array, to help when deleting elements.
Each element of the collection is of type org.kuali.rice.kns.test.document.bo.Account.
The auto-retrieve attribute is true: when the AccountManager is retrieved from the database, the collection will be populated or behave as if it were populated upon accessing the collection. (the proxy setting determines whether the database is queried when the AccountManager is retrieved from the DB or whether it will retrieve from the DB only when the collection is accessed (i.e. lazy loading)).
The auto-update attribute is object: when the AccountManager is inserted or updated, the accounts collection is inserted or updated accordingly.
The auto-delete attribute is object: when the AccountManager is deleted, the corresponding accounts will be deleted as well.
The <orderby> tag specifies the sort order of elements in the collection. In this case, the account numbers will be in ascending order in the collection.
The <inverse-foreignkey> specifies the fields of the element BO (i.e. Account) that will match the primary key fields of the AccountManager BO. The “amId” attribute in the Account table will be used to find objects that match the primary key of the AccountManager object, or in this case the “id” attribute.
Business Objects are java classes that implement the org.kuali.core.bo.BusinessObject interface. However, a majority of business objects extend org.kuali.core.bo.PersistableBusinessObjectBase, which implements org.kuali.core.bo.PersistableBusinessObject and org.kuali.core.bo.BusinessObject. Business Objects which extend from the class PersistableBusinessObjectBase also have an advantage in that they will inherit getter and setter methods for the attributes ‘version number’ and ‘object id’.
In each application, all simple class names (i.e. ignoring the package) should be unique. If multiple packages contain the same class name, the data dictionary may not load the duplicated classes properly.
Business objects need to implement getter and setter methods for each field that is mapped between java business objects and the database table (the mapping is described later). Therefore, if, in java, the ACCOUNT_NM database column is named "accountName", then the getter method should be called getAccountName and the setter should be setAccountName (i.e. the conventions follow the standard Java bean getters and setters practices).
Objects that extend org.kuali.core.bo.BusinessObjectBase must also implement the toStringMapper method, which returns a map of the BO's fields to be used in toString.
The org.kuali.core.bo.PersistableBusinessObjectBase class has several more methods that can be overridden that customize the behavior of the business object. Just a few examples are customizations that can be made upon persistence and retrieval of the business object, and how reference objects of the business object are refreshed, as well as other methods.
A reference object is a member variable of a business object that also implements the BusinessObject interface. It refers to the database row referenced by the values in a foreign key relationship. For example, the CampusImpl BO/table has a column for a campus type code (CAMPUS_TYP_CD). Therefore, the CampusImpl BO may have a referenced CampusTypeImpl object, which represents the campus type row referred to by the campus’ campus type code. Here is the CampusImpl OJB mapping:
<class-descriptor class="org.kuali.rice.kns.bo.CampusImpl" table="KRNS_CAMPUS_T"> <field-descriptor name="campusCode" column="CAMPUS_CD" jdbc-type="VARCHAR" primarykey="true" index="true" /> <field-descriptor name="campusName" column="CAMPUS_NM" jdbc-type="VARCHAR" /> <field-descriptor name="campusShortName" column="CAMPUS_SHRT_NM" jdbc-type="VARCHAR" /> <field-descriptor name="campusTypeCode" column="CAMPUS_TYP_CD" jdbc-type="VARCHAR" /> <field-descriptor name="objectId" column="OBJ_ID" jdbc-type="VARCHAR" index="true" /> <field-descriptor name="versionNumber" column="VER_NBR" jdbc-type="BIGINT" locking="true" /> <field-descriptor name="active" column="ACTV_IND" jdbc-type="VARCHAR" conversion="org.kuali.rice.kns.util.OjbCharBooleanConversion" /> <reference-descriptor name="campusType" class-ref="org.kuali.rice.kns.bo.CampusTypeImpl" auto-retrieve="true" auto-update="none" auto-delete="none"> <foreignkey field-ref="campusTypeCode" /> </reference-descriptor> </class-descriptor>
Here are bits of the CampusImpl class file:
public class CampusImpl extends PersistableBusinessObjectBase implements Campus, Inactivateable { private String campusCode; private String campusName; private String campusShortName; private String campusTypeCode; protected boolean active; private CampusType campusType; ...
A collection reference is a member variable of a business object that implements java.util.Collection, with each element in the collection being a BusinessObject. A collection reference would be appropriate to model something like the list of Kuali Financial sub accounts of the Kuali Financial account business object.
A reference object or collection is defined in two steps:
A field in a business object is created for either the reference object or collection reference
A relationship is mapped within either OJB (See above) or the data dictionary (See below)
To refresh (or retrieve) a reference object is to reload the referenced row from the database, in case the foreign key field values or referenced data have changed.
For references mapped within the data dictionary, the framework does not have the logic to enable refreshing of a reference. The code must both implement the logic to refresh a data dictionary defined reference and the logic to invoke refreshing. A specific explanation can be found below.
For references mapped within OJB, the framework automatically takes care of the logic to enable refreshing of a reference. Under certain circumstances, it's able to automatically refresh references upon retrieval of the main BO from the database, and refreshing can also be invoked manually.
Note that this means that if the value of a foreign key field is changed, the corresponding reference object is not refreshed automatically. Taking the CampusImpl BO example above, if the code alters the CampusImpl’s campusTypeCode field, the framework will not automatically retrieve the new associated CampusTypeImpl BO reference object. To refresh the CampusImpl’s CampusTypeImpl reference object with the new campus type code, refresh/retrieve must be manually called (see below).
For references with relationships that are not mapped in OJB, code will need to be written to accommodate refreshing. A common example of this is Person object references, because institutions may decide to use another source for Identity Management (e.g. LDAP).
Although there are alternative strategies for accommodating refreshing, typically getter methods of these non-OJB mapped reference objects include the code that retrieves the reference object from the underlying datasource.
In contrast to OJB-mapped references, note that this strategy allows for the automatic refreshing of reference objects when a foreign key field value has been changed. If, in our example using CampusImpl above, the reference object for CampusTypeImpl was not defined in OJB, the string campusTypeCode may be changed and that would be enough to alter the getter method for CampusTypeImpl to properly retrieve the correct row from the database.
Business objects fall into two broad, and for the most part mutually exclusive, categories: those that are edited by maintenance documents and those that are not. This section refers only to business objects that are edited by maintenance documents that have updatable collections.
When constructing this type of BusinessObject, initialize each of the updatable collection references to an instance of org.kuali.rice.kns.util.TypedArrayList. TypedArrayList is a subclass of ArrayList that takes in a java.lang.Class object in its constructor. All elements of this list must be of that type, and when the get(int) method is called, if necessary, this list will automatically construct items of the type to avoid an IndexOutOfBoundsException. Take the example below, the SummaryAccount BO contains an updatable reference to a list of PurApSummaryItem objects.
public class SummaryAccount { private List<PurApSummaryItem> items; public SummaryAccount() { super(); items = new TypedArrayList(PurApSummaryItem.class); } }
When a collection is non-updatable (i.e. read only from the database), it is not necessary to initialize the collection. OJB will take care of list construction and population.
Business objects that have active/inactive states should implement the Inactivateable interface:
public interface Inactivateable { public boolean isActive(); /* Indicates whether the record is active or inactive. */ public void setActive(boolean active); /* Sets the record to active or inactive. */ }
By implementing this interface, functionality such as default active checks and inactivation blocking in the maintenance framework can be taken advantage of.
Business objects that have active from and to dates (effective dating) should implement the InactivateableFromTo interface:
public interface InactivateableFromTo extends Inactivateable { /* Sets the date for which record will be active * @param from * - Date value to set */ public void setActiveFromDate(Date from); /* Gets the date for which the record become active * @return Date */ public Date getActiveFromDate(); /* Sets the date for which record will be active to * @param from * - Date value to set */ public void setActiveToDate(Date to); /* Gets the date for which the record become inactive * @return Date */ public Date getActiveToDate(); /* Gets the date for which the record is being compared to in determining active/inactive * @return Date */ public Date getActiveAsOfDate(); /* Sets the date for which the record should be compared to in determining active/inactive, if * not set then the current date will be used * @param activeAsOfDate * - Date value to set */ public void setActiveAsOfDate(Date activeAsOfDate); }
activeFromDate - The date for which the record becomes active (inclusive when checking active status).
activeToDate - The date to which the record is active (exclusive when checking active status).
active - The active field is calculated from the active from and to dates. If the active from date is less than or equal to current date (or from date is null) and the current date is less than the active to date (or to date is null) the active getter will return true, otherwise it will return false.
current - The current field is set to true for records with the greatest active from date less than or equal to the current date.
For example say we have two employee records:
rec 1, empl A, active from 01/01/2010, active to 01/01/2011
rec 2, empl A, active from 03/01/2010, active to 01/01/2011
With 03/01/2010 <= current date < 01/01/2011 both of these records will be active, however only rec 2 would be current since it has a later active begin date.
To determine the maximum active begin date, records are grouped by the fields declared in the data dictionary for the business object.
activeAsOfDate - By default when checking the active or current status the current date is used, however this field can be set to check the status as of another date.
For example say we have a record with active from date 01/01/2010 and active to date 06/01/2010, with the current date equal to 08/01/2010. With the active as of date empty, the current date will be used and this record will be determined inactive. However if we set the active as of date equal to 05/01/2010 (which falls between the active date range) and query, this record will be determined active.
Business objects that implement InactivateableFromTo can participate in default existence checks and inactivation blocking functionality. In addition, the lookup framework contains special logic for searching on InactivateableFromTo instances. This includes:
Translating criteria on the active field (active true or false) to criteria on the active to and from date fields
Translating criteria on the current field (current true of false) to criteria selecting the active record with the greatest active from date less than or equal to the active date
Handles the active as of date when doing active or current queries
For finding active and current InactivateableFromTo records InactivateableFromToService can be used. This service provides many methods for dealing with InactivateableFromTo objects in code.
In order to determine whether or not an InactivateableFromTo record is current, the framework must know what fields of the business object to group by (see ‘current’ in ‘Explanation of InactivateableFromTo fields’). This is configured by setting the groupByAttributesForEffectiveDating property on the data dictionary BusinessObjectEntry.
Example:
<bean id="TravelAccountUseRate-parentBean" abstract="true" parent="BusinessObjectEntry"> <property name="businessObjectClass" value="edu.sampleu.travel.bo.TravelAccountUseRate"/> <property name="inquiryDefinition"> <ref bean="TravelAccountUseRate-inquiryDefinition"/> </property> <property name="lookupDefinition"> <ref bean="TravelAccountUseRate-lookupDefinition"/> </property> <property name="titleAttribute" value="Travel Account Use Rate"/> <property name="objectLabel" value="Travel Account Use Rate"/> <property name="attributes"> <list> <ref bean="TravelAccountUseRate-id"/> <ref bean="TravelAccountUseRate-number"/> <ref bean="TravelAccountUseRate-rate"/> <ref bean="TravelAccountUseRate-activeFromDate"/> <ref bean="TravelAccountUseRate-activeToDate"/> <ref bean="TravelAccountUseRate-activeAsOfDate"/> <ref bean="TravelAccountUseRate-active"/> <ref bean="TravelAccountUseRate-current"/> </list> </property> <property name="groupByAttributesForEffectiveDating"> <list> <value>number</value> </list> </property> </bean>