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类来表达几何信息。
引入依赖:
<!--Geometry工具库依赖-->
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
一、自定义类型处理器
项目中使用 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<Geometry> {
//private static final PrecisionModel PRECISION_MODEL = new PrecisionModel(PrecisionModel.FIXED); // 保留整数
private static final PrecisionModel PRECISION_MODEL = new PrecisionModel(PrecisionModel.FLOATING); // 保留小数
private static final Map<Integer, GeometryFactory> 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。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.charge.ws.core.mapper.GeoWkbMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.charge.ws.core.entity.GeoWkbDO">
<id column="id" property="id"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="del_flag" property="delFlag"/>
<result column="name" property="name"/>
<result column="geo_type" property="geoType"/>
<result column="geo" property="geo" typeHandler="com.charge.ws.handler.jts.GeometryTypeWKBHandler"
jdbcType="BLOB"/>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id
, create_time, update_time, del_flag, name, geo_type, geo
</sql>
</mapper>
使用了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<GeoWkbDO> 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<Object> {
@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
— 求知若饥,虚心若愚。