Fixed #28518 -- Improved performance of loading geometries from DB.

This commit is contained in:
Sergey Fedoseev 2017-08-23 11:30:24 +05:00 committed by Tim Graham
parent 481ba33cd2
commit 1a85b07bdd
8 changed files with 94 additions and 60 deletions

View File

@ -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.'
)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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):
""" """

View File

@ -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: