Hibernate permet de faire du mapping d’héritage de classe. Selon la stratégie utilisée, le développeur peut rencontrer quelques difficultés.

Hibernate propose trois stratégies de mapping d’héritage de classe :

  • une table par hiérarchie de classe
  • une table par classe fille
  • une table par classe concrète

L’objectif ici n’est pas d’expliquer la différence entre chaque solution, mais seulement de présenter un problème qu’il est possible de rencontrer lorsque la stratégie une table par hiérarchie de classe est utilisée avec un discriminant de type entier.

En prenant le cas d’une table listant des employés avec une colonne spécifiant le type d’employé, le fichier de mapping peut ressembler à ceci :

<?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 package="com.company.project.dto">
	<class name="Employee" table="EMPLOYEE">
		<id name="Id" type="int">
			<column name="E_ID" sql-type="int(4)" />
			<generator class="increment" />
		</id>
		<discriminator column="E_TYPE" type="integer" />
		<property name="Name" type="string">
			<column name="E_NAME" sql-type="char(20)" not-null="false" />
		</property>
		<subclass name="Manager" discriminator-value="1" />
		<subclass name="Salesman" discriminator-value="2" />
		<subclass name="Accountant" discriminator-value="3" />
		<subclass name="Secretary" discriminator-value="4" />
	</class> </hibernate-mapping>

Or, lors de la consultation d’enregistrements présents en base de données, l’exception suivante est levée :

org.hibernate.MappingException: Could not format discriminator value to SQL string
	at org.hibernate.persister.entity.SingleTableEntityPersister.<init>(SingleTableEntityPersister.java:284)
	at org.hibernate.persister.PersisterFactory.createClassPersister(PersisterFactory.java:55)
	at org.hibernate.impl.SessionFactoryImpl.<init>(SessionFactoryImpl.java:215)
	at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1164)
	at com.company.project.HibernateUtil.currentSession(HibernateUtil.java:40)
	at com.company.project.dao.EmployeeDAO.displayEmployee(EmployeeDAO.java:162)
	at EmployeeTest.testDisplay(EmployeeTest.java:25)
	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:324)
	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 org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Caused by: java.lang.NumberFormatException: For input string: "com.company.project.dto.Employee"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
	at java.lang.Integer.parseInt(Integer.java:468)
	at java.lang.Integer.<init>(Integer.java:609)
	at org.hibernate.type.IntegerType.stringToObject(IntegerType.java:55)
	at org.hibernate.persister.entity.SingleTableEntityPersister.<init>(SingleTableEntityPersister.java:277)
	... 21 more

Afin d’initialiser le discriminant, Hibernate tente de charger un entier qui a pour valeur com.company.project.dto.Employee. Cette valeur est étrange. Afin de palier au problème, il est possible de modifier le type du discriminant et de le passer en chaîne de caractères. Ce qui donne le fichier de mapping suivant :

<?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 package="com.company.project.dto">
	<class name="Employee" table="EMPLOYEE">
		<id name="Id" type="int">
			<column name="E_ID" sql-type="int(4)" />
			<generator class="increment" />
		</id>
		<discriminator column="E_TYPE" type="string" />
		<property name="Name" type="string">
			<column name="E_NAME" sql-type="char(20)" not-null="false" />
		</property>
		<subclass name="Manager" discriminator-value="1" />
		<subclass name="Salesman" discriminator-value="2" />
		<subclass name="Accountant" discriminator-value="3" />
		<subclass name="Secretary" discriminator-value="4" />
	</class> </hibernate-mapping>

La consultation des enregistrements se déroule correctement. Par contre, il est impossible de créer un nouvel enregistrement. En effet lors de l’enregistrement d’un nouvel objet, l’exception suivante est levée :

org.hibernate.exception.SQLGrammarException: could not insert: [com.company.project.dto.Salesman]
	at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:65)
	at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
	at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2078)
	at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2427)
	at org.hibernate.action.EntityInsertAction.execute(EntityInsertAction.java:51)
	at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:243)
	at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:227)
	at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:140)
	at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:296)
	at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
	at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1007)
	at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:354)
	at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
	at com.company.project.dao.EmployeeDAO.createSalesman(EmployeeDAO.java:136)
	at EmployeeTest.testCreateSalesman(EmployeeTest.java:82)
	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:324)
	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 org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)
Caused by: com.sybase.jdbc2.jdbc.SybSQLException: Implicit conversion from datatype 'VARCHAR' to 'SMALLINT' is not allowed.  Use the CONVERT function to run this query.
	at com.sybase.jdbc2.tds.Tds.processEed(Tds.java:2636)
	at com.sybase.jdbc2.tds.Tds.nextResult(Tds.java:1996)
	at com.sybase.jdbc2.jdbc.ResultGetter.nextResult(ResultGetter.java:69)
	at com.sybase.jdbc2.jdbc.SybStatement.nextResult(SybStatement.java:204)
	at com.sybase.jdbc2.jdbc.SybStatement.nextResult(SybStatement.java:187)
	at com.sybase.jdbc2.jdbc.SybStatement.updateLoop(SybStatement.java:1615)
	at com.sybase.jdbc2.jdbc.SybStatement.executeUpdate(SybStatement.java:1598)
	at com.sybase.jdbc2.jdbc.SybPreparedStatement.executeUpdate(SybPreparedStatement.java:89)
	at craftsman.spy.SpyPreparedStatement.executeUpdate(SpyPreparedStatement.java:219)
	at org.hibernate.jdbc.NonBatchingBatcher.addToBatch(NonBatchingBatcher.java:23)
	at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2062)
	... 27 more

La base de données ne veut pas convertir la chaîne de caractères fournie pour la colonne du discriminant en entier. Il est possible de modifier le modèle de la base de données en changant le type du discriminant. Mais ce genre d’opération n’est pas toujours possible.

La solution est la suivante. Il faut conserver le type entier pour le discriminant car la base de données n’acceptera rien d’autre. Avec un discrimimant de type entier, Hibernate charge des objets dont les discriminants ont pour valeur le nom de la classe mère. Pour résoudre ce problème, il suffit de spécifier une valeur de discriminant pour la classe mère. Le fichier de mapping ressemblera donc à :

<?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 package="com.company.project.dto">
	<class name="Employee" table="EMPLOYEE" discriminator-value="0">
		<id name="Id" type="int">
			<column name="E_ID" sql-type="int(4)" />
			<generator class="increment" />
		</id>
		<discriminator column="E_TYPE" type="integer" />
		<property name="Name" type="string">
			<column name="E_NAME" sql-type="char(20)" not-null="false" />
		</property>
		<subclass name="Manager" discriminator-value="1" />
		<subclass name="Salesman" discriminator-value="2" />
		<subclass name="Accountant" discriminator-value="3" />
		<subclass name="Secretary" discriminator-value="4" />
	</class> </hibernate-mapping>

Même si si cette valeur de discriminant pour la classe mère n’est jamais utilisé (et donc jamais présent en base de données), il faut procéder de la sorte.

Est-ce la bonne méthode ou est-ce un bug d’Hibernate ? Dans tous les cas ce mapping fonctionne très bientôt !