Moving on to the next problem with my resource and resource_cost relationship.
The resource costs have a lifecycle completely dependant on resource. If the resource is deleted, all the resource costs should be deleted. If I add a row to the list it should be persisted, remove it from the list deleted etc. According to the docs this means I should specify cascade="all-delete-orphan". So I did.
However the resourceCost list is time dependant. A person's costPerHour changes from $10, to $20 , to $30 etc. These changes can take effect at any time. The user sees an 'effective date'. Lets say the three costs become effective on 1/1/2002, 1/10/2003 and 1/6/2004 (in d/m/y format).
The data model holds this information as $10 from 1/1/2002 to 1/10/2003, $20 from 1/10/2003 to 1/6/2004, and $30 from 1/6/2004 to 31/12/9999 which is a date we picked that is obscenely in the future. The objective is to make locating the specific cost relevant on a particular date easy. I can invoke sql of the form 'where :targetDate between startDate and endDate' to locate the specific cost record relevant for :targetDate.
However it is equally concievable that the user could edit the last entry and change the effective date to 1/6/2003. This means I have to change all the date ranges to $10 from 1/1/2002 to 1/6/2003, $30 from 1/6/2003 to 1/10/2003, and $20 from 1/10/2003 to 31/12/9999.
This is quite easy to do. In java I sort the list by start date, and set the end dates of element n-1 to the start date of element n, and set the end date of the last element to 31/12/9999.
However Hibernate complains and says "Don't dereference a collection with cascade="all-delete-orphan": com.ilign.ppm.domain.Resource.resourceCosts"
I have tried doing this with a set, sorted set, and bag.
The problem with set is that to sort the list I have to create a new TreeSet with a comparator, add all the elements of my existing set to it, process and then assign my new TreeSet to the entity variable. Quite easy to see how Hibernate complains about that.
The problem with sortedSet (mapping defines as <set with a sort="comparatorClass") is that when the user changes the contents of a ResourceCost, the sorted set doesn't trigger a sort. And even if it did, the user may have changed the data such that the ResourceCost now 'equals' another one which violates the Set contract. So I have to sort the contents elsewhere which brings the same problem as above.
I thought bag would work. Bag works like a List. I can use Collections.sort on that and I thought that the list was sorted in place, but obviously is isn't because I still get the problem.
Does this mean I have to manufacture a lifecycle for ResourceCost and individually manage the saving etc of them purely because I need to be able to manipulate the contents of the list?
Hibernate version: 3
Mapping documents:
Code:
<hibernate-mapping package="com.ilign.ppm.domain">
<class name="Resource" table="RESOURCE" lazy="true" dynamic-update="true">
<property name="firstNames" column="FIRST_NAMES" type="string"/>
.
.
.
<bag name="resourceCosts" table="RESOURCE_COST" order-by="start_date" lazy="true" cascade="all-delete-orphan" inverse="true">
<key column="RESOURCE_ID" />
<one-to-many class="ResourceCost"/>
</bag>
</class>
<class name="ResourceCost" table="RESOURCE_COST" lazy="true" dynamic-update="true">
<many-to-one name="resource" column="RESOURCE_ID" />
<property name = "startDate" column="START_DATE" type="com.ilign.ppm.common.PersistentDateTime"/>
<property name = "endDate" column="END_DATE" type="com.ilign.ppm.common.PersistentDateTime"/>
<component name="costPerHour" class="com.ilign.ppm.common.ForeignCurrency">
<component name="baseMoney" class="com.ilign.ppm.common.Money">
<property name="amount" column="COST_PER_HOUR" type="big_decimal" not-null="true"/>
<property name="currencyCode" column="BASE_CURRENCY_CODE" />
</component>
<component name="srcMoney" class="com.ilign.ppm.common.Money">
<property name="amount" column="SRC_COST" type="big_decimal"/>
<property name="currencyCode" column="SRC_COST_CURRENCY_CODE" type="string"/>
</component>
</component>
<component name="billableRate" class="com.ilign.ppm.common.ForeignCurrency">
<component name="baseMoney" class="com.ilign.ppm.common.Money">
<property name="amount" column="BILLABLE_RATE" type="big_decimal" not-null="true"/>
<property name="currencyCode" column="BASE_CURRENCY_CODE" insert="false" update="false" />
</component>
<component name="srcMoney" class="com.ilign.ppm.common.Money">
<property name="amount" column="SRC_BILLABLE" type="big_decimal"/>
<property name="currencyCode" column="SRC_BILLABLE_CURRENCY_CODE" type="string"/>
</component>
</component>
</class>
</hibernate-mapping>
Code between sessionFactory.openSession() and session.close():Basically the code that does the sort... It is using a bag (list) at the moment since that is where I've ended up.
Code:
Collections.sort( this.resourceCosts, new ResourceCostStartDateComparator() );
// Move over the elements and set the end date of element n to be 1 millisecond less than the start date of element n
ResourceCost previousResourceCost = null;
for ( ResourceCost aResourceCost : this.resourceCosts )
{
if ( previousResourceCost == null )
{
previousResourceCost = aResourceCost;
continue;
}
if ( aResourceCost.getStartDate().equals( previousResourceCost.getStartDate() ) )
{
throw new DuplicateCostException( "A ResourceCost was found that already starts on "
+ aResourceCost.getStartDate() );
}
DateTime newEndDate = new DateTime( aResourceCost.getStartDate() ));
previousResourceCost.setEndDate( newEndDate );
previousResourceCost = aResourceCost;
}
// Set the end date of the last element to be the nominal end date and make the resource point to the new set
this.resourceCosts.get( this.resourceCosts.size() - 1 ).setEndDate( Util.NOMINAL_LATEST_DATE );
Full stack trace of any exception that occurs:
junit.framework.AssertionFailedError: Don't dereference a collection with cascade="all-delete-orphan": com.ilign.ppm.domain.Resource.resourceCosts
at com.ilign.ppm.dao.ResourceDaoHibernateTest.testAddResourceCostMaintainsSortOrder(ResourceDaoHibernateTest.java:113)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at com.intellij.rt.execution.junit2.JUnitStarter.main(JUnitStarter.java:31)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:86)
Name and version of the database you are using: Firebird
The generated SQL (show_sql=true):
2005-08-05 09:23:37,846 INFO [com.ilign.ppm.dao.ResourceDaoHibernateTest] Began transaction: transaction manager [org.springframework.orm.hibernate3.HibernateTransactionManager@1d38b87]; defaultRollback true
2005-08-05 09:23:37,862 DEBUG [org.hibernate.SQL] select resource0_.RESOURCE_ID as RESOURCE1_, resource0_.VSN as VSN17_, resource0_.FIRST_NAMES as FIRST3_17_, resource0_.LAST_NAME as LAST4_17_, resource0_.FULL_NAME as FULL5_17_, resource0_.USER_NAME as USER6_17_, resource0_.PW as PW17_, resource0_.PASSWORD_CHANGED as PASSWORD8_17_, resource0_.PASSWORD_EXPIRY as PASSWORD9_17_, resource0_.EMAIL_ADDRESS as EMAIL10_17_, resource0_.RESOURCE_NUMBER as RESOURCE11_17_, resource0_.SKILL_SET as SKILL12_17_, resource0_.PRIMARY_ADDRESS_ID as PRIMARY13_17_, resource0_.PRIMARY_PHONE_ID as PRIMARY14_17_, resource0_.START_DATE as START15_17_, resource0_.END_DATE as END16_17_, resource0_.ORG_NAME as ORG17_17_, resource0_.RESPONSIBILITY as RESPONS18_17_, resource0_.TIMESHEET_REQUIRED as TIMESHEET19_17_, resource0_.PRIMARY_VALIDATOR_ID as PRIMARY20_17_, resource0_.SECONDARY_VALIDATOR_ID as SECONDARY21_17_, resource0_.LOCATION as LOCATION17_, resource0_.RESOURCE_TYPE as RESOURCE23_17_ from RESOURCE resource0_ where resource0_.USER_NAME=?
2005-08-05 09:23:37,893 DEBUG [org.hibernate.SQL] select usersettin0_.RESOURCE_ID as RESOURCE1_0_, usersettin0_.VSN as VSN27_0_, usersettin0_.CREATE_START_MILESTONE as CREATE3_27_0_, usersettin0_.CREATE_END_MILESTONE as CREATE4_27_0_, usersettin0_.SHOW_PROJECT_MENU as SHOW5_27_0_, usersettin0_.DISPLAY_EXPIRED_ITEMS as DISPLAY6_27_0_, usersettin0_.LOCALE_STRING as LOCALE7_27_0_, usersettin0_.HOME_PAGE_CODE as HOME8_27_0_ from USER_SETTING usersettin0_ where usersettin0_.RESOURCE_ID=?
2005-08-05 09:23:37,940 DEBUG [org.hibernate.SQL] select resourceco0_.RESOURCE_ID as RESOURCE3_1_, resourceco0_.RESOURCE_COST_ID as RESOURCE1_1_, resourceco0_.RESOURCE_COST_ID as RESOURCE1_0_, resourceco0_.VSN as VSN20_0_, resourceco0_.RESOURCE_ID as RESOURCE3_20_0_, resourceco0_.START_DATE as START4_20_0_, resourceco0_.END_DATE as END5_20_0_, resourceco0_.COST_PER_HOUR as COST6_20_0_, resourceco0_.BASE_CURRENCY_CODE as BASE7_20_0_, resourceco0_.SRC_COST as SRC8_20_0_, resourceco0_.SRC_COST_CURRENCY_CODE as SRC9_20_0_, resourceco0_.BILLABLE_RATE as BILLABLE10_20_0_, resourceco0_.SRC_BILLABLE as SRC11_20_0_, resourceco0_.SRC_BILLABLE_CURRENCY_CODE as SRC12_20_0_ from RESOURCE_COST resourceco0_ where resourceco0_.RESOURCE_ID=? order by resourceco0_.start_date
2005-08-05 09:23:38,002 DEBUG [org.hibernate.SQL] select next_value from OBJECT_ID with lock
2005-08-05 09:23:38,018 DEBUG [org.hibernate.SQL] update OBJECT_ID set next_value = ? where next_value = ?
2005-08-05 09:23:38,049 DEBUG 2005-08-05 09:23:38,065 DEBUG [org.hibernate.SQL] select codedvalue0_.CODED_VALUE_ID as CODED1_0_, codedvalue0_.VSN as VSN4_0_, codedvalue0_.DESCRIPTION as DESCRIPT3_4_0_, codedvalue0_.MODIFIABLE as MODIFIABLE4_0_, codedvalue0_.DELETABLE as DELETABLE4_0_, codedvalue0_.SEQUENCE_NUMBER as SEQUENCE6_4_0_, codedvalue0_.START_DATE as START7_4_0_, codedvalue0_.END_DATE as END8_4_0_, codedvalue0_.CODE_TABLE_ID as CODE9_4_0_ from CODED_VALUE codedvalue0_ where codedvalue0_.CODED_VALUE_ID=?
2005-08-05 09:23:38,080 DEBUG [org.hibernate.SQL] select roles0_.RESOURCE_ID as RESOURCE1_1_, roles0_.ROLE_ID as ROLE2_1_, role1_.ROLE_ID as ROLE1_0_, role1_.VSN as VSN22_0_, role1_.NAME as NAME22_0_ from ROLE_RESOURCE roles0_ inner join ROLE_DEF role1_ on roles0_.ROLE_ID=role1_.ROLE_ID where roles0_.RESOURCE_ID=?
2005-08-05 09:23:38,080 DEBUG [org.hibernate.SQL] select projectrol0_.RESOURCE_ID as RESOURCE1_1_, projectrol0_.PROJECT_ROLE_ID as PROJECT2_1_, projectrol1_.PROJECT_ROLE_ID as PROJECT1_0_, projectrol1_.VSN as VSN15_0_, projectrol1_.NAME as NAME15_0_, projectrol1_.PROJECT_ID as PROJECT4_15_0_, projectrol1_.ROLE_TYPE as ROLE5_15_0_ from PROJECT_ROLE_RESOURCE projectrol0_ inner join PROJECT_ROLE projectrol1_ on projectrol0_.PROJECT_ROLE_ID=projectrol1_.PROJECT_ROLE_ID where projectrol0_.RESOURCE_ID=?
2005-08-05 09:23:38,112 INFO [com.ilign.ppm.dao.ResourceDaoHibernateTest] Rolled back transaction after test execution
Debug level Hibernate log excerpt:
Will provide if necessary but this is more an architectural question...