I'm trying to discover why two nearly identical class sets are behaving different from Hibernate 3's perspective. I'm fairly new to Hibernate in general and I'm hoping I'm missing something fairly obvious about the mappings or timing issues or something along those lines but I spent the whole day yesterday staring at the two sets and any differences that would lead to one being able to be persisted and the other not completely escaped me.
I appologize in advance for the length of this question but it all hinges around some pretty specific implementation details.
I have the following class mapped with Annotations and managed by Hibernate 3.? (if the specific specific version turns out to be pertinent, I'll figure out what it is). Java version is 1.6.
...
@Embeddable
public class JobStateChange implements Comparable<JobStateChange> {
@Temporal(TemporalType.TIMESTAMP)
@Column(nullable = false)
private Date date;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = JobState.FIELD_LENGTH)
private JobState state;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "acting_user_id", nullable = false)
private User actingUser;
public JobStateChange() {
}
@Override
public int compareTo(final JobStateChange o) {
return this.date.compareTo(o.date);
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof JobStateChange)) {
return false;
}
JobStateChange candidate = (JobStateChange) obj;
return this.state == candidate.state
&& this.actingUser.equals(candidate.getUser())
&& this.date.equals(candidate.getDate());
}
@Override
public int hashCode() {
return this.state.hashCode()
+ this.actingUser.hashCode()
+ this.date.hashCode();
}
}
It is mapped as a Hibernate CollectionOfElements in the class Job as follows:
...
@Entity
@Table(
name = "job",
uniqueConstraints = {
@UniqueConstraint(
columnNames = {
"agency", //Job Name
"payment_type", //Job Name
"payment_file", //Job Name
"date_of_payment",
"payment_control_number",
"truck_number"
})
})
public class Job implements Serializable {
private static final long serialVersionUID = -1131729422634638834L;
...
@org.hibernate.annotations.CollectionOfElements
@JoinTable(name = "job_state", joinColumns = @JoinColumn(name = "job_id"))
@Sort(type = SortType.NATURAL)
private final SortedSet<JobStateChange> stateChanges = new TreeSet<JobStateChange>();
...
public void advanceState(
final User actor,
final Date date) {
JobState nextState;
LOGGER.debug("Current state of {} is {}.", this, this.getCurrentState());
if (null == this.currentState) {
nextState = JobState.BEGINNING;
} else {
if (!this.isAdvanceable()) {
throw new IllegalAdvancementException(this.currentState.illegalAdvancementStateMessage);
}
if (this.currentState.isDivergent()) {
nextState = this.currentState.getNextState(this);
} else {
nextState = this.currentState.getNextState();
}
}
JobStateChange stateChange = new JobStateChange(nextState, actor, date);
this.setCurrentState(stateChange.getState());
this.stateChanges.add(stateChange);
LOGGER.debug("Advanced {} to {}", this, this.getCurrentState());
}
private void setCurrentState(final JobState jobState) {
this.currentState = jobState;
}
boolean isAdvanceable() {
return this.getCurrentState().isAdvanceable(this);
}
...
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
} else if (!(obj instanceof Job)) {
return false;
}
Job otherJob = (Job) obj;
return this.getName().equals(otherJob.getName())
&& this.getDateOfPayment().equals(otherJob.getDateOfPayment())
&& this.getPaymentControlNumber().equals(otherJob.getPaymentControlNumber())
&& this.getTruckNumber().equals(otherJob.getTruckNumber());
}
@Override
public int hashCode() {
return this.getName().hashCode()
+ this.getDateOfPayment().hashCode()
+ this.getPaymentControlNumber().hashCode()
+ this.getTruckNumber().hashCode();
}
...
}
The purpose of JobStateChange is to record when the Job moves through a series of State Changes that are outline in JobState as enums which know about advancement and decrement rules. The interface used to advance Jobs through a series of states is to call Job.advanceState() with a Date and a User. If the Job is advanceable according to rules coded in the enum, then a new StateChange is added to the SortedSet and everyone's happy. If not, an IllegalAdvancementException is thrown.
The DDL this generates is as follows:
...
drop table job;
drop table job_state;
...
create table job (
id bigint generated by default as identity,
current_state varchar(25),
date_of_payment date not null,
beginningCheckNumber varchar(8) not null,
item_count integer,
agency varchar(10) not null,
payment_file varchar(25) not null,
payment_type varchar(25) not null,
endingCheckNumber varchar(8) not null,
payment_control_number varchar(4) not null,
truck_number varchar(255) not null,
wrapping_system_type varchar(15) not null,
printer_id bigint,
primary key (id),
unique (agency, payment_type, payment_file, date_of_payment, payment_control_number, truck_number)
);
create table job_state (
job_id bigint not null,
acting_user_id bigint not null,
date timestamp not null,
state varchar(25) not null,
primary key (job_id, acting_user_id, date, state)
);
...
alter table job
add constraint FK19BBD12FB9D70
foreign key (printer_id)
references printer;
alter table job_state
add constraint FK57C2418FED1F0D21
foreign key (acting_user_id)
references app_user;
alter table job_state
add constraint FK57C2418FABE090B3
foreign key (job_id)
references job;
...
The database is seeded with the following data prior to running tests
...
insert into job (id, agency, payment_type, payment_file, payment_control_number, date_of_payment, beginningCheckNumber, endingCheckNumber, item_count, current_state, printer_id, wrapping_system_type, truck_number)
values (-3, 'RRB', 'Monthly', 'Monthly','4501','1998-12-01 08:31:16' , '00000001','00040000', 40000, 'UNASSIGNED', null, 'KERN', '02');
insert into job_state (job_id, acting_user_id, date, state)
values (-3, -1, '1998-11-30 08:31:17', 'UNASSIGNED');
...
After the database schema is automatically generated and rebuilt by the Hibernate tool.
The following test runs fine up until the call to Session.flush()
...
@ContextConfiguration(locations = { "/applicationContext-data.xml", "/applicationContext-service.xml" })
public class JobDaoIntegrationTest
extends AbstractTransactionalJUnit4SpringContextTests {
@Autowired
private JobDao jobDao;
@Autowired
private SessionFactory sessionFactory;
@Autowired
private UserService userService;
@Autowired
private PrinterService printerService;
...
@Test
public void saveJob_JobAdvancedToAssigned_AllExpectedStateChanges() {
//Get an unassigned Job
Job job = this.jobDao.getJob(-3L);
assertEquals(JobState.UNASSIGNED, job.getCurrentState());
Date advancedToUnassigned = new GregorianCalendar(1998, 10, 30, 8, 31, 17).getTime();
assertEquals(advancedToUnassigned, job.getStateChange(JobState.UNASSIGNED).getDate());
//Satisfy advancement constraints and advance
job.setPrinter(this.printerService.getPrinter(-1L));
Date advancedToAssigned = new Date();
job.advanceState(
this.userService.getUserByUsername("admin"),
advancedToAssigned);
assertEquals(JobState.ASSIGNED, job.getCurrentState());
assertEquals(advancedToUnassigned, job.getStateChange(JobState.UNASSIGNED).getDate());
assertEquals(advancedToAssigned, job.getStateChange(JobState.ASSIGNED).getDate());
//Persist to DB
this.sessionFactory.getCurrentSession().flush();
...
}
...
}
The error thrown is SQLCODE=-803, SQLSTATE=23505:
could not insert collection rows: [jaci.model.job.Job.stateChanges#-3]
org.hibernate.exception.ConstraintViolationException: could not insert collection rows: [jaci.model.job.Job.stateChanges#-3]
at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:94)
at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
at org.hibernate.persister.collection.AbstractCollectionPersister.insertRows(AbstractCollectionPersister.java:1416)
at org.hibernate.action.CollectionUpdateAction.execute(CollectionUpdateAction.java:86)
at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:279)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:263)
at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:170)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:50)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1027)
at jaci.dao.JobDaoIntegrationTest.saveJob_JobAdvancedToAssigned_AllExpectedStateChanges(JobDaoIntegrationTest.java:98)
at org.springframework.test.context.junit4.SpringTestMethod.invoke(SpringTestMethod.java:160)
at org.springframework.test.context.junit4.SpringMethodRoadie.runTestMethod(SpringMethodRoadie.java:233)
at org.springframework.test.context.junit4.SpringMethodRoadie$RunBeforesThenTestThenAfters.run(SpringMethodRoadie.java:333)
at org.springframework.test.context.junit4.SpringMethodRoadie.runWithRepetitions(SpringMethodRoadie.java:217)
at org.springframework.test.context.junit4.SpringMethodRoadie.runTest(SpringMethodRoadie.java:197)
at org.springframework.test.context.junit4.SpringMethodRoadie.run(SpringMethodRoadie.java:143)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.invokeTestMethod(SpringJUnit4ClassRunner.java:160)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:97)
Caused by: com.ibm.db2.jcc.b.lm: DB2 SQL Error: SQLCODE=-803, SQLSTATE=23505, SQLERRMC=1;ACI_APP.JOB_STATE, DRIVER=3.50.152
at com.ibm.db2.jcc.b.wc.a(wc.java:575)
at com.ibm.db2.jcc.b.wc.a(wc.java:57)
at com.ibm.db2.jcc.b.wc.a(wc.java:126)
at com.ibm.db2.jcc.b.tk.b(tk.java:1593)
at com.ibm.db2.jcc.b.tk.c(tk.java:1576)
at com.ibm.db2.jcc.t4.db.k(db.java:353)
at com.ibm.db2.jcc.t4.db.a(db.java:59)
at com.ibm.db2.jcc.t4.t.a(t.java:50)
at com.ibm.db2.jcc.t4.tb.b(tb.java:200)
at com.ibm.db2.jcc.b.uk.Gb(uk.java:2355)
at com.ibm.db2.jcc.b.uk.e(uk.java:3129)
at com.ibm.db2.jcc.b.uk.zb(uk.java:568)
at com.ibm.db2.jcc.b.uk.executeUpdate(uk.java:551)
at org.hibernate.jdbc.NonBatchingBatcher.addToBatch(NonBatchingBatcher.java:46)
at org.hibernate.persister.collection.AbstractCollectionPersister.insertRows(AbstractCollectionPersister.java:1389)
Therein lies my problem… A nearly identical Class set (in fact, so identical that I've been chomping at the bit to make it a single class that serves both business entities) runs absolutely fine. It is identical except for name. Instead of Job it's Web. Instead of JobStateChange it's WebStateChange. Instead of JobState it's WebState. Both Job and Web's SortedSet of StateChanges are mapped as a Hibernate CollectionOfElements. Both are @Embeddable. Both are SortType.Natural. Both are backed by an Enumeration with some advancement rules in it. And yet when a nearly identical test is run for Web, no issue is discovered and the data flushes fine. For the sake of brevity I won't include all of the Web classes here, but I will include the test and if anyone wants to see the actual sources, I'll include them (just leave a comment).
The data seed:
insert into web (id, stock_type, pallet, pallet_id, date_received, first_icn, last_icn, shipment_id, current_state)
values (-1, 'PF', '0011', 'A', '2008-12-31 08:30:02', '000000001', '000080000', -1, 'UNSTAGED');
insert into web_state (web_id, date, state, acting_user_id)
values (-1, '2008-12-31 08:30:03', 'UNSTAGED', -1);
The test:
...
@ContextConfiguration(locations = { "/applicationContext-data.xml", "/applicationContext-service.xml" })
public class WebDaoIntegrationTest
extends AbstractTransactionalJUnit4SpringContextTests {
@Autowired
private WebDao webDao;
@Autowired
private UserService userService;
@Autowired
private SessionFactory sessionFactory;
...
@Test
public void saveWeb_WebAdvancedToNewState_AllExpectedStateChanges() {
Web web = this.webDao.getWeb(-1L);
Date advancedToUnstaged = new GregorianCalendar(2008, 11, 31, 8, 30, 3).getTime();
assertEquals(WebState.UNSTAGED, web.getCurrentState());
assertEquals(advancedToUnstaged, web.getState(WebState.UNSTAGED).getDate());
Date advancedToStaged = new Date();
web.advanceState(
this.userService.getUserByUsername("admin"),
advancedToStaged);
this.sessionFactory.getCurrentSession().flush();
web = this.webDao.getWeb(web.getId());
assertEquals(
"Web should have moved to STAGED State.",
WebState.STAGED,
web.getCurrentState());
assertEquals(advancedToUnstaged, web.getState(WebState.UNSTAGED).getDate());
assertEquals(advancedToStaged, web.getState(WebState.STAGED).getDate());
assertNotNull(web.getState(WebState.UNSTAGED));
assertNotNull(web.getState(WebState.STAGED));
}
...
}
As you can see, I assert that the Web was reconstituted the way I expect, I advance it, flush it to the DB, and then re-get it and verify that the states are as I expect. Everything works perfectly. Not so with Job.
A possibly pertinent detail: the reconstitution code works fine if I cease to map JobStateChange.data as a TIMESTAMP and instead as a DATE, and ensure that all of the StateChanges always occur on different Dates. The problem is that this particular business entity can go through many state changes in a single day and so it needs to be sorted by time stamp rather than by date. If I don't do this then I can't sort the StateChanges correctly. That being said, WebStateChange.date is also mapped as a TIMESTAMP and so I again remain absolutely befuddled as to where this error is arising from.
I tried to do a fairly thorough job of giving all of the technical details of the implementation but as this particular question is very implementation specific, if I missed anything just let me know in the comments and I'll include it.
Thanks so much for your help!
UPDATE: Since it turns out to be important to the solution of my problem, I have to include the pertinent bits of the WebStateChange class as well.
...
@Embeddable
public class WebStateChange implements Comparable<WebStateChange> {
@Temporal(TemporalType.TIMESTAMP)
@Column(nullable = false)
private Date date;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = WebState.FIELD_LENGTH)
private WebState state;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "acting_user_id", nullable = false)
private User actingUser;
...
WebStateChange(
final WebState state,
final User actingUser,
final Date date) {
ExceptionUtils.illegalNullArgs(state, actingUser, date);
this.state = state;
this.actingUser = actingUser;
this.date = new Date(date.getTime());
}
@Override
public int compareTo(final WebStateChange otherStateChange) {
return this.date.compareTo(otherStateChange.date);
}
@Override
public boolean equals(final Object candidate) {
if (this == candidate) {
return true;
} else if (!(candidate instanceof WebStateChange)) {
return false;
}
WebStateChange candidateWebState = (WebStateChange) candidate;
return this.getState() == candidateWebState.getState()
&& this.getUser().equals(candidateWebState.getUser())
&& this.getDate().equals(candidateWebState.getDate());
}
@Override
public int hashCode() {
return this.getState().hashCode()
+ this.getUser().hashCode()
+ this.getDate().hashCode();
}
...
}