2013-03-12 4 views
2

Я пытаюсь получить поиск по периметру, работая с DBIx :: Class, но пока этого не удалось.DBIx :: Поиск по периметру класса

SQL, я хотел бы, чтобы генерировать выглядит так:

SELECT 
    zip, 
    6371 * ACos(Cos(RADIANS(Lat)) * Cos(RADIANS(USERLAT)) * Cos(RADIANS(USERLNG) - RADIANS(Lng)) + Sin(RADIANS(Lat)) * Sin(RADIANS(USERLAT))) AS Distance 
FROM 
    geopc 
WHERE 
    6371 * ACos(Cos(RADIANS(Lat)) * Cos(RADIANS(USERLAT)) * Cos(RADIANS(USERLNG) - RADIANS(Lng)) + Sin(RADIANS(Lat)) * Sin(RADIANS(USERLAT))) <= DISTANCE 
ORDER BY 
    Distance 

Где USERLAT, USERLNG и DISTANCE должны быть переменными, которые придут в через в WebRequest.

Моих DBIx :: Класс Результат:

use utf8; 
package MyApp::Models::Schema::Result::Geopc; 

use strict; 
use warnings; 

use base 'DBIx::Class::Core'; 

__PACKAGE__->table("geopc"); 

__PACKAGE__->add_columns(
    "id", 
    { data_type => "bigint", is_nullable => 0, is_auto_increment => 1 }, 
    "country", 
    { data_type => "varchar", is_nullable => 0, size => 2 }, 
    "language", 
    { data_type => "varchar", is_nullable => 0, size => 2 }, 
    "iso2", 
    { data_type => "varchar", is_nullable => 0, size => 6 }, 
    "region1", 
    { data_type => "varchar", is_nullable => 0, size => 60 }, 
    "region2", 
    { data_type => "varchar", is_nullable => 0, size => 60 }, 
    "region3", 
    { data_type => "varchar", is_nullable => 0, size => 60 }, 
    "region4", 
    { data_type => "varchar", is_nullable => 0, size => 60 }, 
    "zip", 
    { data_type => "varchar", is_nullable => 0, size => 10 }, 
    "city", 
    { data_type => "varchar", is_nullable => 0, size => 60 }, 
    "area1", 
    { data_type => "varchar", is_nullable => 0, size => 80 }, 
    "area2", 
    { data_type => "varchar", is_nullable => 0, size => 80 }, 
    "lat", 
    { data_type => "double precision", is_nullable => 0 }, 
    "lng", 
    { data_type => "double precision", is_nullable => 0 }, 
    "tz", 
    { data_type => "varchar", is_nullable => 0, size => 30 }, 
    "utc", 
    { data_type => "varchar", is_nullable => 0, size => 10 }, 
    "dst", 
    { data_type => "varchar", is_nullable => 0, size => 1 }, 
); 
__PACKAGE__->set_primary_key('id'); 

Я гугл вокруг, но не нашел хороший способ справиться с этим. Любая помощь будет очень высоко ценится.

Я использую MySQL ...

+0

Лично я использовал бы хранимую процедуру для этого. Вы не можете привязать параметр к функции arg в select. – jordanm

+0

Вы должны сделать это с помощью подзапроса и недокументированного атрибута 'from' ResultSet. Я вижу, работает ли это завтра. – nwellnhof

ответ

1

Одно решение для таких сложных запросов, чтобы определить их как view. Это имеет преимущество, заключающееся в том, что вы входите в команду join и prefetch, если вы определяете отношения с ним.

Еще один, чтобы использовать columns для расчетного столбца «расстояние». «Столбцы» - это всего лишь комбинация параметров «select» и «as», которые, как доказано, являются более надежными api, что приводит к меньшим ошибкам пользователя. Обратите внимание, что синтаксис поиска происходит от SQL::Abstract и предоставляет некоторые средства для использования literal sql.

Лучшее решение без подзапроса:

my $param = \[ 
     '6371 * ACos(Cos(RADIANS(Lat)) * Cos(RADIANS(?)) * Cos(RADIANS(?)' . 
     ' - RADIANS(Lng)) + Sin(RADIANS(Lat)) * Sin(RADIANS(?)))' 
     [ USERLAT => $USERLAT ], 
     [ USERLNG => $USERLNG ], 
     [ USERLAT => $USERLAT ], 
    ]; 

my $geopc = $schema->resultset('Result::Geopc')->search({ 
     $param => { '<=', $distance }, 
    }, { 
     columns => [ 
      'zip', 
      { distance => $param } 
     ], 
     order_by => $param, 
    }); 
+0

Он не может использовать представление, потому что он не может привязать параметр к 'USERLAT'. Это была моя первая мысль. – jordanm

+0

@jordanm да, это правда в этом случае, это было всего лишь общее предложение. –

1

У меня была та же проблема: у меня есть companies которые belongs_toaddress, поэтому адрес has_manycompanies - мне нужно, чтобы найти соседние компании, так, используя Adress модель:

