MySQL的Geometry数据处理之WKB方案

2024-07-03 1535阅读

MySQL的Geometry数据处理之WKT方案:https://blog.csdn.net/qq_42402854/article/details/140134357

MySQL的Geometry数据处理之WKT方案中,介绍WTK方案的优点,也感受到它的繁琐和缺陷。比如:

  • 需要借助 ST_GeomFromText和 ST_AsText,让 SQL语句显得复杂。
    select id, ST_AsText(geometry) AS geometry, update_time, create_time from geometry_data
    
    • 没有一种GeomFromText方案可以覆盖所有的Geometry结构,使得类似的SQL要写多份。比如:ST_GeomFromText不是万能的。针对"几何信息集合"(GeometryCollection)则需要使用ST_GeomCollFromText来转换。
      insert into geometry_data(id, geometry, update_time, create_time) values
              (#{id}, ST_GeomFromText(#{geometry, jdbcType=BLOB, typeHandler=org.example.typehandlers.GeometryTypeWKTHandler}), now(), now())
      insert into geometry_data(id, geometry, update_time, create_time) values
              (#{id}, ST_GeomCollFromText(#{geometry, jdbcType=BLOB, typeHandler=org.example.typehandlers.GeometryTypeWKTHandler}), now(), now())
      
      • 没有针对LinearRing(一种特殊的LineString)的处理方法。

        MySQL的Geometry数据处理之WKB方案,则可以解决上述问题。

        WKB全程Well-Known Binary,它是一种二进制存储几何信息的方法。

        WKT方法,可以用字符串形式表达几何信息,如POINT (1 -1)。

        WKB方法则表达为:0101000000000000000000F03F000000000000F0BF

        这段二进制的拆解如下:

        MySQL的Geometry数据处理之WKB方案

        1. byte order:可以是0或者1,它表示是大顶堆(0)还是小顶堆(1)存储。
        2. WKB type:表示几何类型。值的对应关系如下:

          ○ 1 Point

          ○ 2 LineString

          ○ 3 Polygon

          ○ 4 MultiPoint

          ○ 5 MultiLineString

          ○ 6 MultiPolygon

          ○ 7 GeometryCollection

        3. 剩下的是坐标信息。

        虽然这个结构已经很基础,但是 MySQL的Geometry结构并不是WKB。准确的说,WKB只是 MySQL的Geometry结构中的一部分。它们的差异是,MySQL的Geometry结构是在WKB之前加了4个字节,用于存储SRID。

        MySQL的Geometry数据处理之WKB方案

        还有一点需要注意的是,MySQL存储Geometry数据使用的是小顶堆。所以WKB的Byte order字段值一定是1。 有了这些知识,我们就可以定义WKB类型的TypeHandler了。

        一般我们会使用 org.locationtech.jts的 Geometry类来表达几何信息。

        引入依赖:

                
                
                    org.locationtech.jts
                    jts-core
                    1.19.0
                
        

        一、自定义类型处理器

        项目中使用 MyBatis-Plus,自定义字段类型处理器来实现MySQL的Geometry数据处理之WKB方案。

        MyBatis-Plus字段类型处理器:https://baomidou.com/guides/type-handler/

        在 MyBatis 中,类型处理器(TypeHandler)扮演着 JavaType 与 JdbcType 之间转换的桥梁角色。它们用于在执行 SQL 语句时,将 Java 对象的值设置到 PreparedStatement 中,或者从 ResultSet 或 CallableStatement 中取出值。

        1、完整TypeHandler类

        @MappedTypes({Geometry.class})
        @MappedJdbcTypes(JdbcType.BLOB)
        public class GeometryTypeWKBHandler extends BaseTypeHandler {
            //private static final PrecisionModel PRECISION_MODEL = new PrecisionModel(PrecisionModel.FIXED); // 保留整数
            private static final PrecisionModel PRECISION_MODEL = new PrecisionModel(PrecisionModel.FLOATING); // 保留小数
            private static final Map GEOMETRY_FACTORIES = new ConcurrentHashMap();
            @Override
            public void setNonNullParameter(PreparedStatement ps, int i, Geometry parameter, JdbcType jdbcType) throws SQLException {
                byte[] bytes = serializeGeometry(parameter);
                ps.setBytes(i, bytes);
            }
            @Override
            public Geometry getNullableResult(ResultSet rs, String columnName) throws SQLException {
                byte[] bytes = rs.getBytes(columnName);
                try {
                    return deserializeGeometry(bytes);
                } catch (ParseException e) {
                    throw new SQLException(e);
                }
            }
            @Override
            public Geometry getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
                byte[] bytes = rs.getBytes(columnIndex);
                try {
                    return deserializeGeometry(bytes);
                } catch (ParseException e) {
                    throw new SQLException(e);
                }
            }
            @Override
            public Geometry getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
                byte[] bytes = cs.getBytes(columnIndex);
                try {
                    return deserializeGeometry(bytes);
                } catch (ParseException e) {
                    throw new SQLException(e);
                }
            }
            /**
             * 序列化
             *
             * @param geometry
             * @return
             */
            private byte[] serializeGeometry(Geometry geometry) {
                int srid = geometry.getSRID();
                byte[] bytes = new WKBWriter(2, ByteOrderValues.LITTLE_ENDIAN).write(geometry);
                return ByteBuffer.allocate(bytes.length + 4).order(ByteOrder.LITTLE_ENDIAN)
                        .putInt(srid)
                        .put(bytes)
                        .array();
            }
            /**
             * 反序列化
             *
             * @param bytes
             * @return
             * @throws ParseException
             */
            private static Geometry deserializeGeometry(byte[] bytes) throws ParseException {
                if (bytes == null) {
                    return null;
                }
                ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
                int srid = buffer.getInt();
                byte[] geometryBytes = new byte[buffer.remaining()];
                buffer.get(geometryBytes);
                GeometryFactory geometryFactory = GEOMETRY_FACTORIES.computeIfAbsent(srid, i -> new GeometryFactory(PRECISION_MODEL, i));
                WKBReader reader = new WKBReader(geometryFactory);
                return reader.read(geometryBytes);
            }
        }
        

        2、序列化方法

            private byte[] serializeGeometry(Geometry geometry) {
                int srid = geometry.getSRID();
                byte[] bytes = new WKBWriter(2, ByteOrderValues.LITTLE_ENDIAN).write(geometry);
                return ByteBuffer.allocate(bytes.length + 4).order(ByteOrder.LITTLE_ENDIAN)
                        .putInt(srid)
                        .put(bytes)
                        .array();
            }
        

        这段代码先从org.locationtech.jts.geom.Geometry中获取SRID码;

        然后以小顶堆模式,使用WKBWriter将几何信息保存为WKB的二进制码。

        然后申请比WKB大4个字节的空间,分别填入SRID和WKB。

        这样整个内存结构就匹配Mysql内部的Geometry内存结构了。

        3、反序列化方法

            private static Geometry deserializeGeometry(byte[] bytes) throws ParseException {
                if (bytes == null) {
                    return null;
                }
                ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
                int srid = buffer.getInt();
                byte[] geometryBytes = new byte[buffer.remaining()];
                buffer.get(geometryBytes);
                GeometryFactory geometryFactory = GEOMETRY_FACTORIES.computeIfAbsent(srid, i -> new GeometryFactory(PRECISION_MODEL, i));
                WKBReader reader = new WKBReader(geometryFactory);
                return reader.read(geometryBytes);
            }
        

        这段代码会将Mysql内部的Geometry内存结构读出来,转换成小顶堆模式。

        然后获取SRID,并以此创建GeometryFactory。

        剩下的内容就是WKB的内存了,最后使用WKBReader将这段内存转换成org.locationtech.jts.geom.Geometry。

        二、使用自定义类型处理器

        在实体类中,通过 @TableField注解指定自定义的类型处理器。

        确保 @TableField注解中的属性配置正确无误,特别是value属性是否匹配数据库的实际字段名,以及

        jdbcType是否正确设置为JdbcType.BLOB,因为地理空间数据通常以BLOB形式存储。

        创建表SQL语句:

        CREATE TABLE `t_geo_wkb` (
          `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
          `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
          `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
          `del_flag` char(1)  DEFAULT '0' COMMENT '删除标记,0未删除,1已删除',
          `name` varchar(255) DEFAULT NULL COMMENT '名称',
          `geo_type` varchar(255) DEFAULT NULL COMMENT 'geo_type',
          `geo` geometry NOT NULL COMMENT 'geo几何数据-GCJ02',
          PRIMARY KEY (`id`),
          SPATIAL KEY `idx_geo` (`geo`) COMMENT '空间数据索引'
        ) ENGINE=InnoDB  COMMENT='几何数据wkb表';
        

        1、DO类

        几何数据使用 org.locationtech.jts.geom.Geometry类型。

        @Getter
        @Setter
        @TableName("t_geo_wkb")
        public class GeoWkbDO implements Serializable {
            private static final long serialVersionUID = 1L;
            /**
             * ID
             */
            @TableId(value = "id", type = IdType.AUTO)
            private Long id;
            /**
             * 创建时间
             */
            @TableField("create_time")
            private LocalDateTime createTime;
            /**
             * 修改时间
             */
            @TableField("update_time")
            private LocalDateTime updateTime;
            /**
             * 删除标记,0未删除,1已删除
             */
            @TableField("del_flag")
            private String delFlag;
            /**
             * 名称
             */
            @TableField("name")
            private String name;
            /**
             * geo_type
             */
            @TableField("geo_type")
            private String geoType;
            /**
             * geo几何数据-GCJ02
             */
            @TableField(value = "geo", typeHandler = GeometryTypeWKBHandler.class, jdbcType = JdbcType.BLOB)
            private Geometry geo;
        }
        

        2、Mapper.xml

        在 Mapper文件中指定 typeHandler, jdbcType。

        
        
            
            
                
                
                
                
                
                
                
            
            
            
                id
                , create_time, update_time, del_flag, name, geo_type, geo
            
        
        

        使用了WKB模式,SQL就会写的很简洁,而不需要使用ST_GeomFromText和ST_AsText转来转去。可以见得WKB模式让 SQL XML变得简单。

        三、注册自定义类型处理器

        如果使用 @TableField注解指定自定义类型处理器没有被执行,我们就需要显式注册自定义TypeHandler。

        即在配置文件或启动类中 通过 TypeHandlerRegistry注册自定义的类型处理器。

        @Configuration
        @MapperScan("com.xxx.mapper")
        public class MyBatisPlusConfig {
            @Autowired
            private SqlSessionFactory sqlSessionFactory;
            @Bean
            public void registerCustomTypeHandlers() {
                sqlSessionFactory.getConfiguration().getTypeHandlerRegistry().register(
                        Geometry.class, // JavaType
                        JdbcType.BLOB, // JdbcType
                        GeometryTypeWKBHandler.class // 自定义TypeHandler
                );
            }
        }
        

        四、示例测试

        1、单元测试

            @Autowired
            private GeoWkbService geoWkbService;
            @Autowired
            private GeoWkbMapper geoWkbMapper;
            @Test
            public void testListAll() {
                List doList = geoWkbService.listAll();
                System.out.println(doList);
            }
            @Test
            public void testInsert1() {
                // 点
                GeometryFactory geometryFactory = new GeometryFactory();
                Geometry point = geometryFactory.createPoint(new Coordinate(108.939645, 34.343205));
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("点");
                saveDO.setGeoType("1");
                saveDO.setGeo(point);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testInsert2() {
                // 点集合
                GeometryFactory geometryFactory = new GeometryFactory();
                LineString lineString = geometryFactory
                        .createLineString(new Coordinate[]{new Coordinate(108.939645, 34.343205), new Coordinate(108.939647, 34.343207),
                                new Coordinate(1, 1)});
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("点集合");
                saveDO.setGeoType("2");
                saveDO.setGeo(lineString);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testInsert3() {
                // 线
                GeometryFactory geometryFactory = new GeometryFactory();
                LineString lineString = geometryFactory
                        .createLineString(new Coordinate[]{new Coordinate(108.939645, 34.343205), new Coordinate(108.939647, 34.343207),
                                new Coordinate(2, 2), new Coordinate(3, 3)});
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("线");
                saveDO.setGeoType("3");
                saveDO.setGeo(lineString);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testInsert4() {
                // 线集合
                GeometryFactory geometryFactory = new GeometryFactory();
                MultiLineString multiLineString = geometryFactory.createMultiLineString(new LineString[]{
                        geometryFactory
                                .createLineString(new Coordinate[]{new Coordinate(108.939645, 34.343205), new Coordinate(108.939647, 34.343207)}),
                        geometryFactory
                                .createLineString(new Coordinate[]{new Coordinate(108.939648, 34.343208), new Coordinate(108.939649, 34.343209)})
                });
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("线集合");
                saveDO.setGeoType("4");
                saveDO.setGeo(multiLineString);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testInsert5() {
                // 面
                GeometryFactory geometryFactory = new GeometryFactory();
                Polygon polygon = geometryFactory.createPolygon(new Coordinate[]{new Coordinate(1, 1),
                        new Coordinate(2, 2), new Coordinate(3, 3), new Coordinate(1, 1)});
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("面");
                saveDO.setGeoType("5");
                saveDO.setGeo(polygon);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testInsert6() {
                // 面集合
                GeometryFactory geometryFactory = new GeometryFactory();
                MultiPolygon multiPolygon = geometryFactory.createMultiPolygon(new Polygon[]{
                        geometryFactory.createPolygon(new Coordinate[]{new Coordinate(1, 1), new Coordinate(2, 2),
                                new Coordinate(3, 3), new Coordinate(1, 1)}),
                        geometryFactory.createPolygon(new Coordinate[]{new Coordinate(4, 4), new Coordinate(5, 5),
                                new Coordinate(6, 6), new Coordinate(4, 4)})
                });
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("面集合");
                saveDO.setGeoType("6");
                saveDO.setGeo(multiPolygon);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testInsert7() {
                // 几何信息集合
                GeometryFactory geometryFactory = new GeometryFactory();
                GeometryCollection geometryCollection = geometryFactory.createGeometryCollection(new Geometry[]{
                        geometryFactory.createPoint(new Coordinate(1, 1)),
                        geometryFactory.createLineString(new Coordinate[]{new Coordinate(1, 1), new Coordinate(2, 2)}),
                        geometryFactory.createPolygon(new Coordinate[]{new Coordinate(1, 1), new Coordinate(2, 2),
                                new Coordinate(3, 3), new Coordinate(1, 1)})
                });
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("几何信息集合");
                saveDO.setGeoType("7");
                saveDO.setGeo(geometryCollection);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testInsert8() {
                // 面-点集合
                GeometryFactory geometryFactory = new GeometryFactory();
                MultiPoint multiPoint = geometryFactory.createMultiPointFromCoords(
                        new Coordinate[]{new Coordinate(1, 1), new Coordinate(2, 2), new Coordinate(3, 3)});
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("面-点集合");
                saveDO.setGeoType("8");
                saveDO.setGeo(multiPoint);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testInsert9() {
                // linearRing
                GeometryFactory geometryFactory = new GeometryFactory();
                LinearRing linearRing = geometryFactory.createLinearRing(new Coordinate[] { new Coordinate(1, 1),
                        new Coordinate(2, 2), new Coordinate(3, 3), new Coordinate(1, 1) });
                GeoWkbDO saveDO = new GeoWkbDO();
                saveDO.setDelFlag(CommonConstants.DELETE_FLAG_NORMAL);
                saveDO.setName("linearRing");
                saveDO.setGeoType("9");
                saveDO.setGeo(linearRing);
                geoWkbMapper.insert(saveDO);
            }
            @Test
            public void testUpdateById() {
                GeometryFactory geometryFactory = new GeometryFactory();
                Coordinate coordinate = new Coordinate(2, 2);
                Geometry point = geometryFactory.createPoint(coordinate);
                GeoWkbDO updateDO = new GeoWkbDO();
                updateDO.setId(1L);
                updateDO.setGeo(point);
                geoWkbMapper.updateById(updateDO);
            }
        

        MySQL的Geometry数据处理之WKB方案

        2、返回VO序列化

        因为 DO定义的是 Geometry类型,业务中流转没问题,但是我们希望返回的 VO对象的这个字段为字符串格式的内容。所以,我们需要指定字段的序列化器,下面我们自定义序列化器。

        (1)自定义序列化器

        public class GeometrySerializer extends JsonSerializer {
            @Override
            public void serialize(Object obj, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                if (obj != null) {
                    jsonGenerator.writeString(obj.toString());
                }
            }
        }
        

        (2)VO对象上添加序列化器

            @ApiModelProperty("geo几何数据-GCJ02")
            @JsonSerialize(using = GeometrySerializer.class)
            private Geometry geo;
        

        MySQL的Geometry数据处理之WKB方案

        参考文章:

        • Mysql的Geometry数据处理之WKB方案:https://fangliang.blog.csdn.net/article/details/139097706

          — 求知若饥,虚心若愚。

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]