Hibernate avec H2/GeoDB
Par Cédric Tabin le samedi 01.09.2018, 12:00 - Java - Lien permanent
En travaillant sur un serveur embarqué Java EE avec payara et H2/Geodb (car j'ai besoin d'utiliser des données spatiales), il y a une incompatibilité entre les APIs utilisées qui provoque un crash à la récupération.
Pour faire les choses simples, j'ai une entité basiques qui contient les champs suivants:
@Entity @Table(name = "db_location") public class Location { @Id @Column(name = "ID") private String id; @Column(name = "NAME") private String name; @Column(name = "coordinates") private Geometry coordinates; }
Notez bien que j'utilise ici le type com.vividsolutions.jts.geom.Geometry. Et j'ai ensuite le petit bout de code suivant qui me permet de créer et persister mon entité:
GeometryFactory gf = new GeometryFactory(); LineString ls = gf.createLineString(new Coordinate[]{ new Coordinate(100, 10), new Coordinate(10, 100) }); Location loc = new Location("ID001"); loc.setName("Test"); loc.setCoordinates(ls); entityManager.persist(loc); /* OK */
Jusqu'ici, tout va bien, les données sont bien enregistrées en base de données et le programme continue.
Mais lors de la récupération, drame:
Query q = entityManager.createQuery("SELECT p FROM Location p WHERE p.id='ID001'"); Location loc = (Location)q.getSingleResult(); //Exception
java.lang.NoClassDefFoundError: org/locationtech/jts/geom/Geometry
Une erreur remonte depuis le fin fond d'Hibernate comme quoi la définition de org.locationtech.jts.geom.Geometry n'existe pas.
Ce qu'il faut savoir, c'est que la librairie org.locationtech.jts et l'évolution de com.vividsolutions.jts et visiblement, cette dernière n'a pas été inclue dans Hibernate (cf ce ticket).
Donc à partir de ce moment-là, une solution manuelle doit être trouvée pour contourner ce souci, sinon il ne sera pas possible d'utiliser H2/GeoDB et il faudra abandonner les colonnes de type spatial. Et évidemment, simplement inclure la librairie org.locationtech.jts va conduire à l'erreur suivante
Can't convert database object of type org.locationtech.jts.geom.Geometry
En fouillant dans le code source de org.hibernate.spatial.dialect.h2geodb.GeoDBDialect, j'ai trouvé différents indices qui m'ont permis de trouver une solution propre et générique. A savoir, la classe GeoDbWkb ne gère pas la conversion depuis un objet org.locationtech.jts, il faut donc prendre la main en amont afin d'effectuer la transformation manuellement. Et c'est assez simple à faire !
La première chose est de créer un dialect spécial pour notre cas dans persistence.xml:
<property name="hibernate.dialect" value="demo.CustomGeoDBDialect" />
Et voici, à quoi ressemble ma classe:
package demo; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import java.sql.CallableStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.logging.Logger; import org.hibernate.boot.model.TypeContributions; import org.hibernate.service.ServiceRegistry; import org.hibernate.spatial.JTSGeometryType; import org.hibernate.spatial.dialect.h2geodb.GeoDBDialect; import org.hibernate.spatial.dialect.h2geodb.GeoDBGeometryTypeDescriptor; import org.hibernate.spatial.dialect.h2geodb.GeoDbWkb; import org.hibernate.type.StringType; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaTypeDescriptor; import org.hibernate.type.descriptor.sql.BasicExtractor; public class CustomGeoDBDialect extends GeoDBDialect { private static final InternalGeoDBGeometryTypeDescriptor INTERNAL_TYPEDESC_INSTANCE = new InternalGeoDBGeometryTypeDescriptor(); @Override public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.contributeTypes( typeContributions, serviceRegistry ); typeContributions.contributeType( new JTSGeometryType( INTERNAL_TYPEDESC_INSTANCE ) ); } } class InternalGeoDBGeometryTypeDescriptor extends GeoDBGeometryTypeDescriptor { private static final Logger log = Logger.getLogger(InternalGeoDBGeometryTypeDescriptor.class.getName()); @Override public <X> ValueExtractor<X> getExtractor(JavaTypeDescriptor<X> javaTypeDescriptor) { return new BasicExtractor<X>( javaTypeDescriptor, this ) { @Override protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { Object obj = convertLocationtechToVivid( rs.getObject( name ) ); return getJavaDescriptor().wrap( GeoDbWkb.from( obj ), options ); } @Override protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { Object obj = convertLocationtechToVivid( statement.getObject( index ) ); return getJavaDescriptor().wrap( GeoDbWkb.from( obj ), options ); } @Override protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { Object obj = convertLocationtechToVivid( statement.getObject( name ) ); return getJavaDescriptor().wrap( GeoDbWkb.from( obj ), options ); } //Conversion de org.locationtech vers com.vividsolutions private Object convertLocationtechToVivid(Object geom) { if(geom==null) { return null; } if(geom instanceof org.locationtech.jts.geom.LineString) { org.locationtech.jts.geom.LineString lg = (org.locationtech.jts.geom.LineString)geom; org.locationtech.jts.geom.Coordinate[] ocs = lg.getCoordinates(); Coordinate[] cs = new Coordinate[ocs.length]; for(int i=0 ; i<cs.length ; ++i) { org.locationtech.jts.geom.Coordinate oc = ocs[i]; cs[i] = new Coordinate(oc.x, oc.y, oc.z); } GeometryFactory gf = new GeometryFactory(); LineString ls = gf.createLineString(cs); return ls; } else if(geom instanceof org.locationtech.jts.geom.LineString) { //pour gérer les autres type de conversion, il faut rajouter les cas ici throw new RuntimeException("Conversion not supported: "+geom.getClass().getName()); } return geom; } }; } }
Vous l'aurez compris, j'ai juste rajouté le traitement dans la fonction convertLocationtechToVivid avant de laisser la suite du traitement s'effectuer comme normalement. Je n'ai pris en compte que la transformation du LineString puisque c'est le seul objet que nous utilisons dans notre application, mais il devrait être assez simple de faire le même genre de conversion pour les autres objets.