MySQL的Geometry数据处理之WKB方案
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
这段二进制的拆解如下:
- byte order:可以是0或者1,它表示是大顶堆(0)还是小顶堆(1)存储。
- WKB type:表示几何类型。值的对应关系如下:
○ 1 Point
○ 2 LineString
○ 3 Polygon
○ 4 MultiPoint
○ 5 MultiLineString
○ 6 MultiPolygon
○ 7 GeometryCollection
- 剩下的是坐标信息。
虽然这个结构已经很基础,但是 MySQL的Geometry结构并不是WKB。准确的说,WKB只是 MySQL的Geometry结构中的一部分。它们的差异是,MySQL的Geometry结构是在WKB之前加了4个字节,用于存储SRID。
还有一点需要注意的是,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); }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方案:https://fangliang.blog.csdn.net/article/details/139097706
— 求知若饥,虚心若愚。
- 没有针对LinearRing(一种特殊的LineString)的处理方法。
- 没有一种GeomFromText方案可以覆盖所有的Geometry结构,使得类似的SQL要写多份。比如:ST_GeomFromText不是万能的。针对"几何信息集合"(GeometryCollection)则需要使用ST_GeomCollFromText来转换。




