瀏覽代碼

Fixed #28436 -- Added support for distance lookups on MySQL.

Sergey Fedoseev 7 年之前
父節點
當前提交
f3bada9889

+ 4 - 1
django/contrib/gis/db/backends/base/features.py

@@ -37,7 +37,6 @@ class BaseSpatialFeatures:
 
     # The following properties indicate if the database backend support
     # certain lookups (dwithin, left and right, relate, ...)
-    supports_distances_lookups = True
     supports_left_right_lookups = False
 
     # Does the database have raster support?
@@ -58,6 +57,10 @@ class BaseSpatialFeatures:
     def supports_crosses_lookup(self):
         return 'crosses' in self.connection.ops.gis_operators
 
+    @property
+    def supports_distances_lookups(self):
+        return self.has_Distance_function
+
     @property
     def supports_dwithin_lookup(self):
         return 'dwithin' in self.connection.ops.gis_operators

+ 0 - 1
django/contrib/gis/db/backends/mysql/features.py

@@ -10,7 +10,6 @@ class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
     supports_distance_geodetic = False
     supports_length_geodetic = False
     supports_area_geodetic = False
-    supports_distances_lookups = False
     supports_transform = False
     supports_real_shape_operations = False
     supports_null_geometries = False

+ 14 - 0
django/contrib/gis/db/backends/mysql/operations.py

@@ -4,6 +4,7 @@ from django.contrib.gis.db.backends.base.operations import (
 )
 from django.contrib.gis.db.backends.utils import SpatialOperator
 from django.contrib.gis.db.models import GeometryField, aggregates
+from django.contrib.gis.measure import Distance
 from django.db.backends.mysql.operations import DatabaseOperations
 from django.utils.functional import cached_property
 
@@ -87,6 +88,19 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
     def geo_db_type(self, f):
         return f.geom_type
 
+    def get_distance(self, f, value, lookup_type):
+        value = value[0]
+        if isinstance(value, Distance):
+            if f.geodetic(self.connection):
+                raise ValueError(
+                    'Only numeric values of degree units are allowed on '
+                    'geodetic distance queries.'
+                )
+            dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
+        else:
+            dist_param = value
+        return [dist_param]
+
     def get_db_converters(self, expression):
         converters = super().get_db_converters(expression)
         if isinstance(expression.output_field, GeometryField) and self.uses_invalid_empty_geometry_collection:

+ 5 - 1
docs/ref/contrib/gis/geoquerysets.txt

@@ -606,7 +606,7 @@ PostGIS equivalent::
 Distance Lookups
 ================
 
-*Availability*: PostGIS, Oracle, SpatiaLite, PGRaster (Native)
+*Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Native)
 
 For an overview on performing distance queries, please refer to
 the :ref:`distance queries introduction <distance-queries>`.
@@ -639,6 +639,10 @@ spheroid based lookups.
 
     Support for the ``'spheroid'`` option on SQLite was added.
 
+.. versionadded:: 2.0
+
+    MySQL support was added.
+
 .. fieldlookup:: distance_gt
 
 ``distance_gt``

+ 2 - 2
docs/releases/2.0.txt

@@ -65,8 +65,8 @@ Minor features
 * Added MySQL support for the
   :class:`~django.contrib.gis.db.models.functions.AsGeoJSON` function,
   :class:`~django.contrib.gis.db.models.functions.GeoHash` function,
-  :class:`~django.contrib.gis.db.models.functions.IsValid` function, and
-  :lookup:`isvalid` lookup.
+  :class:`~django.contrib.gis.db.models.functions.IsValid` function,
+  :lookup:`isvalid` lookup, and :ref:`distance lookups <distance-lookups>`.
 
 * Added the :class:`~django.contrib.gis.db.models.functions.Azimuth` and
   :class:`~django.contrib.gis.db.models.functions.LineLocatePoint` functions,

+ 18 - 8
tests/gis_tests/distapp/tests.py

@@ -1,5 +1,7 @@
+import unittest
+
 from django.contrib.gis.db.models.functions import (
-    Area, Distance, Intersection, Length, Perimeter, Transform,
+    Area, Distance, Length, Perimeter, Transform, Union,
 )
 from django.contrib.gis.geos import GEOSGeometry, LineString, Point
 from django.contrib.gis.measure import D  # alias for Distance
@@ -7,7 +9,7 @@ from django.db import connection
 from django.db.models import F, Q
 from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
 
-from ..utils import no_oracle, oracle, postgis, spatialite
+from ..utils import mysql, no_oracle, oracle, postgis, spatialite
 from .models import (
     AustraliaCity, CensusZipcode, Interstate, SouthTexasCity, SouthTexasCityFt,
     SouthTexasInterstate, SouthTexasZipcode,
@@ -107,8 +109,9 @@ class DistanceTest(TestCase):
         # (thus, Houston and Southside place will be excluded as tested in
         # the `test02_dwithin` above).
         for model in [SouthTexasCity, SouthTexasCityFt]:
-            qs = model.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(
-                point__distance_lte=(self.stx_pnt, D(km=20)),
+            stx_pnt = self.stx_pnt.transform(model._meta.get_field('point').srid, clone=True)
+            qs = model.objects.filter(point__distance_gte=(stx_pnt, D(km=7))).filter(
+                point__distance_lte=(stx_pnt, D(km=20)),
             )
             cities = self.get_names(qs)
             self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
@@ -183,8 +186,9 @@ class DistanceTest(TestCase):
 
     @skipUnlessDBFeature("supports_distances_lookups")
     def test_distance_lookups_with_expression_rhs(self):
+        stx_pnt = self.stx_pnt.transform(SouthTexasCity._meta.get_field('point').srid, clone=True)
         qs = SouthTexasCity.objects.filter(
-            point__distance_lte=(self.stx_pnt, F('radius')),
+            point__distance_lte=(stx_pnt, F('radius')),
         ).order_by('name')
         self.assertEqual(
             self.get_names(qs),
@@ -193,7 +197,7 @@ class DistanceTest(TestCase):
 
         # With a combined expression
         qs = SouthTexasCity.objects.filter(
-            point__distance_lte=(self.stx_pnt, F('radius') * 2),
+            point__distance_lte=(stx_pnt, F('radius') * 2),
         ).order_by('name')
         self.assertEqual(len(qs), 5)
         self.assertIn('Pearland', self.get_names(qs))
@@ -207,12 +211,18 @@ class DistanceTest(TestCase):
             self.assertEqual(self.get_names(qs), ['Canberra', 'Hobart', 'Melbourne'])
 
         # With a complex geometry expression
-        self.assertFalse(SouthTexasCity.objects.filter(point__distance_gt=(Intersection('point', 'point'), 0)))
+        self.assertFalse(SouthTexasCity.objects.filter(point__distance_gt=(Union('point', 'point'), 0)))
         self.assertEqual(
-            SouthTexasCity.objects.filter(point__distance_lte=(Intersection('point', 'point'), 0)).count(),
+            SouthTexasCity.objects.filter(point__distance_lte=(Union('point', 'point'), 0)).count(),
             SouthTexasCity.objects.count(),
         )
 
+    @unittest.skipUnless(mysql, 'This is a MySQL-specific test')
+    def test_mysql_geodetic_distance_error(self):
+        msg = 'Only numeric values of degree units are allowed on geodetic distance queries.'
+        with self.assertRaisesMessage(ValueError, msg):
+            AustraliaCity.objects.filter(point__distance_lte=(Point(0, 0), D(m=100))).exists()
+
 
 '''
 =============================