javaspringjdbcjdbctemplatejts

How convert type Point from MySQL with JdbcTemplate?


Need Help! I am unable to convert a Point from MySQL using JdbcTemplate. However, when using JPA it worked out!

Don't suggest an answer with SELECT ST_AsWKT() + WKTReader(), I have already tried and it did not work out very well...

Maybe you need to specify the SpatialDialect for JDBC or write a custom converter???

Error:

java.sql.SQLException: Conversion not supported for type org.locationtech.jts.geom.Point

Source

Type in Mysql: POINT NULL SRID 4326

Type in Java: org.locationtech.jts.geom.Point

in Gradle: implementation 'org.locationtech.jts:jts-core:1.18.0'

NtpJdbcRepository.java:

import org.locationtech.jts.geom.Point;

@Repository
@AllArgsConstructor
public class NtpJdbcRepository implements NtpRepository {

private final JdbcTemplate jdbcTemplate;

@Override
public List<NtpSource> getNtpSourceWithNtpIdandDate(List<Integer> ntp_id, Date startDate, Date endDate) {

    String selectQuery = "SELECT "
            + "u.idTable,"
            + "u.ntp_id,"
            + "u.dataDT,"
            + "u.ip,"
            + "u.country,"
            + "u.iso_code,"
            + "u.city,"
            + "u.coordinates,"
            + "SUM(u.counts_requests) as counts_requests "
            + "FROM ntp_source AS u "
            + "WHERE u.ntp_id IN (?) and u.dataDT BETWEEN ? AND ? "
            + "GROUP BY u.ip";
          
    String inSql = ntp_id.stream().map(String::valueOf)
            .collect(Collectors.joining(","));

    return jdbcTemplate.query(selectQuery,
            new Object[]{inSql, startDate, endDate},
            (rs, rowNum) -> {
                NtpSource ntpSource = new NtpSource();
                ntpSource.setId(rs.getInt("idTable"));
                ntpSource.setNtpId(rs.getInt("ntp_id"));
                ntpSource.setDataDT(rs.getDate("dataDT"));
                ntpSource.setIp(rs.getString("ip"));
                ntpSource.setCountry(rs.getString("country"));
                ntpSource.setIsoCode(rs.getString("iso_code"));
                ntpSource.setCity(rs.getString("city"));
                ntpSource.setCountsRequests(rs.getLong("counts_requests"));
                ntpSource.setCoordinates(rs.getObject("coordinates", Point.class)); // <-- There error: SQLException: Conversion not supported for type org.locationtech.jts.geom.Point

                return ntpSource;
            });
      }
}

NtpSource.java:

import org.locationtech.jts.geom.Point;

@Entity
@Table(name = "ntp_source")
@ToString
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NtpSource implements Serializable {

@Id
@Column(name = "idTable")
private int id;

@Column(name = "ntp_id")
private int ntpId;

@Column(name = "dataDT", columnDefinition="DATETIME")
@Temporal(TemporalType.TIMESTAMP)
private Date dataDT;

private String ip;

private String country;

@Column(name = "iso_code")
private String isoCode;

private String city;

@Column(name = "coordinates", nullable = true, columnDefinition = "POINT SRID 4326")
public Point coordinates;

@Column(name = "counts_requests")
private long countsRequests;  
}

Analogical request with JPA (NtpJpaRepository.java):

@Repository
public interface NtpJpaRepository extends JpaRepository<NtpSource, Long> {

@Query(nativeQuery = true, value = "SELECT "
        +"u.idTable,"
        +"u.ntp_id, "
        +"u.dataDT, "
        +"u.ip,"
        +"u.country,"
        +"u.iso_code,"
        +"u.city,"
        +"u.coordinates,"
        +"SUM(u.counts_requests) as counts_requests"
        +"FROM ntp_source AS u "
        +"WHERE u.ntp_id IN (:ntp_id) and u.dataDT BETWEEN :startDate AND :endDate "
        +"GROUP BY u.ip")
List<NtpSource> getNtpSourceWithNtpIdandDate(
        @Param("ntp_id") List<Integer> ntp_id,
        @Param("startDate") Date startDate,
        @Param("endDate") Date endDate);

   ...
}

application.properties:

    spring.datasource.url=jdbc:mysql://localhost/ntp
    spring.datasource.username=*****
    spring.datasource.password=*****
    spring.sql.init.encoding=UTF-8
    spring.datasource.connectionProperties=useUnicode=true;
 org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=org.hibernate.spatial.dialect.mysql.MySQL8SpatialDialect #testing
    
    server.servlet.context-path=/ntp_visits
    server.port = 8090
    
    ## Hibernate & JPA Properties
    # The SQL dialect makes Hibernate generate better SQL for the chosen database
    spring.jpa.properties.hibernate.dialect = org.hibernate.spatial.dialect.mysql.MySQL8SpatialDialect
    spring.jpa.hibernate.ddl-auto = update
    spring.jpa.show-sql=true
    #spring.jpa.properties.hibernate.jdbc.batch_size=3500
    spring.jpa.open-in-view=false
    spring.jpa.properties.hibernate.format_sql=true
    logging.level.org.springframework.jdbc.core = TRACE
    
    #logging.level.org.hibernate.SQL=DEBUG
    #logging.level.org.hibernate.type=trace
    spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
    spring.jpa.properties.hibernate.use_sql_comments=false
    
    # THYMELEAF (ThymeleafAutoConfiguration)
    #spring.thymeleaf.prefix=classpath:/templates/
    spring.thymeleaf.suffix=.html
    spring.thymeleaf.mode=HTML5
    spring.thymeleaf.encoding=UTF-8
    spring.web.resources.add-mappings=true
    spring.thymeleaf.cache=true      

