从零搭建开发脚手架 mybatis自定义字段类型 以Mysql空间数据存储为例

525 阅读4分钟

文章目录

前言

最近项目上有个需求,需要存储一些经纬度信息,用于实现类似返回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支持空间数据类型。

空间数据类型和函数可用于 MyISAMInnoDBNDB,和 ARCHIVE表。用于索引空间列,MyISAM并同时InnoDB 支持SPATIAL和非SPATIAL索引。其他存储引擎支持非SPATIAL索引

MySQL底层存储格式

查询了mysql官网,结果如下

Geometry 实际存储格式为:长度为25个字节

  • 4个字节用于整数SRID(0)
  • 1个字节(整数字节顺序)(1 =小字节序)
  • 4个字节用于整数类型信息(MySQL使用从1个值至7,以表示 PointLineStringPolygonMultiPointMultiLineStringMultiPolygon,和 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 |
+----------------------------------------------------+
组成大小
SRID4个字节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 参数错误。

参考: