Fixed #28518 -- Improved performance of loading geometries from DB.
This commit is contained in:
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:
|
||||||
|
|
Loading…
Reference in New Issue