大纲
- 几何结构构建
- 点
- 点集合
- 线
- 线集合
- 面
- 面集合
- 几何信息集合
- TypeHandler
- SQL操作
- 写入操作
- 读取操作
- 完整XML
- Mapper
- 测试代码
- 建表SQL
- 总结
- 代码
- 参考资料
WKT全称是Well-Known Text。它是一种表达几何信息的字符串内容。比如点可以用WKT表示为POINT (3 3);线可以用WKT表示为LINESTRING (1 1, 2 2)。
Mysql数据库可以存储一些几何类型数据,比如点、线、多边形等。这在一些基于地理信息的服务上比较有用,比如在地图上的商店地理坐标(点),或者路径规划中的行进路线(线)等。
目前我使用的Mysql是8.4.0版本,它支持如下几何类型数据结构。
类型 | 说明 | 样例 | 图例 |
---|---|---|---|
点 | POINT (3 3) | ||
点集合 | MULTIPOINT ((1 1), (2 2), (3 3)) | ||
线 | LINESTRING (1 1, 2 2) | ||
线集合 | MULTILINESTRING ((1 1, 2 2), (3 3, 4 4)) | ||
多边形 | POLYGON ((1 1, 2 2, 3 3, 1 1)) | ||
多边形集合 | MULTIPOLYGON (((1 1, 2 2, 3 3, 1 1)), ((4 4, 5 5, 6 6, 4 4))) | ||
多种几何类型集合 | GEOMETRYCOLLECTION (POINT (1 1), LINESTRING (1 1, 2 2), POLYGON ((1 1, 2 2, 3 3, 1 1))) |
一般我们会使用org.locationtech.jts的Geometry类来表达几何信息。
<dependency><groupId>org.locationtech.jts</groupId><artifactId>jts-core</artifactId><version>1.19.0</version>
</dependency>
然后使用下面的方法构建各种结构
几何结构构建
点
GeometryFactory geometryFactory = new GeometryFactory();
Geometry geometry = geometryFactory.createPoint(new Coordinate(3, 3));
点集合
GeometryFactory geometryFactory = new GeometryFactory();
LineString lineString = geometryFactory.createLineString(new Coordinate[] { new Coordinate(1, 1), new Coordinate(2, 2) });
线
GeometryFactory geometryFactory = new GeometryFactory();
LineString lineString = geometryFactory.createLineString(new Coordinate[] { new Coordinate(1, 1), new Coordinate(2, 2) });
线集合
GeometryFactory geometryFactory = new GeometryFactory();
MultiLineString multiLineString = geometryFactory.createMultiLineString(new LineString[] {geometryFactory.createLineString(new Coordinate[] { new Coordinate(1, 1), new Coordinate(2, 2) }),geometryFactory.createLineString(new Coordinate[] { new Coordinate(3, 3), new Coordinate(4, 4) })
});
面
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) });
面集合
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) })
});
几何信息集合
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) })
});
下面我们需要将这些结构保存到Mysql数据库中。
由于org.locationtech.jts.geom.Geometry和Mysql内部存储的Geometry不配,所以需要转换操作,于是就要引入typehandler。
TypeHandler
package org.example.typehandlers;import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class GeometryTypeWKTHandler extends BaseTypeHandler<Geometry> {private static GeometryFactory factory = new GeometryFactory(new PrecisionModel(PrecisionModel.FLOATING), 4326);@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, Geometry parameter, JdbcType jdbcType) throws SQLException {String str = serializeGeometry(parameter);ps.setString(i, str);;}@Overridepublic Geometry getNullableResult(ResultSet rs, String columnName) throws SQLException {String str = rs.getString(columnName);try {return deserializeGeometry(str);} catch (ParseException e) {throw new SQLException(e);}}@Overridepublic Geometry getNullableResult(ResultSet rs, int columnIndex) throws SQLException {String str = rs.getString(columnIndex);try {return deserializeGeometry(str);} catch (ParseException e) {throw new SQLException(e);}}@Overridepublic Geometry getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {String str = cs.getString(columnIndex);try {return deserializeGeometry(str);} catch (ParseException e) {throw new SQLException(e);}}private static String serializeGeometry(Geometry geometry) {WKTWriter writer = new WKTWriter(2);return writer.write(geometry);}private static Geometry deserializeGeometry(String wkt) throws ParseException {return new WKTReader(factory).read(wkt);}}
WKTWriter会将org.locationtech.jts.geom.Geometry转换为String,然后交由SQL语句处理;
WKTReader会将SQL语句读取出来的String转换为org.locationtech.jts.geom.Geometry对象。
SQL操作
在上图我们看到,TypeHandler主要使用String类型作为媒介来和SQL语句联系。那么SQL语句是如何把String转成Mysql的Geometry内部结构的呢?
这就需要引入ST_GeomFromText和ST_AsText。
ST_GeomFromText可以将WKT格式的几何信息转换为Mysql内部的Geometry结构。比如
ST_GeomFromText('MULTIPOINT (1 1, 2 2, 3 3)')
ST_AsText则可以将Mysql内部的Geometry结构转换为WKT格式的几何信息。
在Mybatis的SQL XML中
写入操作
对org.locationtech.jts.geom.Geometry结构(即geometry字段)使用org.example.typehandlers.GeometryTypeWKTHandler处理成WTK(字符串)几何信息格式后,用ST_GeomFromText转换成Mysql内部的Geometry结构,然后存储。
<insert id="insertOne" parameterType="org.example.model.GeometryData" useGeneratedKeys="true" keyProperty="id">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>
这儿需要注意的是ST_GeomFromText不是万能的。比如针对“几何信息集合”(GeometryCollection)则需要使用ST_GeomCollFromText来转换
<insert id="insertGeometryCollection" parameterType="org.example.model.GeometryData" useGeneratedKeys="true" keyProperty="id">insert into geometry_data(id, geometry, update_time, create_time) values(#{id}, ST_GeomCollFromText(#{geometry, jdbcType=BLOB, typeHandler=org.example.typehandlers.GeometryTypeWKTHandler}), now(), now())</insert>
读取操作
由于需要对geometry字段特殊处理,所以不能使用Select * From geometry_data,而需要把每个参数都写好。
ST_AsText会将Mysql的内部的Geometry结构转换成WKT格式(字符串)的几何信息,然后交由org.example.typehandlers.GeometryTypeWKTHandler转换成org.locationtech.jts.geom.Geometry结构。
<resultMap id="GeometryDataResultMap" type="org.example.model.GeometryData"><result property="id" column="id"/><result property="geometry" column="geometry" typeHandler="org.example.typehandlers.GeometryTypeWKTHandler" jdbcType="BLOB"/><result property="updateTime" column="update_time"/><result property="createTime" column="create_time"/></resultMap><select id="findAll" resultMap="GeometryDataResultMap">select id, ST_AsText(geometry) AS geometry, update_time, create_time from geometry_data</select>
完整XML
<?xml version="1.0" encoding="UTF-8"?>
<!-- AllTypeMapper-1.xml -->
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mapper.GeometryDataWKTMapper"><resultMap id="GeometryDataResultMap" type="org.example.model.GeometryData"><result property="id" column="id"/><result property="geometry" column="geometry" typeHandler="org.example.typehandlers.GeometryTypeWKTHandler" jdbcType="BLOB"/><result property="updateTime" column="update_time"/><result property="createTime" column="create_time"/></resultMap><select id="findAll" resultMap="GeometryDataResultMap">select id, ST_AsText(geometry) AS geometry, update_time, create_time from geometry_data</select><select id="find" resultMap="GeometryDataResultMap">select id, ST_AsText(geometry) AS geometry, update_time, create_time from geometry_data where id = #{id}</select><insert id="insertOne" parameterType="org.example.model.GeometryData" useGeneratedKeys="true" keyProperty="id">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><insert id="insertList" parameterType="list">insert into geometry_data(id, geometry, update_time, create_time) values<foreach item="item" collection="list" separator=",">(#{item.id}, ST_GeomFromText(#{item.geometry, jdbcType=BLOB, typeHandler=org.example.typehandlers.GeometryTypeWKTHandler}), now(), now())</foreach></insert><update id="updateOne" parameterType="org.example.model.GeometryData">update geometry_data set geometry = ST_GeomFromText(#{geometry, jdbcType=BLOB, typeHandler=org.example.typehandlers.GeometryTypeWKTHandler}), update_time = now() where id = #{id} </update><insert id="insertGeometryCollection" parameterType="org.example.model.GeometryData" useGeneratedKeys="true" keyProperty="id">insert into geometry_data(id, geometry, update_time, create_time) values(#{id}, ST_GeomCollFromText(#{geometry, jdbcType=BLOB, typeHandler=org.example.typehandlers.GeometryTypeWKTHandler}), now(), now())</insert></mapper>
Mapper
package org.example.mapper;import java.util.List;import org.example.model.GeometryData;public interface GeometryDataWKTMapper {public List<GeometryData> findAll();public GeometryData find(Long id);public Long insertOne(GeometryData geometryData);public Long insertList(List<GeometryData> geometryDataList);public Long updateOne(GeometryData geometryData);public Long insertGeometryCollection(GeometryData geometryData);
}
测试代码
package org.example;import static org.junit.jupiter.api.Assertions.fail;import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.example.mapper.GeometryDataWKTMapper;
import org.example.model.GeometryData;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;public class GeometryDataWKTTest {private static SqlSessionFactory sqlSF;@BeforeAllstatic void CreateSessionFactory() throws IOException {InputStream in = Resources.getResourceAsStream("mybatis/config/mybatis-config-geometry-wkt.xml");sqlSF = new SqlSessionFactoryBuilder().build(in);}@Testpublic void testFindAll() {List<GeometryData> all = null;try (SqlSession session = sqlSF.openSession()) {all = session.getMapper(GeometryDataWKTMapper.class).findAll();} catch (Exception e) {System.out.println(e.getMessage());}for (GeometryData a : Objects.requireNonNull(all)) {System.out.println(a.getGeometry());}}@Testpublic void testFind() {try (SqlSession session = sqlSF.openSession()) {GeometryDataWKTMapper GeometryDataWKTMapper = session.getMapper(GeometryDataWKTMapper.class);GeometryData one = GeometryDataWKTMapper.find(1L);System.out.println(one.getGeometry());} catch (Exception e) {System.out.println(e.getMessage());}}@Testpublic void testInsert() {try (SqlSession session = sqlSF.openSession()) {GeometryDataWKTMapper GeometryDataWKTMapper = session.getMapper(GeometryDataWKTMapper.class);GeometryData geometryData = new GeometryData();GeometryFactory geometryFactory = new GeometryFactory();Coordinate coordinate = new Coordinate(1, 1);Geometry geometry = geometryFactory.createPoint(coordinate);geometryData.setGeometry(geometry);long count = GeometryDataWKTMapper.insertOne(geometryData);System.out.println(count);session.commit();} catch (Exception e) {System.out.println(e.getMessage());fail();}}@Testpublic void testUpdate() {try (SqlSession session = sqlSF.openSession()) {GeometryDataWKTMapper GeometryDataWKTMapper = session.getMapper(GeometryDataWKTMapper.class);GeometryData geometryData = new GeometryData();GeometryFactory geometryFactory = new GeometryFactory();Coordinate coordinate = new Coordinate(2, 2);Geometry geometry = geometryFactory.createPoint(coordinate);geometryData.setId(1L);geometryData.setGeometry(geometry);long count = GeometryDataWKTMapper.updateOne(geometryData);System.out.println(count);session.commit();} catch (Exception e) {System.out.println(e.getMessage());fail();}}@Testpublic void testInsertList() {try (SqlSession session = sqlSF.openSession()) {GeometryDataWKTMapper GeometryDataWKTMapper = session.getMapper(GeometryDataWKTMapper.class);List<GeometryData> geometryDataList = new ArrayList<>();{GeometryData geometryData = new GeometryData();GeometryFactory geometryFactory = new GeometryFactory();Coordinate coordinate = new Coordinate(3, 3);Geometry geometry = geometryFactory.createPoint(coordinate);geometryData.setGeometry(geometry);geometryDataList.add(geometryData);}{GeometryData geometryData = new GeometryData();GeometryFactory geometryFactory = new GeometryFactory();LineString lineString = geometryFactory.createLineString(new Coordinate[] { new Coordinate(1, 1), new Coordinate(2, 2) });geometryData.setGeometry(lineString);geometryDataList.add(geometryData);}{GeometryData geometryData = new GeometryData();GeometryFactory geometryFactory = new GeometryFactory();MultiLineString multiLineString = geometryFactory.createMultiLineString(new LineString[] {geometryFactory.createLineString(new Coordinate[] { new Coordinate(1, 1), new Coordinate(2, 2) }),geometryFactory.createLineString(new Coordinate[] { new Coordinate(3, 3), new Coordinate(4, 4) })});geometryData.setGeometry(multiLineString);geometryDataList.add(geometryData);}{GeometryData geometryData = new GeometryData();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) })});geometryData.setGeometry(multiPolygon);geometryDataList.add(geometryData);}// {// GeometryData geometryData = new GeometryData();// 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) });// geometryData.setGeometry(linearRing);// geometryDataList.add(geometryData);// }{GeometryData geometryData = new GeometryData();GeometryFactory geometryFactory = new GeometryFactory();MultiPoint multiPoint = geometryFactory.createMultiPointFromCoords(new Coordinate[] { new Coordinate(1, 1), new Coordinate(2, 2), new Coordinate(3, 3) });geometryData.setGeometry(multiPoint);geometryDataList.add(geometryData);}{GeometryData geometryData = new GeometryData();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) });geometryData.setGeometry(polygon);geometryDataList.add(geometryData);}// {// GeometryData geometryData = new GeometryData();// 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) })// });// geometryData.setGeometry(geometryCollection);// geometryDataList.add(geometryData);// }long count = GeometryDataWKTMapper.insertList(geometryDataList);System.out.println(count);session.commit();} catch (Exception e) {System.out.println(e.getMessage());fail();}}@Testpublic void testInsertGeometryCollection() {try (SqlSession session = sqlSF.openSession()) {GeometryDataWKTMapper GeometryDataWKTMapper = session.getMapper(GeometryDataWKTMapper.class);GeometryData geometryData = new GeometryData();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) })});geometryData.setGeometry(geometryCollection);Long index = GeometryDataWKTMapper.insertGeometryCollection(geometryData);System.out.println(index);session.commit();} catch(Exception e) {System.out.println(e.getMessage());fail();}}
}
建表SQL
CREATE TABLE `geometry_data` (`id` BIGINT(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',`geometry` GEOMETRY NOT NULL COMMENT '几何信息',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',`deleted` boolean DEFAULT false COMMENT '是否已被删除',PRIMARY KEY (`id`),SPATIAL INDEX `spatial_geometry` (`geometry`)
) COMMENT='几何数据表';
总结
很多数据库为了兼容Mysql,针对Geometry类型,在WKT模式下是兼容的。因为如何将WKT转换成自己数据库内部的结构,即对ST_GeomFromText等方法的实现是可以自己内部处理,让用户不会感知。这让WKT方案在跨数据库时有比较好的兼容性。
但是如果只是针对Mysql数据库,或者像OceanBase这类对Mysql底层也兼容很好的数据库,则可以考虑WKB方案。这块的内容我们会在《0基础学习Mybatis系列数据库操作框架——Mysql的Geometry数据处理之WKB方案》中介绍。
代码
https://github.com/f304646673/mybatis_demo
参考资料
- https://dev.mysql.com/doc/refman/8.0/en/gis-data-formats.html#gis-wkb-format
- https://dev.mysql.com/doc/refman/8.0/en/gis-wkt-functions.html#function_st-geomfromtext
- https://www.keene.edu/campus/maps/tool/