mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #28518 -- Improved performance of loading geometries from DB.
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							481ba33cd2
						
					
				
				
					commit
					1a85b07bdd
				
			| @@ -1,4 +1,6 @@ | |||||||
|  | from django.contrib.gis.db.models import GeometryField | ||||||
| from django.contrib.gis.db.models.functions import Distance | from django.contrib.gis.db.models.functions import Distance | ||||||
|  | from django.utils.functional import cached_property | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseSpatialOperations: | class BaseSpatialOperations: | ||||||
| @@ -13,6 +15,10 @@ class BaseSpatialOperations: | |||||||
|     # How the geometry column should be selected. |     # How the geometry column should be selected. | ||||||
|     select = None |     select = None | ||||||
|  |  | ||||||
|  |     @cached_property | ||||||
|  |     def select_extent(self): | ||||||
|  |         return self.select | ||||||
|  |  | ||||||
|     # Does the spatial database have a geometry or geography type? |     # Does the spatial database have a geometry or geography type? | ||||||
|     geography = False |     geography = False | ||||||
|     geometry = False |     geometry = False | ||||||
| @@ -117,3 +123,15 @@ class BaseSpatialOperations: | |||||||
|         raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_ref_sys() method') |         raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_ref_sys() method') | ||||||
|  |  | ||||||
|     distance_expr_for_lookup = staticmethod(Distance) |     distance_expr_for_lookup = staticmethod(Distance) | ||||||
|  |  | ||||||
|  |     def get_db_converters(self, expression): | ||||||
|  |         converters = super().get_db_converters(expression) | ||||||
|  |         if isinstance(expression.output_field, GeometryField): | ||||||
|  |             converters.append(self.get_geometry_converter(expression)) | ||||||
|  |         return converters | ||||||
|  |  | ||||||
|  |     def get_geometry_converter(self, expression): | ||||||
|  |         raise NotImplementedError( | ||||||
|  |             'Subclasses of BaseSpatialOperations must provide a ' | ||||||
|  |             'get_geometry_converter() method.' | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures | |||||||
| from django.db.backends.mysql.features import ( | from django.db.backends.mysql.features import ( | ||||||
|     DatabaseFeatures as MySQLDatabaseFeatures, |     DatabaseFeatures as MySQLDatabaseFeatures, | ||||||
| ) | ) | ||||||
|  | from django.utils.functional import cached_property | ||||||
|  |  | ||||||
|  |  | ||||||
| class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): | class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): | ||||||
| @@ -14,3 +15,7 @@ class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): | |||||||
|     supports_real_shape_operations = False |     supports_real_shape_operations = False | ||||||
|     supports_null_geometries = False |     supports_null_geometries = False | ||||||
|     supports_num_points_poly = False |     supports_num_points_poly = False | ||||||
|  |  | ||||||
|  |     @cached_property | ||||||
|  |     def supports_empty_geometry_collection(self): | ||||||
|  |         return self.connection.mysql_version >= (5, 7, 5) | ||||||
|   | |||||||
| @@ -3,7 +3,9 @@ from django.contrib.gis.db.backends.base.operations import ( | |||||||
|     BaseSpatialOperations, |     BaseSpatialOperations, | ||||||
| ) | ) | ||||||
| from django.contrib.gis.db.backends.utils import SpatialOperator | from django.contrib.gis.db.backends.utils import SpatialOperator | ||||||
| from django.contrib.gis.db.models import GeometryField, aggregates | from django.contrib.gis.db.models import aggregates | ||||||
|  | from django.contrib.gis.geos import GEOSGeometry | ||||||
|  | from django.contrib.gis.geos.prototypes.io import wkb_r | ||||||
| from django.contrib.gis.measure import Distance | from django.contrib.gis.measure import Distance | ||||||
| from django.db.backends.mysql.operations import DatabaseOperations | from django.db.backends.mysql.operations import DatabaseOperations | ||||||
| from django.utils.functional import cached_property | from django.utils.functional import cached_property | ||||||
| @@ -28,13 +30,9 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|     def is_mysql_5_6(self): |     def is_mysql_5_6(self): | ||||||
|         return self.connection.mysql_version < (5, 7, 6) |         return self.connection.mysql_version < (5, 7, 6) | ||||||
|  |  | ||||||
|     @cached_property |  | ||||||
|     def uses_invalid_empty_geometry_collection(self): |  | ||||||
|         return self.connection.mysql_version >= (5, 7, 5) |  | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|     def select(self): |     def select(self): | ||||||
|         return self.geom_func_prefix + 'AsText(%s)' |         return self.geom_func_prefix + 'AsBinary(%s)' | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|     def from_text(self): |     def from_text(self): | ||||||
| @@ -97,15 +95,12 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|             dist_param = value |             dist_param = value | ||||||
|         return [dist_param] |         return [dist_param] | ||||||
|  |  | ||||||
|     def get_db_converters(self, expression): |     def get_geometry_converter(self, expression): | ||||||
|         converters = super().get_db_converters(expression) |         read = wkb_r().read | ||||||
|         if isinstance(expression.output_field, GeometryField) and self.uses_invalid_empty_geometry_collection: |         srid = expression.output_field.srid | ||||||
|             converters.append(self.convert_invalid_empty_geometry_collection) |         if srid == -1: | ||||||
|         return converters |             srid = None | ||||||
|  |  | ||||||
|     # https://dev.mysql.com/doc/refman/en/spatial-function-argument-handling.html |         def converter(value, expression, connection): | ||||||
|     # MySQL 5.7.5 adds support for the empty geometry collections, but they are represented with invalid WKT. |             return None if value is None else GEOSGeometry(read(memoryview(value)), srid) | ||||||
|     def convert_invalid_empty_geometry_collection(self, value, expression, connection): |         return converter | ||||||
|         if value == b'GEOMETRYCOLLECTION()': |  | ||||||
|             return b'GEOMETRYCOLLECTION EMPTY' |  | ||||||
|         return value |  | ||||||
|   | |||||||
| @@ -16,6 +16,8 @@ from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter | |||||||
| from django.contrib.gis.db.backends.utils import SpatialOperator | from django.contrib.gis.db.backends.utils import SpatialOperator | ||||||
| from django.contrib.gis.db.models import aggregates | from django.contrib.gis.db.models import aggregates | ||||||
| from django.contrib.gis.geometry.backend import Geometry | from django.contrib.gis.geometry.backend import Geometry | ||||||
|  | from django.contrib.gis.geos import GEOSGeometry | ||||||
|  | from django.contrib.gis.geos.prototypes.io import wkb_r | ||||||
| from django.contrib.gis.measure import Distance | from django.contrib.gis.measure import Distance | ||||||
| from django.db.backends.oracle.operations import DatabaseOperations | from django.db.backends.oracle.operations import DatabaseOperations | ||||||
|  |  | ||||||
| @@ -85,7 +87,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|     # However, this adversely affects performance (i.e., Java is called |     # However, this adversely affects performance (i.e., Java is called | ||||||
|     # to convert to WKT on every query).  If someone wishes to write a |     # to convert to WKT on every query).  If someone wishes to write a | ||||||
|     # SDO_GEOMETRY(...) parser in Python, let me know =) |     # SDO_GEOMETRY(...) parser in Python, let me know =) | ||||||
|     select = 'SDO_UTIL.TO_WKTGEOMETRY(%s)' |     select = 'SDO_UTIL.TO_WKBGEOMETRY(%s)' | ||||||
|  |  | ||||||
|     gis_operators = { |     gis_operators = { | ||||||
|         'contains': SDOOperator(func='SDO_CONTAINS'), |         'contains': SDOOperator(func='SDO_CONTAINS'), | ||||||
| @@ -112,24 +114,12 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|     def geo_quote_name(self, name): |     def geo_quote_name(self, name): | ||||||
|         return super().geo_quote_name(name).upper() |         return super().geo_quote_name(name).upper() | ||||||
|  |  | ||||||
|     def get_db_converters(self, expression): |  | ||||||
|         converters = super().get_db_converters(expression) |  | ||||||
|         internal_type = expression.output_field.get_internal_type() |  | ||||||
|         geometry_fields = ( |  | ||||||
|             'PointField', 'GeometryField', 'LineStringField', |  | ||||||
|             'PolygonField', 'MultiPointField', 'MultiLineStringField', |  | ||||||
|             'MultiPolygonField', 'GeometryCollectionField', |  | ||||||
|         ) |  | ||||||
|         if internal_type in geometry_fields: |  | ||||||
|             converters.append(self.convert_textfield_value) |  | ||||||
|         return converters |  | ||||||
|  |  | ||||||
|     def convert_extent(self, clob): |     def convert_extent(self, clob): | ||||||
|         if clob: |         if clob: | ||||||
|             # Generally, Oracle returns a polygon for the extent -- however, |             # Generally, Oracle returns a polygon for the extent -- however, | ||||||
|             # it can return a single point if there's only one Point in the |             # it can return a single point if there's only one Point in the | ||||||
|             # table. |             # table. | ||||||
|             ext_geom = Geometry(clob.read()) |             ext_geom = Geometry(memoryview(clob.read())) | ||||||
|             gtype = str(ext_geom.geom_type) |             gtype = str(ext_geom.geom_type) | ||||||
|             if gtype == 'Polygon': |             if gtype == 'Polygon': | ||||||
|                 # Construct the 4-tuple from the coordinates in the polygon. |                 # Construct the 4-tuple from the coordinates in the polygon. | ||||||
| @@ -207,3 +197,13 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|         if placeholder == 'NULL': |         if placeholder == 'NULL': | ||||||
|             return [] |             return [] | ||||||
|         return super().modify_insert_params(placeholder, params) |         return super().modify_insert_params(placeholder, params) | ||||||
|  |  | ||||||
|  |     def get_geometry_converter(self, expression): | ||||||
|  |         read = wkb_r().read | ||||||
|  |         srid = expression.output_field.srid | ||||||
|  |         if srid == -1: | ||||||
|  |             srid = None | ||||||
|  |  | ||||||
|  |         def converter(value, expression, connection): | ||||||
|  |             return None if value is None else GEOSGeometry(read(memoryview(value.read())), srid) | ||||||
|  |         return converter | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ from django.contrib.gis.db.backends.base.operations import ( | |||||||
| from django.contrib.gis.db.backends.utils import SpatialOperator | from django.contrib.gis.db.backends.utils import SpatialOperator | ||||||
| from django.contrib.gis.db.models import GeometryField, RasterField | from django.contrib.gis.db.models import GeometryField, RasterField | ||||||
| from django.contrib.gis.gdal import GDALRaster | from django.contrib.gis.gdal import GDALRaster | ||||||
|  | from django.contrib.gis.geos import GEOSGeometry | ||||||
|  | from django.contrib.gis.geos.prototypes.io import wkb_r | ||||||
| from django.contrib.gis.measure import Distance | from django.contrib.gis.measure import Distance | ||||||
| from django.core.exceptions import ImproperlyConfigured | from django.core.exceptions import ImproperlyConfigured | ||||||
| from django.db.backends.postgresql.operations import DatabaseOperations | from django.db.backends.postgresql.operations import DatabaseOperations | ||||||
| @@ -132,6 +134,12 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|  |  | ||||||
|     unsupported_functions = set() |     unsupported_functions = set() | ||||||
|  |  | ||||||
|  |     @cached_property | ||||||
|  |     def select(self): | ||||||
|  |         return '%s::bytea' | ||||||
|  |  | ||||||
|  |     select_extent = None | ||||||
|  |  | ||||||
|     def __init__(self, connection): |     def __init__(self, connection): | ||||||
|         super().__init__(connection) |         super().__init__(connection) | ||||||
|  |  | ||||||
| @@ -381,3 +389,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|             isinstance(arg, GDALRaster) |             isinstance(arg, GDALRaster) | ||||||
|         ) |         ) | ||||||
|         return ST_Polygon(arg) if is_raster else arg |         return ST_Polygon(arg) if is_raster else arg | ||||||
|  |  | ||||||
|  |     def get_geometry_converter(self, expression): | ||||||
|  |         read = wkb_r().read | ||||||
|  |         return lambda value, expression, connection: None if value is None else GEOSGeometry(read(value)) | ||||||
|   | |||||||
| @@ -10,6 +10,8 @@ from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter | |||||||
| from django.contrib.gis.db.backends.utils import SpatialOperator | from django.contrib.gis.db.backends.utils import SpatialOperator | ||||||
| from django.contrib.gis.db.models import aggregates | from django.contrib.gis.db.models import aggregates | ||||||
| from django.contrib.gis.geometry.backend import Geometry | from django.contrib.gis.geometry.backend import Geometry | ||||||
|  | from django.contrib.gis.geos import GEOSGeometry | ||||||
|  | from django.contrib.gis.geos.prototypes.io import wkb_r, wkt_r | ||||||
| from django.contrib.gis.measure import Distance | from django.contrib.gis.measure import Distance | ||||||
| from django.core.exceptions import ImproperlyConfigured | from django.core.exceptions import ImproperlyConfigured | ||||||
| from django.db.backends.sqlite3.operations import DatabaseOperations | from django.db.backends.sqlite3.operations import DatabaseOperations | ||||||
| @@ -35,7 +37,6 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|     unionagg = 'GUnion' |     unionagg = 'GUnion' | ||||||
|  |  | ||||||
|     from_text = 'GeomFromText' |     from_text = 'GeomFromText' | ||||||
|     select = 'AsText(%s)' |  | ||||||
|  |  | ||||||
|     gis_operators = { |     gis_operators = { | ||||||
|         # Binary predicates |         # Binary predicates | ||||||
| @@ -63,6 +64,10 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|  |  | ||||||
|     disallowed_aggregates = (aggregates.Extent3D,) |     disallowed_aggregates = (aggregates.Extent3D,) | ||||||
|  |  | ||||||
|  |     @cached_property | ||||||
|  |     def select(self): | ||||||
|  |         return 'CAST (AsEWKB(%s) AS BLOB)' if self.spatial_version >= (4, 3, 0) else 'AsText(%s)' | ||||||
|  |  | ||||||
|     @cached_property |     @cached_property | ||||||
|     def function_names(self): |     def function_names(self): | ||||||
|         return { |         return { | ||||||
| @@ -192,3 +197,14 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): | |||||||
|     def spatial_ref_sys(self): |     def spatial_ref_sys(self): | ||||||
|         from django.contrib.gis.db.backends.spatialite.models import SpatialiteSpatialRefSys |         from django.contrib.gis.db.backends.spatialite.models import SpatialiteSpatialRefSys | ||||||
|         return SpatialiteSpatialRefSys |         return SpatialiteSpatialRefSys | ||||||
|  |  | ||||||
|  |     def get_geometry_converter(self, expression): | ||||||
|  |         if self.spatial_version >= (4, 3, 0): | ||||||
|  |             read = wkb_r().read | ||||||
|  |             return lambda value, expression, connection: None if value is None else GEOSGeometry(read(value)) | ||||||
|  |         else: | ||||||
|  |             read = wkt_r().read | ||||||
|  |             srid = expression.output_field.srid | ||||||
|  |             if srid == -1: | ||||||
|  |                 srid = None | ||||||
|  |             return lambda value, expression, connection: None if value is None else GEOSGeometry(read(value), srid) | ||||||
|   | |||||||
| @@ -51,23 +51,6 @@ def get_srid_info(srid, connection): | |||||||
|     return _srid_cache[alias][srid] |     return _srid_cache[alias][srid] | ||||||
|  |  | ||||||
|  |  | ||||||
| class GeoSelectFormatMixin: |  | ||||||
|     def select_format(self, compiler, sql, params): |  | ||||||
|         """ |  | ||||||
|         Return the selection format string, depending on the requirements |  | ||||||
|         of the spatial backend.  For example, Oracle and MySQL require custom |  | ||||||
|         selection formats in order to retrieve geometries in OGC WKT. For all |  | ||||||
|         other fields, return a simple '%s' format string. |  | ||||||
|         """ |  | ||||||
|         connection = compiler.connection |  | ||||||
|         if connection.ops.select: |  | ||||||
|             # This allows operations to be done on fields in the SELECT, |  | ||||||
|             # overriding their values -- used by the Oracle and MySQL |  | ||||||
|             # spatial backends to get database values as WKT. |  | ||||||
|             sql = connection.ops.select % sql |  | ||||||
|         return sql, params |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class BaseSpatialField(Field): | class BaseSpatialField(Field): | ||||||
|     """ |     """ | ||||||
|     The Base GIS Field. |     The Base GIS Field. | ||||||
| @@ -205,7 +188,7 @@ class BaseSpatialField(Field): | |||||||
|         return obj |         return obj | ||||||
|  |  | ||||||
|  |  | ||||||
| class GeometryField(GeoSelectFormatMixin, BaseSpatialField): | class GeometryField(BaseSpatialField): | ||||||
|     """ |     """ | ||||||
|     The base Geometry field -- maps to the OpenGIS Specification Geometry type. |     The base Geometry field -- maps to the OpenGIS Specification Geometry type. | ||||||
|     """ |     """ | ||||||
| @@ -255,14 +238,6 @@ class GeometryField(GeoSelectFormatMixin, BaseSpatialField): | |||||||
|             kwargs['geography'] = self.geography |             kwargs['geography'] = self.geography | ||||||
|         return name, path, args, kwargs |         return name, path, args, kwargs | ||||||
|  |  | ||||||
|     def from_db_value(self, value, expression, connection): |  | ||||||
|         if value: |  | ||||||
|             value = Geometry(value) |  | ||||||
|             srid = value.srid |  | ||||||
|             if not srid and self.srid != -1: |  | ||||||
|                 value.srid = self.srid |  | ||||||
|         return value |  | ||||||
|  |  | ||||||
|     def contribute_to_class(self, cls, name, **kwargs): |     def contribute_to_class(self, cls, name, **kwargs): | ||||||
|         super().contribute_to_class(cls, name, **kwargs) |         super().contribute_to_class(cls, name, **kwargs) | ||||||
|  |  | ||||||
| @@ -280,6 +255,15 @@ class GeometryField(GeoSelectFormatMixin, BaseSpatialField): | |||||||
|             defaults['widget'] = forms.Textarea |             defaults['widget'] = forms.Textarea | ||||||
|         return super().formfield(**defaults) |         return super().formfield(**defaults) | ||||||
|  |  | ||||||
|  |     def select_format(self, compiler, sql, params): | ||||||
|  |         """ | ||||||
|  |         Return the selection format string, depending on the requirements | ||||||
|  |         of the spatial backend. For example, Oracle and MySQL require custom | ||||||
|  |         selection formats in order to retrieve geometries in OGC WKB. | ||||||
|  |         """ | ||||||
|  |         select = compiler.connection.ops.select | ||||||
|  |         return select % sql if select else sql, params | ||||||
|  |  | ||||||
|  |  | ||||||
| # The OpenGIS Geometry Type Fields | # The OpenGIS Geometry Type Fields | ||||||
| class PointField(GeometryField): | class PointField(GeometryField): | ||||||
| @@ -324,7 +308,7 @@ class GeometryCollectionField(GeometryField): | |||||||
|     description = _("Geometry collection") |     description = _("Geometry collection") | ||||||
|  |  | ||||||
|  |  | ||||||
| class ExtentField(GeoSelectFormatMixin, Field): | class ExtentField(Field): | ||||||
|     "Used as a return value from an extent aggregate" |     "Used as a return value from an extent aggregate" | ||||||
|  |  | ||||||
|     description = _("Extent Aggregate Field") |     description = _("Extent Aggregate Field") | ||||||
| @@ -332,6 +316,10 @@ class ExtentField(GeoSelectFormatMixin, Field): | |||||||
|     def get_internal_type(self): |     def get_internal_type(self): | ||||||
|         return "ExtentField" |         return "ExtentField" | ||||||
|  |  | ||||||
|  |     def select_format(self, compiler, sql, params): | ||||||
|  |         select = compiler.connection.ops.select_extent | ||||||
|  |         return select % sql if select else sql, params | ||||||
|  |  | ||||||
|  |  | ||||||
| class RasterField(BaseSpatialField): | class RasterField(BaseSpatialField): | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -248,7 +248,7 @@ class GISFunctionsTests(TestCase): | |||||||
|         geom = Point(5, 23, srid=4326) |         geom = Point(5, 23, srid=4326) | ||||||
|         qs = Country.objects.annotate(inter=functions.Intersection('mpoly', geom)) |         qs = Country.objects.annotate(inter=functions.Intersection('mpoly', geom)) | ||||||
|         for c in qs: |         for c in qs: | ||||||
|             if spatialite or (mysql and not connection.ops.uses_invalid_empty_geometry_collection) or oracle: |             if spatialite or (mysql and not connection.features.supports_empty_geometry_collection) or oracle: | ||||||
|                 # When the intersection is empty, some databases return None. |                 # When the intersection is empty, some databases return None. | ||||||
|                 expected = None |                 expected = None | ||||||
|             else: |             else: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user