使用 Mongodb 进行地理空间查询
目前越来越多的项目和产品,需要具有空间查询的需求,如外卖送餐时骑手的定位,地图上搜索以自己为中心点附近的餐厅等等,所以当前基本上所有的关系型数据库以及 nosql 数据库都具有空间查询的函数功能。但是总体而言 nosql 数据库的空间查询性能更高,这里不深入探讨具体的原因,有兴趣可以自行查询资料或动手试验对比。本篇博客主要从代码层面介绍如何通过 SpringData 操作 mongodb 实现对空间数据的查询操作。
对于空间查询,一般分为平面几何空间查询,以及球面地理空间查询,绝大多数情况下,我们都使用球面地理空间,这就要求每个点必须是有效的经纬度点。对于 mongodb 来说,需要针对经纬度数据建立 2DSphere 索引,然后才能进行球面地理空间查询。本篇博客主要介绍最常用的点是否在面内的判断,以及在查询结果中显示与给定点的距离等等,在本篇博客的最后会提供源代码下载。
Mongodb 的中文官网地址:https://www.mongodb.com/zh-cn
Spring Data Mongodb 的官网地址:https://spring.io/projects/spring-data-mongodb
一、搭建工程
我的虚拟机 ip 地址是:192.168.136.128,仍然使用 docker-compose 部署 mongodb,初始化一个 root 角色的账号 jobs ,密码是 123456。新建一个 SpringBoot 工程,结构如下所示:
Company 是公司类,针对 mongodb 数据库中要操作的表 tb_company 而创建。
MongoGeoTest 类中编写了一些测试方法,用来对 mongodb 中 tb_company 表进行空间查询。
该工程的 pom 文件内容如下:(最主要的是引入 spring-boot-starter-data-mongodb 这个依赖)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jobs</groupId>
<artifactId>springboot_mongo_geo</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.9.RELEASE</version>
</parent>
<dependencies>
<!--引入最基本的 springboot 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--引入 springdata mongodb 的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!--引入 test 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入该依赖,可以打印日志,以及省去实例类的 get 和 set 方法-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
项目工程中的 application.yml 文件如下,主要配置了 mongodb 的连接字符串
spring:
data:
mongodb:
# 连接字符串格式
# mongodb://用户名:密码@Ip地址:端口/数据库名
# 如果使用的是 root 角色的用户登录,则必须在后面加上 authSource=admin 参数
# 之前在 admin 库中创建了一个 root 角色的账号 jobs
# 在实际项目中,强烈建议,针对每个数据库创建一个 readwrite 角色的用户
uri: mongodb://jobs:123456@192.168.136.128:27017/mytest?authSource=admin
# 允许在实体类上,索引注解生效,可以在 mongodb 表中建立相应的索引
auto-index-creation: true
二、代码细节
实体类 Company 的具体细节如下:
package com.jobs.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexType;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoId;
//使用该注解,类中可以省略 get set 等方法的编写
@Data
//使用该注解,标明要操作的 mongodb 的文档(相当于数据库的表)
@Document("tb_company")
//使用该注解,可以使用对象实例化赋值采用链式编写
@Accessors(chain = true)
public class Company {
//使用该注解,标明 mongodb 文档的主键 id
@MongoId
private String id;
//如果 mongodb 文档的字段名与该实体类的字段名不一致
//使用该注解,标明 mongodb 文档中实际的字段名
@Field("cname")
private String name; //企业名称
//该索引表示空间索引(必须要使用该索引,否则空间查询可能会报错)
//GEO_2DSPHERE 表示类似球面数据存储索引
//数组中,0 索引存储经度,1 索引存储纬度
@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
private double[] location;
}
需要注意的是:必须要有 @GeoSpatialIndexed 的索引注解,这样 mongodb 才知道哪个字段是空间字段。
如上所示针对 location 数组创建了地理空间索引(索引的类型是 2dSphere )
在 MongoGeoTest 类中编写了空间查询的测试代码,具体细节如下:
package com.jobs.test;
import com.jobs.pojo.Company;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.*;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.geo.GeoJsonPolygon;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
public class MongoGeoTest {
//注入 spring-data-mongodb 自带的 MongoTemplate 对象
@Autowired
private MongoTemplate mongoTemplate;
//添加空间测试的样例数据
@Test
public void addSampleTestData() {
List<Company> companyList = new ArrayList<>();
companyList.add(new Company().setName("任肥肥红烧肉")
.setLocation(new double[]{116.409488, 39.917015}));
companyList.add(new Company().setName("侯胖胖烤全鱼")
.setLocation(new double[]{116.39818, 39.918844}));
companyList.add(new Company().setName("王棒棒火锅店")
.setLocation(new double[]{116.411615, 39.921669}));
companyList.add(new Company().setName("李墩墩咖啡馆")
.setLocation(new double[]{116.409229, 39.920868}));
companyList.add(new Company().setName("范呆呆羊肉汤")
.setLocation(new double[]{116.3949, 39.97528}));
companyList.add(new Company().setName("蔺赞赞刀削面")
.setLocation(new double[]{116.398053, 39.920301}));
companyList.add(new Company().setName("杨壮壮家常菜")
.setLocation(new double[]{116.412929, 39.917812}));
companyList.add(new Company().setName("乔豆豆大排档")
.setLocation(new double[]{116.409182, 39.921745}));
//批量添加 8 条样例数据
mongoTemplate.insert(companyList, Company.class);
}
//给定一个中心点,查询 800 米以内的公司
@Test
public void testCircle() {
//指定中心点
GeoJsonPoint point = new GeoJsonPoint(116.404, 39.915);
//KILOMETERS 表示千米
Distance distance = new Distance(0.8, Metrics.KILOMETERS);
//构建一个圆形范围
Circle circle = new Circle(point, distance);
//构造查询条件
Query query = Query.query(Criteria.where("location").withinSphere(circle));
//5、查询
List<Company> list = mongoTemplate.find(query, Company.class);
list.forEach(System.out::println);
}
//查询距离给定的中心点 800 米以内的公司,并按照距离,由近到远进行排列。
@Test
public void testNearest() {
//指定中心点
GeoJsonPoint point = new GeoJsonPoint(116.404, 39.915);
//指定查询条件
NearQuery query = NearQuery.near(point).maxDistance(new Distance(0.8, Metrics.KILOMETERS));
//由于 Company 的字段 location 设置了空间索引,因此针对该字段与给定点进行距离计算并筛选。
GeoResults<Company> results = mongoTemplate.geoNear(query, Company.class);
//打印查询结果,按照距离给定点的距离由近到远排列
for (GeoResult<Company> result : results) {
Company company = result.getContent();
double value = result.getDistance().getValue();
System.out.println(company.getName() + " 距离给定点:" + (int) (value * 1000) + " 米");
}
}
//查询自定义多边形区域内的公司
@Test
public void testRectangle() {
//构建一个三角形(需要注意:多边形首尾两个点的坐标必须相同)
List<Point> points = new ArrayList<>();
points.add(new Point(116.361, 39.924));
points.add(new Point(116.415, 39.933));
points.add(new Point(116.393, 39.891));
points.add(new Point(116.361, 39.924));
GeoJsonPolygon polygon = new GeoJsonPolygon(points);
Query query = Query.query(Criteria.where("location").within(polygon));
List<Company> list = mongoTemplate.find(query, Company.class);
list.forEach(System.out::println);
}
}
当然 mongodb 也支持点、线、面,以及之间的交互查询判断,这里就不再介绍了,一般很少使用。
有兴趣的话,可以查询其它网上的相关资料,以及 Spring Data Mongodb 官网的 api 文档介绍。
本篇博客的源代码下载地址:https://files.cnblogs.com/files/blogs/699532/springboot_mongo_geo.zip