文章目录
前言
最近项目上有个需求,需要存储一些经纬度信息,用于实现类似返回5公里范围内的兴趣点(例如:周边5公里内的厕所),考虑到技术熟练度和运维成本等,选型如下:
- 持久层框架:mybatis-plus
- 数据库:Mysql5.7.x
一般实现
数据库
数据库创建2个字段,经度:lng,纬度:lat
- lng:字段类型decimal(9,6)
- lat:字段类型decimal(9,6)
代码
class toilet {
String name;
double lng;
double lat
}
这里不细讲了,很常见的实现方式
这里由于不是空间类型的数据结构,无法创建空间索引,查询性能容易碰到瓶颈。
自定义类型+空间数据类型
MySQL支持空间数据类型。
空间数据类型和函数可用于 MyISAM, InnoDB, NDB,和 ARCHIVE表。用于索引空间列,MyISAM并同时InnoDB 支持SPATIAL和非SPATIAL索引。其他存储引擎支持非SPATIAL索引
MySQL底层存储格式
查询了mysql官网,结果如下
Geometry 实际存储格式为:长度为25个字节
- 4个字节用于整数SRID(0)
- 1个字节(整数字节顺序)(1 =小字节序)
- 4个字节用于整数类型信息(MySQL使用从1个值至7,以表示
Point,LineString,Polygon,MultiPoint,MultiLineString,MultiPolygon,和GeometryCollection。) - 8字节的双精度X坐标
- 8字节的双精度Y坐标
例如,POINT(1 -1)由以下25个字节的序列组成,每个序列由两个十六进制数字表示:
mysql> SET @g = ST_GeomFromText('POINT(1 -1)');
mysql> SELECT LENGTH(@g);
+------------+
| LENGTH(@g) |
+------------+
| 25 |
+------------+
mysql> SELECT HEX(@g);
+----------------------------------------------------+
| HEX(@g) |
+----------------------------------------------------+
| 000000000101000000000000000000F03F000000000000F0BF |
+----------------------------------------------------+
| 组成 | 大小 | 值 |
|---|---|---|
| SRID | 4个字节 | 00000000 |
| 字节顺序 | 1个字节 | 01 |
| WKB类型 | 4字节 | 01000000 |
| X坐标 | 8字节 | 000000000000F03F |
| Y坐标 | 8字节 | 000000000000F0BF |
数据库
数据库创建一个字段coordinate
- coordinate:字段类型point
代码
实体类
class toilet {
String name;
geopoint location;
}
// 自定义数据类型
class geopoint {
double lng;
double lat
}
GeoPointTypeHandler
@Slf4j
@MappedTypes({GeoPoint.class})
public class GeoPointTypeHandler extends BaseTypeHandler<GeoPoint> {
/**
* 空间参照标识系 MySQL数据库默认为0
*/
private static int SRID = 0;
/**
* 字节顺序指示符为1或0表示小端或大端存储。小字节序和大字节序分别也称为网络数据表示(NDR)和外部数据表示(XDR)
*/
private static byte ENDIAN = (byte) 1;
@Override
public void setNonNullParameter(PreparedStatement ps, int i, GeoPoint parameter, JdbcType jdbcType) throws SQLException {
ps.setBytes(i, to(parameter));
}
@Override
public GeoPoint getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parse(rs.getBytes(columnName));
}
@Override
public GeoPoint getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parse(rs.getBytes(columnIndex));
}
@Override
public GeoPoint getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parse(cs.getBytes(columnIndex));
}
/**
* bytes转GeoPoint对象
*
* @param bytes
*/
private GeoPoint parse(byte[] bytes) {
ByteBuffer wrap = ByteBuffer.wrap(bytes)
// 小端点排序(Java默认是大端点排序,这里要改下)
.order(ByteOrder.LITTLE_ENDIAN);
int SRID = wrap.getInt();
byte endian = wrap.get();
int wkbType = wrap.getInt();
double x = wrap.getDouble();
double y = wrap.getDouble();
GeoPoint geoPoint = new GeoPoint(x, y);
log.info("geo-point:{}", JSONUtil.toJsonStr(geoPoint));
return geoPoint;
}
/**
* GeoPoint转bytes对象
*
* @param geoPoint
*/
private byte[] to(GeoPoint geoPoint) {
ByteBuffer wrap = ByteBuffer.allocate(25)
// 小端点排序(Java默认是大端点排序,这里要改下)
.order(ByteOrder.LITTLE_ENDIAN);
// SRID: 0
wrap.putInt(SRID);
// 字节顺序指示符为1或0表示小端或大端存储。小字节序和大字节序分别也称为网络数据表示(NDR)和外部数据表示(XDR)
wrap.put(ENDIAN);
// WKB类型是指示几何类型的代码 wkbType: 1 MySQL使用从1个值至7,以表示 Point,LineString, Polygon,MultiPoint, MultiLineString, MultiPolygon,和 GeometryCollection。
wrap.putInt(1);
// X坐标
wrap.putDouble(geoPoint.getLon());
// Y坐标
wrap.putDouble(geoPoint.getLat());
return wrap.array();
}
}
实体类加上相关注解
@TableName(autoResultMap = true) // 注意!! 必须开启映射注解
class toilet {
String name;
@TableField(typeHandler = GeoPointTypeHandler.class)
geopoint location;
}
测试
Toilet toilet = new Toilet();
toilet.setName("laker");
toilet.setLocation(new GeoPoint(123.23, 1.2)); // 直接塞实体
toiletService.save(toilet);
// 查询
List<Toilet> toilets = toiletService.list();
...
[{"name":"laker",location:{"lng":123.23,"lat":1.2}}]
计算距离
已于高德提供的计算距离api对比其计算结果是一致的。
高德计算距离webapi
SELECT ( st_distance_sphere ( point ( 116.481028,39.989643 ), point ( 114.465302,40.004717 ), 6378137.0 ) ) AS distance
这里注意,数据不能是随便乱造的,否则回报st_distance_sphere 参数错误。
参考: