SpringBoot+MyBatis+MySQL的Point实现范围查找
前言
最近做了一个功能,需要通过用户当前位置点获取指定范围内的数据。由于后端存储用的是 MySQL,故选择使用 MySQL 中的 Point 实现范围查找功能。ORM 框架用的是 MyBatis,MyBatis 原生并不支持 Point 字段与 POJO 的映射,需要自定义 MyBatis 的 TypeHandler 实现该功能。
当然,你可以通过定义两个 MySQL 字段(经度和维度)来代替 Point 也可以实现范围查找,但是既然是使用的 MyBatis,那么还是希望能在 MyBatis 中直接操作 Point,提高代码通用性。
关于 MySQL 的 POINT
在MySQL中,POINT 是一种用于存储地理空间数据的数据类型,它表示二维空间中的一个点。MySQL 从 5.7
版本开始,提供了对地理空间数据类型的原生支持,包括 POINT、LINESTRING、POLYGON 等。
POINT 数据类型用于存储一个二维坐标点,其格式为 (X, Y),其中 X 和 Y 分别表示该点在二维平面上的横坐标和纵坐标。
注意,在用 POINT 存储经纬度时,X 为经度,Y 为纬度,不要弄反了。因为将经纬度存储到 POINT 时并没有循序限制,但是使用 POINT 相关函数时就有限制了。比如ST_Distance_Sphere
。
组件版本
- SpringBoot 2.4.3
- MyBatis-Plus 3.4.2
- MySQL 8.0.26
建表(含 POINT 字段)
create table group_ride_info
(
id bigint unsigned not null comment '主键id'
primary key,
create_time datetime not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间',
create_by int unsigned default '0' not null comment '创建人',
update_by int unsigned default '0' not null comment '修改人',
is_delete tinyint unsigned default '0' not null comment '是否删除。默认0,1-是,0-否',
...
create_point point not null comment '创建时坐标'
)
comment '团信息表';
create spatial index create_point
on group_ride_info (create_point);
其中,create_point 是通过 POINT 字段记录的经纬度坐标,POINT 字段建议设置为为空。同时需要给 PIOINT 类型字段创建空间索引。
alter table group_ride_info add SPATIAL index(create_point);
定义 GeoPoint 对象
@Builder
@AllArgsConstructor
@Data
public class GeoPoint implements Serializable {
/**
* 经度
*/
private Double longitude;
/**
* 纬度
*/
private Double latitude;
}
定义表对象
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("group_ride_info")
@ApiModel(value="GroupRideInfo对象", description="团信息表")
public class GroupRideInfo implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键id")
@TableId(value = "id", type = IdType.NONE)
private Long id;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
@ApiModelProperty(value = "修改时间")
private LocalDateTime updateTime;
@ApiModelProperty(value = "创建人")
private Integer createBy;
@ApiModelProperty(value = "修改人")
private Integer updateBy;
@ApiModelProperty(value = "是否删除。默认0,1-是,0-否")
private Integer isDelete;
...
@ApiModelProperty(value = "创建时坐标")
private GeoPoint createPoint;
}
定义坐标转换器 GeoPointConverter
public class GeoPointConverter {
/**
* Little endian or Big endian
*/
private int byteOrder = ByteOrderValues.LITTLE_ENDIAN;
/**
* Precision model
*/
private PrecisionModel precisionModel = new PrecisionModel();
/**
* Coordinate sequence factory
*/
private CoordinateSequenceFactory coordinateSequenceFactory = CoordinateArraySequenceFactory.instance();
/**
* Output dimension
*/
private int outputDimension = 2;
/**
* Convert byte array containing SRID + WKB Geometry into Geometry object
*/
public GeoPoint from(byte[] bytes) {
if (bytes == null) {
return null;
}
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) {
// Read SRID
byte[] sridBytes = new byte[4];
inputStream.read(sridBytes);
int srid = ByteOrderValues.getInt(sridBytes, byteOrder);
// Prepare Geometry factory
GeometryFactory geometryFactory = new GeometryFactory(precisionModel, srid, coordinateSequenceFactory);
// Read Geometry
WKBReader wkbReader = new WKBReader(geometryFactory);
Geometry geometry = wkbReader.read(new InputStreamInStream(inputStream));
Point point = (Point) geometry;
// convert to GeoPoint
GeoPoint geoPoint = new GeoPoint(point.getX(), point.getY());
return geoPoint;
} catch (IOException | ParseException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Convert Geometry object into byte array containing SRID + WKB Geometry
*/
public byte[] to(GeoPoint geoPoint) {
if (geoPoint == null) {
return null;
}
Coordinate coordinate = new Coordinate(geoPoint.getLongitude(), geoPoint.getLatitude());
CoordinateArraySequence coordinateArraySequence = new CoordinateArraySequence(new Coordinate[]{coordinate}, 2);
Point point = new Point(coordinateArraySequence, new GeometryFactory());
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
// Write SRID
byte[] sridBytes = new byte[4];
ByteOrderValues.putInt(point.getSRID(), sridBytes, byteOrder);
outputStream.write(sridBytes);
// Write Geometry
WKBWriter wkbWriter = new WKBWriter(outputDimension, byteOrder);
wkbWriter.write(point, new OutputStreamOutStream(outputStream));
return outputStream.toByteArray();
} catch (IOException ioe) {
throw new IllegalArgumentException(ioe);
}
}
}
定义 GeoPointTypeHandler
@MappedTypes({GeoPoint.class})
public class GeoPointTypeHandler extends BaseTypeHandler<GeoPoint> {
GeoPointConverter converter = new GeoPointConverter();
@Override
public void setNonNullParameter(PreparedStatement ps, int i, GeoPoint parameter, JdbcType jdbcType) throws SQLException {
ps.setBytes(i, converter.to(parameter));
}
@Override
public GeoPoint getNullableResult(ResultSet rs, String columnName) throws SQLException {
return converter.from(rs.getBytes(columnName));
}
@Override
public GeoPoint getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return converter.from(rs.getBytes(columnIndex));
}
@Override
public GeoPoint getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return converter.from(cs.getBytes(columnIndex));
}
}
配置扫描 TypeHandler
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = {
"${spring.xxx.data.db.basepackage}"}, sqlSessionFactoryRef = "sqlSessionFactoryMasterDb")
@Slf4j
public class DbConfig implements TransactionManagementConfigurer {
/**
* 注释
*
* @return SqlSessionFactory
* @throws Exception 异常
*/
@Bean
public SqlSessionFactory sqlSessionFactoryMasterDb() throws Exception {
MybatisSqlSessionFactoryBean factoryBean =
new MybatisSqlSessionFactoryBean();
...
// 此处为定义TypeHandler所在的包名
factoryBean.setTypeHandlersPackage("com.xxx.module.typehandler");
return factoryBean.getObject();
}
}
注意,此处代码仅为示例代码,关键代码在factoryBean.setTypeHandlersPackage("com.xxx.module.typehandler");
SpringBoot 项目也可以在配置文件中配置,请自行百度,目的是让自定义 TypeHandler 生效。
MyBatis 使用 POINT
原生 getById()
自定义 SQL(指定范围查找)
- Mapper 接口中定义方法
List<GroupRideInfo> getListByPoint2(Integer distance, GeoPoint point, String ticket);
- Mapper.xml 中定义查询语句
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.xxx.groupride.domain.po.GroupRideInfo">
<id column="id" property="id" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
<result column="create_by" property="createBy" />
<result column="update_by" property="updateBy" />
<result column="is_delete" property="isDelete" />
...
<result column="create_point" property="createPoint" />
</resultMap>
<select id="getListByPoint2" resultMap="BaseResultMap">
SELECT *,
ST_Distance_Sphere(create_point, #{point}) AS distance_meters
FROM group_ride_info
HAVING distance_meters < #{distance} and status=1 and ticket=#{ticket}
ORDER BY distance_meters asc
</select>
- 调用方法
可以看到,可以在 MyBatis 中像普通类型参数一样使用 POINT 了。上面示例仅列举了查询操作,新增/修改也是可以的。