Laravel Query Builder如何筛选指定经纬度范围的关联Advertisement?
嘿,这个需求我刚好折腾过类似的,给你两种靠谱的实现思路,你可以根据自己的业务场景选:
方法一:使用
whereHas关联筛选(更贴合Laravel Eloquent风格) 因为你用的是多对多关联,用whereHas可以很优雅地约束关联的Location模型,在闭包里加入地理范围的判断逻辑就行。这里推荐用MySQL原生的空间函数来计算距离,效率比手写公式高很多:
// 先定义好目标经纬度和搜索半径(这里半径单位是米,比如1000米=1公里) $targetLong = 116.403874; $targetLat = 39.914885; $radius = 1000; $query = QueryBuilder::for(Advertisement::class) ->with('locations') ->whereHas('locations', function ($query) use ($targetLong, $targetLat, $radius) { // 用MySQL的ST_Distance_Sphere计算两点球面距离(返回单位是米) $query->whereRaw( "ST_Distance_Sphere(point(long, latitude), point(?, ?)) <= ?", [$targetLong, $targetLat, $radius] ); }); // 拿到符合条件的广告数据 $advertisements = $query->get();
兼容旧版MySQL的备选方案
如果你的MySQL版本低于5.7.6(不支持ST_Distance_Sphere),可以用经典的Haversine公式来计算距离:
$query->whereRaw( "(6371 * acos(cos(radians(?)) * cos(radians(latitude)) * cos(radians(long) - radians(?)) + sin(radians(?)) * sin(radians(latitude)))) <= ?", [$targetLat, $targetLong, $targetLat, $radius/1000] );
这里6371是地球半径(单位:公里),所以要把原本的米制半径转成公里(比如$radius/1000)。
方法二:直接JOIN关联表(大数据量下性能更优)
如果你的广告和地点数据量很大,whereHas生成的子查询可能会有点慢,这时候直接JOIN中间表和Location表是更好的选择:
$query = QueryBuilder::for(Advertisement::class) ->with('locations') ->join('advertisement_locations', 'advertisements.id', '=', 'advertisement_locations.advertisement_id') ->join('locations', 'advertisement_locations.location_id', '=', 'locations.id') ->whereRaw( "ST_Distance_Sphere(point(locations.long, locations.latitude), point(?, ?)) <= ?", [$targetLong, $targetLat, $radius] ) ->select('advertisements.*') // 只选广告表的字段,避免和关联表字段冲突 ->distinct(); // 必须加!因为多对多关联会导致同一广告出现多次重复记录 $advertisements = $query->get();
额外优化建议
- 确保
Location表的long和latitude字段是数值类型(比如DECIMAL(10,8)或者FLOAT) - 如果经常做地理范围查询,给
(long, latitude)建立空间索引,能大幅提升查询速度:CREATE SPATIAL INDEX idx_locations_coords ON locations(long, latitude); - 注意单位统一:
ST_Distance_Sphere返回的是米,Haversine公式用6371时返回的是公里,别搞混了!
内容的提问来源于stack exchange,提问作者Jenssen
相关产品推荐
相关产品推荐

