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 了。上面示例仅列举了查询操作,新增/修改也是可以的。

相关推荐
小徍24 分钟前
MySQL 8.0特性-自增变量的持久化
数据库·mysql
冰红茶兑滴水1 小时前
MySQL 表的约束
数据库·mysql
小五Z1 小时前
MySql--增删改查表设计总结
数据结构·数据库·mysql
杨荧2 小时前
【JAVA毕业设计】基于Vue和SpringBoot的宠物咖啡馆平台
java·开发语言·jvm·vue.js·spring boot·spring cloud·开源
锐策2 小时前
〔 MySQL 〕数据类型
数据库·mysql
门牙咬脆骨2 小时前
【MYSQL】数据库日志 (了解即可)
数据库·mysql
ModelBulider2 小时前
十三、注解配置SpringMVC
java·开发语言·数据库·sql·mysql
豆 腐3 小时前
MySQL【四】
android·数据库·笔记·mysql
苹果酱05673 小时前
C语言 char 字符串 - C语言零基础入门教程
java·开发语言·spring boot·mysql·中间件