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 字段)

sql 复制代码
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 类型字段创建空间索引。

sql 复制代码
alter table group_ride_info add SPATIAL index(create_point);

定义 GeoPoint 对象

java 复制代码
@Builder
@AllArgsConstructor
@Data
public class GeoPoint implements Serializable {

    /**
     * 经度
     */
    private Double longitude;

    /**
     * 纬度
     */
    private Double latitude;
}

定义表对象

java 复制代码
@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

java 复制代码
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

java 复制代码
@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

java 复制代码
@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(指定范围查找)

  1. Mapper 接口中定义方法
java 复制代码
List<GroupRideInfo> getListByPoint2(Integer distance, GeoPoint point, String ticket);
  1. Mapper.xml 中定义查询语句
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 &lt; #{distance} and status=1 and ticket=#{ticket}
  ORDER BY distance_meters asc
</select>
  1. 调用方法

可以看到,可以在 MyBatis 中像普通类型参数一样使用 POINT 了。上面示例仅列举了查询操作,新增/修改也是可以的。

相关推荐
计算机毕设定制辅导-无忧学长18 分钟前
MySQL 实战:小型项目中的数据库应用(二)
数据库·mysql
中科院提名者20 分钟前
python如何操控mysql
开发语言·python·mysql
王佑辉1 小时前
【mysql】如何解决主从架构从库延迟问题
mysql
李少兄2 小时前
使用 MyBatis-Plus Wrapper 构建自定义 SQL 查询
数据库·sql·mybatis
Iam傅红雪2 小时前
修改springboot的配置文件
java·spring boot·spring
云计算老王2 小时前
MySQL 数据类型
android·mysql·adb
璀若星辰3 小时前
Spring Boot 项目中 JUnit 使用总结
spring boot·后端·junit
Dream25123 小时前
【MySQL数据基础】
数据库·mysql
文浩(楠搏万)3 小时前
Java Spring Boot 项目中嵌入前端静态资源:完整教程与实战案例
java·服务器·前端·spring boot·后端·nginx·github
毕业设计-013 小时前
0005.基于SpringBoot+LayUI客户关系管理系统
spring boot·后端·layui