使用 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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通