GeoDjango дистанционный фильтр с дистанционным значением, хранящимся в модели - запрос
У меня есть модель Order
, имеющая origin
PointField и range
IntegerField. Кроме того, существует модель UserProfile
, которая имеет geo_location
PointField. Теперь у меня есть экземпляр User
, User
. Я хочу выбрать все Orders
, расстояние между Order.origin
и user.userprofile.geo_location
меньше значения (метров) в поле модели Order.range
.
Итак, упрощенные модели:
class Order(models.Model):
origin = models.PointField()
range = models.IntegerField(blank=True, default=10000)
class UserProfile(models.Model):
geo_location = models.PointField()
У меня это работает (прохождение расстояния статически):
>>> Order.objects.filter(origin__distance_lte=(user.profile.geo_location, D(m=3000)))
Моя следующая (неудачная) попытка заключалась в использовании выражения F() для использования значения из поля Order.range
:
>>> Order.objects.filter(origin__distance_lte=(user.profile.geo_location, D(m=F('range'))))
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/contrib/gis/measure.py", line 165, in __init__
self.m, self._default_unit = self.default_units(kwargs)
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/contrib/gis/measure.py", line 49, in default_units
if not isinstance(value, float): value = float(value)
TypeError: float() argument must be a string or a number
Я думаю, проблема в том, что D() не работает лениво - я могу это понять.
Поэтому я попытался просто взять исходное значение из поля range
(целое число), которое я должен был работать, но:
>>> Order.objects.filter(origin__distance_lte=(user.profile.geo_location, F('range')))
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/query.py", line 69, in __repr__
data = list(self[:REPR_OUTPUT_SIZE + 1])
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/query.py", line 84, in __len__
self._result_cache.extend(self._iter)
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/query.py", line 273, in iterator
for row in compiler.results_iter():
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 680, in results_iter
for rows in self.execute_sql(MULTI):
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 725, in execute_sql
sql, params = self.as_sql()
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 68, in as_sql
where, w_params = self.query.where.as_sql(qn=qn, connection=self.connection)
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/where.py", line 92, in as_sql
sql, params = child.as_sql(qn=qn, connection=connection)
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/where.py", line 95, in as_sql
sql, params = self.make_atom(child, qn, connection)
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/contrib/gis/db/models/sql/where.py", line 47, in make_atom
spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn)
File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/contrib/gis/db/backends/postgis/operations.py", line 531, in spatial_lookup_sql
raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
ValueError: Argument type should be (<class 'decimal.Decimal'>, <class 'django.contrib.gis.measure.Distance'>, <type 'float'>, <type 'int'>, <type 'long'>), got <class 'django.db.models.expressions.F'> instead.
Итак, как я могу выполнить то, что я пытаюсь сделать? Любая помощь приветствуется!
Ответы
Ответ 1
Я думаю, вам придется отбросить SQL-код, чтобы делать то, что вы хотите, поскольку помощники GeoDjango не имеют способа сделать подходящий объект Distance
, который одновременно является объектом Django F
(т.е. поиск в поле). Вы не говорите, какую базу данных вы используете, но вот как вы это делаете с PostGIS:
Order.objects.all().extra(
where=['ST_Distance(origin, ST_PointFromText(%s, 4326)) <= CAST(range AS double precision) / 1000'],
params=[user.profile.geo_location.wkt]
)
Позвольте объяснить, что происходит здесь, потому что это довольно тернистый. Справа слева:
-
.extra()
позволяет добавлять в запрос дополнительные поля и ограничения; здесь мы добавляем ограничение
-
ST_Distance()
- это функция PostGIS, с которой оператор GeoDjango distance_lte
преобразуется в (из документация Django)
-
ST_PointFromText()
преобразует синтаксис WKT в объект PostGIS Geometry
-
4326
SRID по умолчанию для Django, но не для PostGIS, поэтому мы должны указать его
- мы должны
CAST
ваше поле удвоить точность, потому что вы используете целое число
- тогда мы должны разделить на 1000, чтобы конвертировать из метров в километры, что, я считаю, является тем, что нам нужно
- В поля GeoDjango
Point
есть аксессор .wkt
, который дает их представление WKT, которое нам было необходимо ранее в нашем вызове ST_PointFromText()
Обратите внимание, что в соответствии с документацией PostGIS ST_Distance()
не использует индексы, поэтому вам может понадобиться исследовать с помощью ST_DWithin()
вместо этого (он задокументирован сразу после ST_Distance()
).