如何使用 JdbcTemplate 从 MySQL 转换类型 Point?

How convert type Point from MySQL with JdbcTemplate?

需要帮助!我无法使用 JdbcTemplate 从 MySQL 转换点。但是,当使用 JPA 时,它成功了!

不要建议 SELECT ST_AsWKT() + WKTReader() 的答案,我已经尝试过但效果不是很好...

也许您需要为 JDBC 指定 SpatialDialect 或编写自定义转换器???

错误:

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

来源

输入Mysql:POINT NULL SRID 4326

输入Java:org.locationtech.jts.geom.Point

在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;  
}

使用 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()
}

假设您只想“提取”点几何体,
我认为您应该查看 Hibernate Spatial 如何从 JDBC ResultSet 中提取 JTS Geometry - 您的起点应该是 org.hibernate.spatial.dialect.oracle.SDOGeometryValueExtractor

(如果您还想“存储”几何图形,我现在没有任何提示给您)

代码(改编自上述 class)可能如下所示:

private Geometry loadGeometry(ResultSet rs) throws SQLException {
    final Object geomObj = rs.getObject( "coordinates" );
    if (rs.wasNull()) {
        return null;
    } else {
        return convertToGeometry(geomObj);
    }
}

private Geometry convertToGeometry(Object struct) {
    final SDOGeometry sdogeom = SDOGeometry.load( (Struct) struct );
    final var geolateGeom = toGeomerty( sdogeom );
    return JTS.to(geolateGeom);
}

private org.geolatte.geom.Geometry toGeomerty(SDOGeometry sdoGeom) {
    return Decoders.decode( sdoGeom );
}

我已经解决这个问题很久了,我会公布我是如何解决的。也许有更好的方法。

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);
    }
}