I have an Oracle table with an SDO_GEOMETRY column and I am trying to use EclipseLink JPA2 2.6.1 for persistence. My entity class uses a JTS Geometry for geometry objects and I have written a AttributeConverter to convert from a SDO_GEOMETRY to a JTS Geometry. This works well and I can read and write the geometries from the database. The problem I am having is that I cannot persist a null JTS Geometry. I get the following error:
ORA-00932: inconsistent datatypes: expected MDSYS.SDO_GEOMETRY got CHAR
Not sure if I am doing something wrong or if there is a bug in EclipseLink or Oracle.
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="mainPersistence">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>persistence.v1.dao.jpa2.converters.GeometryConverter</class>
<class>persistence.v1.dto.AuthorizationDto</class>
<properties>
<property name="eclipselink.weaving" value="false"/>
</persistence-unit>
Entity class
package persistence.v1.dto;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import persistence.v1.dao.jpa2.converters.GeometryConverter;
import com.vividsolutions.jts.geom.Geometry;
@Entity
@Table(name="AUTHORIZATION")
public class AuthorizationDto {
private String authorizationGuid;
private Geometry authorizationGeometry;
public AuthorizationDto() {
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "system-uuid")
@Column(name="AUTHORIZATION_GUID", nullable=false)
public String getAuthorizationGuid() {
return this.authorizationGuid;
}
public void setAuthorizationGuid(String authorizationGuid) {
this.authorizationGuid = authorizationGuid;
}
@javax.persistence.Convert(converter=GeometryConverter.class)
@Column(name="AUTHORIZATION_GEOMETRY")
public Geometry getAuthorizationGeometry() {
return this.authorizationGeometry;
}
public void setAuthorizationGeometry(Geometry authorizationGeometry) {
this.authorizationGeometry = authorizationGeometry;
}
}
GeometryConverter class
package persistence.v1.dao.jpa2.converters;
import java.sql.SQLException;
import java.sql.Struct;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import oracle.jdbc.OracleConnection;
import oracle.sql.STRUCT;
import oracle.sql.StructDescriptor;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.oracle.OraReader;
import com.vividsolutions.jts.io.oracle.OraWriter;
@Converter(autoApply = true)
public class GeometryConverter implements AttributeConverter<Geometry, Object> {
private static ThreadLocal<OracleConnection> currentConnection = new ThreadLocal<>();
public static void setConnection(OracleConnection connection) {
currentConnection.set(connection);
}
private Geometry toGeometry(Object geometryData) {
Geometry result = null;
OraReader reader = new OraReader();
try {
StructDescriptor descriptor = new StructDescriptor(
"MDSYS.SDO_GEOMETRY", currentConnection.get());
STRUCT geometryStruct = new STRUCT(descriptor,
currentConnection.get(), (Object[]) geometryData);
result = reader.read(geometryStruct);
} catch (SQLException e) {
logger.warn("Cound not create geometry from database column", e);
throw new RuntimeException(e);
}
return result;
}
private Struct fromGeometry(Geometry geometry) {
try {
return new OraWriter().write(geometry, currentConnection.get());
} catch (SQLException e) {
logger.warn("Cound not create database column from geometry "
+ geometry.toText(), e);
throw new RuntimeException(e);
}
}
@Override
public Object convertToDatabaseColumn(Geometry geometry) {
logger.debug("<convertToDatabaseColumn");
Object result = null;
if(geometry!=null) {
result = fromGeometry(geometry);
}
logger.debug(">convertToDatabaseColumn "+result);
return result;
}
@Override
public Geometry convertToEntityAttribute(Object geometryData) {
logger.debug("<convertToEntityAttribute");
Geometry result = null;
if(geometryData!=null) {
result = toGeometry(geometryData);
}
logger.debug(">convertToEntityAttribute "+result);
return result;
}
}
Thanks
The exception is being caused by EclipseLink using VARCHAR as the default for unknown types. Oddly the AttibuteConverter.convertToEntityAttribute method receives an Object array instead of a Struct for the SDO_GEOMETRY type but expects a Struct to be returned by the AttibuteConverter.convertToDatabaseColumn method. This is probably a symptom of the underlying problem. There may be a way to tell EclipseLink what the type is using annotations or some other configuration but I could not discover how so this is my workaround.
I created an EclipseLink SessionEventListener that uses the prelogin method to identify Entity methods that return the Geometry type. I use the DatabaseField object to create a new ObjectRelationalDatabaseField then I set the sqlType property to Struct and the sqlTypeName property to "MDSYS.SDO_GEOMETRY". then I update the mapping with the new ObjectRelationalDatabaseField object. The EclipseLink code now has enough information to correctly set the Statement.setNull (String parameterName, int sqlType, String typeName) method.
The SessionEventListener is configured in the persistence.xml and the null insert is now successful.
GeometryInitializerSessionEventListener class
package persistence.v1.dao.jpa2.listeners;
import java.lang.reflect.Method;
import java.sql.Types;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.descriptors.MethodAttributeAccessor;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.DirectToFieldMapping;
import org.eclipse.persistence.mappings.structures.ObjectRelationalDatabaseField;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.sessions.SessionEvent;
import org.eclipse.persistence.sessions.SessionEventListener;
import com.vividsolutions.jts.geom.Geometry;
public class GeometryInitializerSessionEventListener implements SessionEventListener {
// Omitting empty interface methods
@Override
public void preLogin(SessionEvent event) {
Session s = event.getSession();
@SuppressWarnings("rawtypes")
Map<Class, ClassDescriptor> descriptorMap = s.getDescriptors();
for (@SuppressWarnings("rawtypes") Entry<Class, ClassDescriptor> entry : descriptorMap.entrySet()) {
Class<?> cls = entry.getKey();
ClassDescriptor desc = entry.getValue();
Vector<DatabaseMapping> mappings = desc.getMappings();
for(DatabaseMapping mapping:mappings) {
if (mapping.getAttributeAccessor() instanceof MethodAttributeAccessor) {
MethodAttributeAccessor maa = (MethodAttributeAccessor) mapping.getAttributeAccessor();
String methodName = maa.getGetMethodName();
Method method;
try {
method = cls.getMethod(methodName, new Class[]{});
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (SecurityException e) {
throw new RuntimeException(e);
}
Class<?> returnType = method.getReturnType();
if(Geometry.class.equals(returnType)) {
DirectToFieldMapping directToFieldMapping = (DirectToFieldMapping) mapping;
ObjectRelationalDatabaseField objectRelationalDatabaseField = new ObjectRelationalDatabaseField(mapping.getField());
objectRelationalDatabaseField.setSqlType(Types.STRUCT);
objectRelationalDatabaseField.setSqlTypeName("MDSYS.SDO_GEOMETRY");
directToFieldMapping.setField(objectRelationalDatabaseField);
}
}
}
}
}
}
Persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="mainPersistence">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider
<class>persistence.v1.dao.jpa2.converters.GeometryConverter</class>
<class>persistence.v1.dto.AuthorizationDto</class>
<properties>
<property name="eclipselink.weaving" value="false"/>
<property name="eclipselink.session-event-listener" value="persistence.v1.dao.jpa2.listeners.GeometryInitializerSessionEventListener"/>
</properties>
</persistence-unit>