I'm not sure if this is the result of an error in my mapping file/code, or if I have stumbled across a bug in Hibernate. Any help would be appreciated.
I'm trying to map a class which has a Map collection property. This Map contains name/value pairs which are stored in another table, and joined by a foreign key column.
The tables in question are as follows:
Code:
SQL> desc t_identifier;
Name Null? Type
----------------------------------------- -------- ----------------------------
ID_KEY NOT NULL NUMBER(12)
ID NOT NULL RAW(64)
ID_CI NOT NULL NUMBER(5)
ID_TYPE NOT NULL NUMBER(4)
ATTRIBUTE_SET_ID NUMBER(12)
SQL> desc t_attribute_set;
Name Null? Type
----------------------------------------- -------- ----------------------------
ATTRIBUTE_SET_ID NOT NULL NUMBER(12)
ATTRIBUTE_NAME NOT NULL VARCHAR2(60)
ATTRIBUTE_VALUE NOT NULL VARCHAR2(1000)
An "attribute set" is a collection of N unique name/value pairs, and every name/value pair in the set has the same attribute_set_id. In other words the primary key of t_attribute_set is (attribute_set_id, attribute_name).
Note that the Map is an optional property, meaning the foreign key column pointing to the Map table (i.e. t_identifier.attribute_set_id) is nullable.
My mapping file is as follows:
Code:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="com.xxx.common.dto.IdentifierDTO" table="t_identifier">
<id name="idKey" column="id_key">
<generator class="sequence">
<param name="sequence">seq_id_key</param>
</generator>
</id>
<property name="id" column="id" type="binary"/>
<property name="cryptoIndex" column="id_ci"/>
<property name="idType" column="id_type"/>
<property name="attributeSetID" column="attribute_set_id" not-null="false"/>
<!-- attribute set -->
<map name="attributeSet" table="t_attribute_set" lazy="true" cascade="all" inverse="false">
<key property-ref="attributeSetID" column="attribute_set_id" not-null="false" update="false"/>
<map-key column="attribute_name" type="string"/>
<element column="attribute_value" type="string" not-null="true"/>
</map>
</class>
</hibernate-mapping>
And my Hibernate class is as follows:
Code:
package com.xxx.common.dto;
(Note: imports ommitted for simplicity)
public class IdentifierDTO extends DTO {
private static final Crypto crypto;
static {
crypto = ApplicationContext.getContext().getCrypto();
}
private long idKey;
private byte[] id;
private int cryptoIndex;
private int idType;
private Long attributeSetID;
private Map<String, String> attributeSet;
public IdentifierDTO() { }
public Long getAttributeSetID() {
return attributeSetID;
}
public void setAttributeSetID(Long attributeSetID) {
this.attributeSetID = attributeSetID;
}
public int getCryptoIndex() {
return cryptoIndex;
}
public void setCryptoIndex(int cryptoIndex) {
this.cryptoIndex = cryptoIndex;
}
public byte[] getId() {
return id;
}
public void setId(byte[] id) {
this.id = id;
}
public long getIdKey() {
return idKey;
}
public void setIdKey(long idKey) {
this.idKey = idKey;
}
public int getIdType() {
return idType;
}
public void setIdType(int idType) {
this.idType = idType;
}
public Map getAttributeSet() {
return attributeSet;
}
public void setAttributeSet(Map<String, String> attributeSet) {
this.attributeSet = attributeSet;
}
public void addAttribute(String key, String value) {
if (attributeSet == null) {
attributeSet = new HashMap<String, String>();
DAOFactory daoFactory = DAOFactory.get();
AttributeSetDAO attributeSetDAO = daoFactory.getDAO(AttributeSetDAO.class);
this.setAttributeSetID(attributeSetDAO.getNextAttributeSetID());
}
attributeSet.put(key, value);
}
}
So when *both* the attributeSetID and attributeSet properties of a IdentifierDTO instance are null, everything is fine when I flush a Session. However, when I actually set the attributeSetID property and add name/value pairs to the Map so that it's not null, I get a NullPointerException when doing a Session.flush. Here's the test code in question:
Code:
*** UNIT TEST MAIN CODE ***
public void testUpdate(){
try{
/* get test DTO */
T dto = getTestDTO();
/* save this DTO */
DAO<T> dao = getDAO();
dao.save(dto);
/* flush and clear the hibernate session */
DAOFactory.flushAndClear();
/* find this dto */
dto = (T)dao.find(getPrimaryId(dto));
/* update this DTO */
modifyDTO(dto);
dao.update(dto);
/* flush and clear the hibernate session */
DAOFactory.flushAndClear(); // ***THIS IS WHERE THE EXCEPTION OCCURS, when Session.flush() is called***
/* find this dto */
T dto2 = (T)dao.find(getPrimaryId(dto));
/* verify that the DTOs are equal */
assertEqualsDTO(dto, dto2);
} catch(ObjectNotFoundException e) {
fail(e.getMessage());
} catch (Throwable e){
logger.log(Level.SEVERE, "Failure in testUpdate", e);
throw new RuntimeException(e);
} finally {
/* rollback so that we can re-run this test */
DAOFactory.rollback();
DAOFactory.close();
}
*** CODE IN modifyDTO() ***
protected void modifyDTO(IdentifierDTO id) {
// create a new attribute set
id.addAttribute("whenRecieved", new Timestamp(System.currentTimeMillis()).toString());
id.addAttribute("special", "false");
}
When I do this I get the following exception:
Code:
FINE: listing entities:
Dec 21, 2005 1:28:39 PM org.hibernate.pretty.Printer toString
FINE: com.xxx.common.dto.IdentifierDTO{cryptoIndex=100, attributeSetID=305, idKey=3010, id=b1b2b3b4b5b6b7b8b9b0b1b2b3b4b5b6, attributeSet=[false, 2005-12-21 13:28:38.85], idType=1}
Dec 21, 2005 1:28:39 PM org.hibernate.event.def.AbstractFlushingEventListener performExecutions
FINEST: executing flush
Dec 21, 2005 1:28:39 PM com.xxx.common.dao.DAOTest testUpdate
SEVERE: Failure in testUpdate
java.lang.NullPointerException
at org.hibernate.type.AbstractType.getHashCode(AbstractType.java:111)
at org.hibernate.type.AbstractType.getHashCode(AbstractType.java:119)
at org.hibernate.cache.CacheKey.<init>(CacheKey.java:40)
at org.hibernate.action.CollectionAction.beforeExecutions(CollectionAction.java:73)
at org.hibernate.engine.ActionQueue.prepareActions(ActionQueue.java:246)
at org.hibernate.engine.ActionQueue.prepareActions(ActionQueue.java:152)
at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingE
ventListener.java:273)
at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:
27)
at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:730)
at com.xxx.common.dao.DAOFactory.flushAndClear(DAOFactory.java:189)
at com.xxx.common.dao.DAOTest.testUpdate(DAOTest.java:95)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.codehaus.surefire.battery.JUnitBattery.executeJUnit(JUnitBattery.java:246)
at org.codehaus.surefire.battery.JUnitBattery.execute(JUnitBattery.java:220)
at org.codehaus.surefire.Surefire.executeBattery(Surefire.java:204)
at org.codehaus.surefire.Surefire.run(Surefire.java:153)
at org.codehaus.surefire.Surefire.run(Surefire.java:77)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.codehaus.surefire.SurefireBooter.run(SurefireBooter.java:104)
at org.apache.maven.test.SurefirePlugin.execute(SurefirePlugin.java:303)
at org.apache.maven.plugin.DefaultPluginManager.executeMojo(DefaultPluginManager.java:399)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoals(DefaultLifecycleExecutor
.java:519)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoalWithLifecycle(DefaultLifec
ycleExecutor.java:469)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoal(DefaultLifecycleExecutor.
java:448)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoalAndHandleFailures(DefaultL
ifecycleExecutor.java:301)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeTaskSegments(DefaultLifecycleE
xecutor.java:268)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.execute(DefaultLifecycleExecutor.java
:137)
at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:316)
at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:113)
at org.apache.maven.cli.MavenCli.main(MavenCli.java:249)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.codehaus.classworlds.Launcher.launchEnhanced(Launcher.java:315)
at org.codehaus.classworlds.Launcher.launch(Launcher.java:255)
at org.codehaus.classworlds.Launcher.mainWithExitCode(Launcher.java:430)
at org.codehaus.classworlds.Launcher.main(Launcher.java:375)
Notice when it lists the entities prior to executing the flush() above that both attributeSetID and the attributeSet Map object are both not null, and the attributeSet Map has values in it. I added some debugging code to the org.hibernate.cache.CacheKey constructor and it is failing when trying to create a CacheKey for the attributeSet property of IdentifierDTO. It is the "Serializable id" parameter (i.e. CacheKey.key) that is null.
So why is the key null? Is it because the primary key of t_attribute_set is NOT just the attribute_set_id foreign key column in t_identifier, and I have to do some kind of composite-id type mapping? Is it a bug in Hibernate? Is this kind of mapping not possible in Hibernate?
Any and all help greatly appreciated. Thanks in advance.