build.gradle:

plugins {
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id 'war'
}

group = 'org.vniiftri'
version = '1.0'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
configurations.all {
    exclude module: 'slf4j-log4j12'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-rest'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'com.google.code.gson:gson:2.8.7'
    implementation group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final'
    //implementation 'org.locationtech.jts:jts:1.18.0'
    implementation 'org.locationtech.jts:jts-core:1.18.0'
    //implementation 'org.n52.jackson:jackson-datatype-jts:1.2.4'
    //implementation group: 'com.bedatadriven', name: 'jackson-datatype-jts', version: '2.4'
    implementation 'org.hibernate:hibernate-core:5.5.5.Final'
    implementation group: 'org.hibernate', name: 'hibernate-spatial', version: '5.5.5.Final'
    //implementation group: 'com.graphhopper.external', name: 'jackson-datatype-jts', version: '0.10-2.5-1'

    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.projectlombok:lombok'
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

Solution

  • I have already solved this issue for a long time, I will publish how I did it. Maybe there is a better way.

    NtpJdbcRepository.java:

    @Repository
    @AllArgsConstructor
    public class NtpJdbcRepository implements NtpRepository {
    
    private final NamedParameterJdbcTemplate namedJdbcTemplate;
    private final PointReader pointReader;
    
    @Override
    public List<NtpSource> getNtpSource(List<Integer> ntp_id, Date startDate, Date endDate) {
    
        String selectQuery = "SELECT "
                + "u.ntp_id, "
                + "u.ip,"
                + "u.country,"
                + "u.iso_code,"
                + "u.city,"
                + "ST_AsWKB(u.coordinates) as coordinates," // <- returns a binary result.
                + "SUM(u.counts_requests) as counts_requests "
                + "FROM ntp_source AS u "
                + "WHERE u.ntp_id IN (:ntp_ids) and u.dataDT BETWEEN :startDate AND :endDate "
                + "GROUP BY u.ip, u.ntp_id";
    
        MapSqlParameterSource parameters = new MapSqlParameterSource()
                .addValue("ntp_ids", ntp_id)
                .addValue("startDate", startDate)
                .addValue("endDate", endDate);
    
    
        return namedJdbcTemplate.query(selectQuery,
                parameters, (rs, rowNum) -> {
                    NtpSource ntpSource = new NtpSource();
                    ntpSource.setNtpId(rs.getInt("ntp_id"));
                    ntpSource.setIp(rs.getString("ip"));
                    ntpSource.setCountry(rs.getString("country"));
                    ntpSource.setIsoCode(rs.getString("iso_code"));
                    ntpSource.setCity(rs.getString("city"));
                    ntpSource.setCountsRequests(rs.getLong("counts_requests"));
                    ntpSource.setCoordinates(pointReader.read(rs.getBytes("coordinates"))); // <- get byte[]
                    return ntpSource;
                });
        }
    }
    

    PointReader.java:

    @Component
    @Slf4j
    public class PointReader {
        private static final int SRID = 4326;
        private final WKTReader reader;
        private final WKBReader wkbReader;
        private final GeometryFactory gf;
    
        public PointReader() {
            PrecisionModel pm = new PrecisionModel(PrecisionModel.FLOATING);
            gf = new GeometryFactory(pm, SRID);
            wkbReader = new WKBReader(gf);
            reader = new WKTReader(gf);
        }      
    
        public Point read(@NotNull byte[] bytes) {
            if (bytes == null) {
                return null;
            } else {
                Geometry geometry;
                try {
                    geometry = wkbReader.read(bytes);
                    return convert3Dto2D(geometry);
                } catch (ParseException e) {
                    log.error("Ошибка при чтении Point в виде байтов.", e);
                    return null;
                }
            }
        }
    
        // need the point to have only 2 measurements, but not three
        private Point convert3Dto2D(Geometry g3D) {
            Coordinate geomCoord = g3D.getCoordinate().copy();
            CoordinateSequence seq = new PackedCoordinateSequenceFactory().create(1, 2);
            seq.setOrdinate(0, CoordinateSequence.X, geomCoord.x);
            seq.setOrdinate(0, CoordinateSequence.Y, geomCoord.y);
            return gf.createPoint(seq);
        }
    }