当前位置: 首页 > article >正文

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(指定范围查找)

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

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


http://www.kler.cn/a/394244.html

相关文章:

  • 单片机:实现数码管动态显示(0~99999999)74hc138驱动(附带源码)
  • 【微信小程序】2|轮播图 | 我的咖啡店-综合实训
  • 个人笔记:ORM数据库框架EFCore使用示例,运行通过,附源码
  • MySQL 8.0:explain analyze 分析 SQL 执行过程
  • 【QSS样式表 - ⑥】:QPushButton控件样式
  • 抢单人机交互「新红利」!哪些细分赛道“多金”?
  • MATLAB用CNN-LSTM神经网络的语音情感分类深度学习研究
  • [DDNS][SSL][HTTPS]阿里云ACME Ubuntu22.04Server 安装教程
  • Mysql每日一题(行程与用户,困难※)
  • Vue3 项目权限控制最佳实践
  • 消息队列系列一:RabbitMQ入门讲解
  • git 提交报错 Error updating changes: bad signature 0x00000000index file corrupt
  • STM32编程遇到的问题随笔【一】
  • 深入探索React合成事件(SyntheticEvent):跨浏览器的事件处理利器
  • Windowos系统部署mino
  • Spring Boot应用开发实战:构建高效、可维护的Web应用
  • linux startup.sh shutdown.sh (kkFileView)
  • HBase理论_HBase架构组件介绍
  • IEC61850服务分类说明
  • SpringBoot 3.3.5 试用CRaC,启动速度提升3到10倍
  • 【学习率】
  • 时间序列分析——移动平均法、指数平滑法、逐步回归法、趋势外推法等(基于Python实现)
  • SpringBoot(二十二)SpringBoot集成smart-doc自动生成文档
  • gorm使用注意事项
  • 轮转数组
  • 旅行照片 4.0 问题三、四