__PACKAGE__->add_columns(
    "id", 
    { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, 
    "country", 
    { data_type => "varchar", is_nullable => 0, size => 64 }, 
    "county", 
    { data_type => "varchar", is_nullable => 1, size => 45 }, 
    "city", 
    { data_type => "varchar", is_nullable => 0, size => 64 }, 
    "street", 
    { data_type => "varchar", is_nullable => 0, size => 128 }, 
    "street_no", 
    { data_type => "varchar", is_nullable => 1, size => 24 }, 
    "apartment_no", 
    { data_type => "varchar", is_nullable => 1, size => 24 }, 
    "extra", 
    { data_type => "varchar", is_nullable => 1, size => 128 }, 
    "lat", 
    { data_type => "decimal", is_nullable => 1, size => [10, 7] }, 
    "long", 
    { data_type => "decimal", is_nullable => 1, size => [10, 7] }, 
); 

Я реализовал метод get_neighbour_companies в этой модели:

sub get_neighbour_companies{ 
    my ($self, $args) = @_; 

    my $distance = $args->{distance} // 15; 

    my $geo_clause = sprintf('(6371 * acos(cos(radians(%s)) * cos(radians(me.lat)) * cos(radians(me.`long`) - radians(%s)) + sin(radians(%s)) * sin(radians(me.lat)))) AS distance', $self->lat, $self->long, $self->lat); 

    my $rs = $self->result_source->schema->resultset('Address') 
    ->search_rs(
     { 
     'companies.company_type_id' => ($args->{company_type_id} // 1), #defaults to 'orderer' type 
     }, 
     { 
     prefetch => { 'companies' => 'address' }, 
     select => [ 'id', \$geo_clause ], 
     as  => [qw/ id distance /], 
     having => { distance => { '<=' => $distance } }, 
     order_by => 'distance', 
     } 
    ); 

    my @companies; 
    while (my $address = $rs->next){ 
    my @comps = $address->companies()->all; 
    next unless @comps; 

    foreach my $company (@comps) { 
     push @companies, { 
      company => $company,    
      distance => $address->get_column('distance'), 
      }; 
    }  
    }; 
    return [ @companies ]; 
} 

Я использую это так:

my $customers = $comp->address->get_neighbour_companies({ 
      distance  => 12, 
      company_type_id => 1, 
     }); 

где $customers будет исх массив в список companies в пределах 12 км от $comp, который также является company

+0

Ваше решение не использует значения привязки для заданных пользователем значений, которые могут привести к внедрению sql. –

+1

У меня почти идентичный код. В прошлый раз я пробовал (около 18 месяцев назад?) SQL :: Abstract не смог обработать запрос - комбинацию «AS», «HAVING» и «ORDER BY» и функции с несколькими параметрами. Конечно, это может измениться. Что касается возможной инъекции SQL - это только расстояние и лат/longs, которые были интерполированы, первое легко освободить, а остальные будут поступать из других строк в базе данных. – plusplus

+0

@abraxxa - как упоминалось в @plusplus, я использую только '$ self-> lat' и' $ self-> long' в интерполяции - те, которые поступают из БД и имеют тип 'DECIMAL' - если вы можете понять что-то, что может пойти не так, пожалуйста, дайте мне знать - иначе, да, я не использую или не поощряю этот тип построения запроса –

0

Вы можете переписать ваш запрос на использование подзапроса следующим образом:

SELECT zip, Distance 
FROM (SELECT zip, 
     6371 * ACos(Cos(RADIANS(Lat)) * Cos(RADIANS(USERLAT)) * Cos(RADIANS(USERLNG) - RADIANS(Lng)) + Sin(RADIANS(Lat)) * Sin(RADIANS(USERLAT))) 
     AS Distance 
    FROM geopc) AS tmp 
WHERE Distance <= DISTANCE 
ORDER BY Distance 

Тогда-то вроде следующего должно работать:

my $geopc = $schema->resultset('Result::Geopc'); 

my $subquery = $geopc->search({}, { 
    select => [ 
     'zip', 
     \[ 
      '6371 * ACos(Cos(RADIANS(Lat)) * Cos(RADIANS(?)) * Cos(RADIANS(?)' . 
      ' - RADIANS(Lng)) + Sin(RADIANS(Lat)) * Sin(RADIANS(?)))' . 
      ' AS Distance', 
      [ USERLAT => $USERLAT ], 
      [ USERLNG => $USERLNG ], 
      [ USERLAT => $USERLAT ], 
     ], 
    ], 
})->as_query; 

my $rs = $geopc->search({ 
    Distance => { '<=' => $DISTANCE }, 
}, { 
    alias => 'geopc2', 
    from => [ 
     { geopc2 => $subquery }, 
    ], 
    select => [ qw(zip Distance) ], 
    order_by => 'Distance', 
}); 

Этот подход использует literal SQL with placeholders и недокументированные атрибут ResultSet from. Некоторые примеры использования атрибута from можно найти в DBIx::Class test suite. Обратите внимание: поскольку этот атрибут недокументирован, он может не поддерживаться в будущих версиях.