【问题标题】:How to sort query results by distance in Laravel QueryBuilder / MySQL Spatial package?如何在 Laravel Query Builder / MySQL Spatial 包中按距离对查询结果进行排序?
【发布时间】:2018-03-13 20:24:26
【问题描述】:

首先我想向您展示当前的数据库结构。共有三个表:

菜肴(id、名称)

位置(id、名称、坐标(POINT))

dish_location(location_id,dish_id)

现在我想实现一个 API,它获取用户的位置(纬度、经度)并返回按距离(以公里为单位)排序的菜肴列表。我已经有一个方法,它需要两个纬度和两个经度,然后给我距离。但我相信您可以向我展示一种方法,这是一种直接在 MySQL 查询中执行此操作的更高效的方法。

附加:我想在 API 中执行“加载更多”功能。所以我传递了已经收到的物品的数量,或者在这种情况下我将如何解决这个问题?

我将这个用于MySQL Spatial package

【问题讨论】:

  • 这是一个两部分排序。您以用户的位置为中心绘制一个正方形。然后你减去/添加一个最大距离来搜索以获得 4 个最小和最大点。然后查询数据库以返回该正方形内的结果。然后,您使用真正的公式对这个子集执行真正的排序,我相信 hasrsine。
  • 这个解决方案的问题是,这是一个无限列表。
  • 那你得想办法限制列表...

标签: php mysql laravel laravel-5 eloquent


【解决方案1】:

Postgis 有两个运算符( 和 )来计算距离和计算 KNN(最近邻)。您可以使用它们代替 st_distance。 Operators 查询类似于:

WITH index_query AS (
 SELECT ST_Distance(geom, 'SRID=3005;POINT(1011102 450541)'::geometry) as d,edabbr, vaabbr
FROM va2005
ORDER BY geom <-> 'SRID=3005;POINT(1011102 450541)'::geometry LIMIT 100)
SELECT *
   FROM index_query
ORDER BY d limit 10;

使用距离作为 d 你可以订购。

【讨论】:

    【解决方案2】:

    首先,让我们看一下如何使用基本查询生成器来执行此操作。然后,我们将讨论如何使用 Eloquent 模型执行此查询:

    function paginateDishesFromPoint(Point $point, $pageSize) 
    {
        $distanceField = "ST_Distance_Sphere(locations.coordinates, "
            . "ST_GeomFromText('{$point->toWKT()}') AS distance"; 
    
        return DB::table('dishes') 
            ->select('dishes.*', DB::raw($distanceField))
            ->join('dish_locations', 'dish_locations.dish_id', '=', 'dishes.id')
            ->join('locations', 'locations.id', '=', 'dish_locations.location_id')
            ->orderBy('distance') 
            ->paginate($pageSize);
    }
    

    ST_Distance_Sphere() 函数计算我们可以对结果进行排序的距离。 Laravel 的 paginate() 方法使用通过请求 URL 传递的 page 参数为我们执行自动分页。阅读pagination docs 了解更多信息。使用上面的函数,我们可以获取一个分页的结果集,如下所示:

    $point = new Point($latitude, $longitude); 
    $sortedDishes = paginateDishesFromPoint($point, 15); 
    

    ...其中Point 是我们正在使用的the package 中的Grimzy\LaravelMysqlSpatial\Types\Point 类,15 是每页的结果数。

    现在,让我们尝试用 Eloquent 模型来做这件事。我们将使用local query scope 来封装创建执行排序的查询部分所需的逻辑:

    class Dish extends Model 
    {
        ...
    
        public function locations() 
        {
            return $this->belongsToMany(App\Location::class);
        }
    
        public function scopeOrderByDistanceFrom($query, Point $point) 
        {
            $relation = $this->locations();
            $locationsTable = $relation->getRelated()->getTable();
            $distanceField = "ST_Distance_Sphere($locationsTable.coordinates, "
            . "ST_GeomFromText('{$point->toWKT()}') AS distance";
    
            return $query
                ->select($this->getTable() . '.*', DB::raw($distanceField))
                ->join(
                    $relation->getTable(), 
                    $relation->getQualifiedForeignKeyName(), 
                    '=', 
                    $relation->getQualifiedParentKeyName()
                )
                ->join(
                    $locationsTable,
                    $relation->getRelated()->getQualifiedKeyName(),
                    '=', 
                    $relation->getQualifiedRelatedKeyName()
                )
                ->orderBy('distance');
        }
    }
    

    此实现使用模型上的元数据将表和字段名称添加到查询中,因此如果它们发生更改,我们无需更新此方法。现在我们可以使用模型获取有序集:

    $point = new Point($latitude, $longitude); 
    $sortedDishes = Dish::orderByDistanceFrom($point)->paginate($pageSize);
    

    $sortedDishes 是 Laravel 的 LengthAwarePaginator 的一个实例,它包装了模型的 Collection。如果我们将结果传递给视图,以下是如何在 Blade 模板中显示它们:

    <ul>
        @foreach($sortedDishes as $dish) 
            <li>{{ $dish->name }} is {{ $dish->distance }} meters away.</li>
        @endforeach
    </ul>
    
    <a href="{{ $sortedDishes->nextPageUrl() }}">Load more...</a>
    

    如上所示,分页器提供了convenience methods,我们可以使用它在分页结果之间轻松移动。

    或者,我们可以使用 AJAX 请求来加载结果。只要确保在请求数据的page参数中传递当前页面+1即可。

    【讨论】:

    • 谢谢!这解决了我的问题。你有什么想法,我怎么能把菜的位置作为结果数组中的字段?在模型中,我有一个用于位置关系的函数位置(),但我只需要我们要加入的“一个位置”..
    • 不客气!我们可以将"$locationsTable.name AS location_name" 添加到对select() 的调用中,这将作为$dish-&gt;location_name 提供。对我来说,“位置”与“菜肴”是多对多的关系,这对我来说似乎很奇怪。一道菜真的可以存在多个地方吗?
    【解决方案3】:

    SQL

    SELECT d.name AS `Dish`,
           l.name AS `Location`,
           ST_Distance(l.coordinates, POINT(<<<longitude>>>, <<<latitude>>>)) AS `Distance`
    FROM dish_locations dl
    JOIN dishes d
    ON d.id = dl.dish_id
    JOIN locations l
    ON l.id = dl.location_id
    ORDER BY `Distance`
    LIMIT <<<n-1>>>, <<<page size>>>;
    

    ...从用户的位置插入&lt;&lt;&lt;longitude&gt;&gt;&gt;&lt;&lt;&lt;latitude&gt;&gt;&gt;,从已经检索到的行数减一中插入&lt;&lt;&lt;n-1&gt;&gt;&gt;&lt;&lt;&lt;page size&gt;&gt;&gt; 作为下一个要检索的行数。

    演示

    http://rextester.com/YAEBF16430

    说明

    【讨论】:

    • 谢谢!现在最大的问题是,如果你知道如何将这个纯 SQL 查询“转换”为 Laravel 代码以使用 QueryBuilder (laravel.com/docs/5.5/queries) 构建这个查询。所以我可以从模型的自动匹配和关系的加载中获益。
    • 抱歉,我对 Laravel 不熟悉,但 Cy Rossignol 的回答似乎已经解决了这样做的一种方法。
    猜你喜欢
    • 1970-01-01
    • 2015-08-06
    • 2015-04-27
    • 2019-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多