PostGIS Functions in hibernate spatial 6 migration

611 Views Asked by At

First Issue: I have an existing code written for hibernate spatial 5 to find records within given radius, which works fine. I am in the process of migrating the code hibernate-spatial 6.1.7.Final. But getting below error:

java.lang.IllegalArgumentException: Passed `invariantType` for function return cannot be null
    at org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.invariant(StandardFunctionReturnTypeResolvers.java:45) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final]
    at org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder.function(SqmCriteriaNodeBuilder.java:1495) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final]
    at org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder.function(SqmCriteriaNodeBuilder.java:153) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final]
    at com.adani.amm.specification.AssetLocationSpecification.toPredicate(AssetLocationSpecification.java:35) ~[classes/:na]

As per my debugging so far, it seems that POSTGIS geography function is not supported in the version. Below is a snippet of my code.

public class AssetLocationSpecification implements Specification<Location> {

    /**
     * 
     */
    private static final long serialVersionUID = 1L;
    private final Double radius;
    private final Double latitude;
    private final Double longitude;

    public AssetLocationSpecification(Double radius, Double latitude, Double longitude) {
        super();
        this.radius = radius;
        this.latitude = latitude;
        this.longitude = longitude;
    }

    @Override
    public Predicate toPredicate(Root<Location> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        Expression<Geometry> geography = criteriaBuilder.function("geography", Geometry.class, root.get("geometry"));
        Expression<Point> point = criteriaBuilder.function("ST_Point", Point.class, criteriaBuilder.literal(longitude),
                criteriaBuilder.literal(latitude));
        Expression<Point> centerPoint = criteriaBuilder.function("ST_SetSRID", Point.class, point,
                criteriaBuilder.literal(4326));
        Expression<Boolean> expression = criteriaBuilder.function(SpatialFunction.dwithin.toString(), boolean.class,
                geography, centerPoint, criteriaBuilder.literal(radius));
        return criteriaBuilder.equal(expression, true);
    }
}

Second Issue: org.hibernate.spatial.SpatialFunction.dwithin is deprecated and replacement enum (org.hibernate.spatial.CommonSpatialFunction) does not contain that function.

1

There are 1 best solutions below

1
Enrico On

I got the same error in a project of mine while performing exactly the same migration. I debugged the Hibernate Spatial code which produced the error, and as far as I understand the way the Hibernate types are registered changed in this version of Hibernate Spatial, and the Geometry type you use as return type in the first statement can't be found in the list of registered basic types. I can't be more specific because I also did not understand the mechanism completely.

After saying that, this error looked to me too weird and made me considering if the mistake was not on my side. I thought maybe I was using the Spatial APi in an "uncommon" way, and I tried to refactor my code trying to be more generic, without using PostGIS functions directly.

My original code was this:

public static Specification<Station> withinBBox(
        final double lon1,final double lat1,
        final double lon2, final double lat2) {
    return new Specification<Station>() {
        public Predicate toPredicate(Root<Station> root, CriteriaQuery<?> query,
                                     final CriteriaBuilder builder) {
            Expression<Geometry> bBoxExpression = builder.function(
                    "ST_MakeEnvelope",
                    Geometry.class,
                    builder.literal(lon1),
                    builder.literal(lat1),
                    builder.literal(lon2),
                    builder.literal(lat2),
                    builder.literal(4326));
            return builder.isTrue(
                    builder.function(
                            "ST_Within",
                            Boolean.class,
                            root.<Point>get("geoLocation"),
                            bBoxExpression)
            );
        }
    };
}

And I refactored to this:

public static Specification<Station> withinBBox(
        final double lon1,final double lat1,
        final double lon2, final double lat2) {
    return new Specification<Station>() {
        public Predicate toPredicate(Root<Station> root, CriteriaQuery<?> query,
                                     final CriteriaBuilder builder) {
            if(builder instanceof SqmCriteriaNodeBuilder) {
                JTSSpatialCriteriaBuilder jtsBuilder = ((SqmCriteriaNodeBuilder) builder).unwrap(JTSSpatialCriteriaBuilder.class);
                Expression<? extends Geometry> bBoxExpression = jtsBuilder.literal(GEOMETRY_FACTORY.createPolygon(new Coordinate[] {
                    new Coordinate(lon1, lat1),
                    new Coordinate(lon1, lat2),
                    new Coordinate(lon2, lat2),
                    new Coordinate(lon2, lat1),
                    new Coordinate(lon1, lat1)
                }));
                Expression<? extends Geometry> stationLocation = root.<Point>get("geoLocation");
                return jtsBuilder.isTrue(jtsBuilder.within(stationLocation, bBoxExpression));
            }
            return builder.isTrue(builder.literal(false));
        }
    };
}

You can see I do not use PostGIS anymore to create Geometry objects, but a GeometryFactory which I created this way: new GeometryFactory(new PrecisionModel(), 4326);

This approach is database agnostic.

Applying the same approach to your case, you could try to write something like this:

JTSSpatialCriteriaBuilder jtsBuilder = ((SqmCriteriaNodeBuilder) builder).unwrap(JTSSpatialCriteriaBuilder.class);
GeometryFactory gf = new GeometryFactory(new PrecisionModel(), 4326);
Expression<? extends Geometry> geography = jtsBuilder.literal(root.get("geometry"));
Expression<? extends Geometry> centerPoint = jtsBuilder.literal(gf.createPoint(new Coordinate(longitude, latitude)));
//This is not needed, the geometryfactory already works with SRID 4326
//Expression<Point> centerPoint = criteriaBuilder.function("ST_SetSRID", Point.class, point, criteriaBuilder.literal(4326)); 
Expression<Boolean> isWithin = jtsBuilder.distanceWithin(geography, centerPoint, radius);