Skip to content

Commit b616889

Browse files
authored
Merge pull request #1690 from touilleMan/lazyreference-field
Add LazyReferenceField & GenericLazyReferencField
2 parents a1494c4 + da33cb5 commit b616889

File tree

5 files changed

+706
-9
lines changed

5 files changed

+706
-9
lines changed

docs/changelog.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
Changelog
33
=========
44

5-
Development
6-
===========
7-
- (Fill this out as you fix issues and develop your features).
5+
Changes in 0.15.0
6+
=================
7+
- Add LazyReferenceField and GenericLazyReferenceField to address #1230
88

99
Changes in 0.14.1
1010
=================

mongoengine/base/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
'UPDATE_OPERATORS', '_document_registry', 'get_document',
1616

1717
# datastructures
18-
'BaseDict', 'BaseList', 'EmbeddedDocumentList',
18+
'BaseDict', 'BaseList', 'EmbeddedDocumentList', 'LazyReference',
1919

2020
# document
2121
'BaseDocument',

mongoengine/base/datastructures.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import itertools
22
import weakref
33

4+
from bson import DBRef
45
import six
56

67
from mongoengine.common import _import_class
78
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
89

9-
__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList')
10+
__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList', 'LazyReference')
1011

1112

1213
class BaseDict(dict):
@@ -445,3 +446,42 @@ def __repr__(self):
445446

446447
cls._classes[allowed_keys] = SpecificStrictDict
447448
return cls._classes[allowed_keys]
449+
450+
451+
class LazyReference(DBRef):
452+
__slots__ = ('_cached_doc', 'passthrough', 'document_type')
453+
454+
def fetch(self, force=False):
455+
if not self._cached_doc or force:
456+
self._cached_doc = self.document_type.objects.get(pk=self.pk)
457+
if not self._cached_doc:
458+
raise DoesNotExist('Trying to dereference unknown document %s' % (self))
459+
return self._cached_doc
460+
461+
@property
462+
def pk(self):
463+
return self.id
464+
465+
def __init__(self, document_type, pk, cached_doc=None, passthrough=False):
466+
self.document_type = document_type
467+
self._cached_doc = cached_doc
468+
self.passthrough = passthrough
469+
super(LazyReference, self).__init__(self.document_type._get_collection_name(), pk)
470+
471+
def __getitem__(self, name):
472+
if not self.passthrough:
473+
raise KeyError()
474+
document = self.fetch()
475+
return document[name]
476+
477+
def __getattr__(self, name):
478+
if not object.__getattribute__(self, 'passthrough'):
479+
raise AttributeError()
480+
document = self.fetch()
481+
try:
482+
return document[name]
483+
except KeyError:
484+
raise AttributeError()
485+
486+
def __repr__(self):
487+
return "<LazyReference(%s, %r)>" % (self.document_type, self.pk)

mongoengine/fields.py

Lines changed: 206 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
Int64 = long
2727

2828
from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField,
29-
GeoJsonBaseField, ObjectIdField, get_document)
29+
GeoJsonBaseField, LazyReference, ObjectIdField,
30+
get_document)
3031
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
3132
from mongoengine.document import Document, EmbeddedDocument
3233
from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError
@@ -46,6 +47,7 @@
4647
'GenericEmbeddedDocumentField', 'DynamicField', 'ListField',
4748
'SortedListField', 'EmbeddedDocumentListField', 'DictField',
4849
'MapField', 'ReferenceField', 'CachedReferenceField',
50+
'LazyReferenceField', 'GenericLazyReferenceField',
4951
'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy',
5052
'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField',
5153
'GeoPointField', 'PointField', 'LineStringField', 'PolygonField',
@@ -953,6 +955,15 @@ class ReferenceField(BaseField):
953955
"""A reference to a document that will be automatically dereferenced on
954956
access (lazily).
955957
958+
Note this means you will get a database I/O access everytime you access
959+
this field. This is necessary because the field returns a :class:`~mongoengine.Document`
960+
which precise type can depend of the value of the `_cls` field present in the
961+
document in database.
962+
In short, using this type of field can lead to poor performances (especially
963+
if you access this field only to retrieve it `pk` field which is already
964+
known before dereference). To solve this you should consider using the
965+
:class:`~mongoengine.fields.LazyReferenceField`.
966+
956967
Use the `reverse_delete_rule` to handle what should happen if the document
957968
the field is referencing is deleted. EmbeddedDocuments, DictFields and
958969
MapFields does not support reverse_delete_rule and an `InvalidDocumentError`
@@ -1087,8 +1098,8 @@ def prepare_query_value(self, op, value):
10871098

10881099
def validate(self, value):
10891100

1090-
if not isinstance(value, (self.document_type, DBRef, ObjectId)):
1091-
self.error('A ReferenceField only accepts DBRef, ObjectId or documents')
1101+
if not isinstance(value, (self.document_type, LazyReference, DBRef, ObjectId)):
1102+
self.error('A ReferenceField only accepts DBRef, LazyReference, ObjectId or documents')
10921103

10931104
if isinstance(value, Document) and value.id is None:
10941105
self.error('You can only reference documents once they have been '
@@ -1263,6 +1274,12 @@ class GenericReferenceField(BaseField):
12631274
"""A reference to *any* :class:`~mongoengine.document.Document` subclass
12641275
that will be automatically dereferenced on access (lazily).
12651276
1277+
Note this field works the same way as :class:`~mongoengine.document.ReferenceField`,
1278+
doing database I/O access the first time it is accessed (even if it's to access
1279+
it ``pk`` or ``id`` field).
1280+
To solve this you should consider using the
1281+
:class:`~mongoengine.fields.GenericLazyReferenceField`.
1282+
12661283
.. note ::
12671284
* Any documents used as a generic reference must be registered in the
12681285
document registry. Importing the model will automatically register
@@ -2141,3 +2158,189 @@ class MultiPolygonField(GeoJsonBaseField):
21412158
.. versionadded:: 0.9
21422159
"""
21432160
_type = 'MultiPolygon'
2161+
2162+
2163+
class LazyReferenceField(BaseField):
2164+
"""A really lazy reference to a document.
2165+
Unlike the :class:`~mongoengine.fields.ReferenceField` it must be manually
2166+
dereferenced using it ``fetch()`` method.
2167+
2168+
.. versionadded:: 0.15
2169+
"""
2170+
2171+
def __init__(self, document_type, passthrough=False, dbref=False,
2172+
reverse_delete_rule=DO_NOTHING, **kwargs):
2173+
"""Initialises the Reference Field.
2174+
2175+
:param dbref: Store the reference as :class:`~pymongo.dbref.DBRef`
2176+
or as the :class:`~pymongo.objectid.ObjectId`.id .
2177+
:param reverse_delete_rule: Determines what to do when the referring
2178+
object is deleted
2179+
:param passthrough: When trying to access unknown fields, the
2180+
:class:`~mongoengine.base.datastructure.LazyReference` instance will
2181+
automatically call `fetch()` and try to retrive the field on the fetched
2182+
document. Note this only work getting field (not setting or deleting).
2183+
"""
2184+
if (
2185+
not isinstance(document_type, six.string_types) and
2186+
not issubclass(document_type, Document)
2187+
):
2188+
self.error('Argument to LazyReferenceField constructor must be a '
2189+
'document class or a string')
2190+
2191+
self.dbref = dbref
2192+
self.passthrough = passthrough
2193+
self.document_type_obj = document_type
2194+
self.reverse_delete_rule = reverse_delete_rule
2195+
super(LazyReferenceField, self).__init__(**kwargs)
2196+
2197+
@property
2198+
def document_type(self):
2199+
if isinstance(self.document_type_obj, six.string_types):
2200+
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2201+
self.document_type_obj = self.owner_document
2202+
else:
2203+
self.document_type_obj = get_document(self.document_type_obj)
2204+
return self.document_type_obj
2205+
2206+
def __get__(self, instance, owner):
2207+
"""Descriptor to allow lazy dereferencing."""
2208+
if instance is None:
2209+
# Document class being used rather than a document object
2210+
return self
2211+
2212+
value = instance._data.get(self.name)
2213+
if isinstance(value, LazyReference):
2214+
if value.passthrough != self.passthrough:
2215+
instance._data[self.name] = LazyReference(
2216+
value.document_type, value.pk, passthrough=self.passthrough)
2217+
elif value is not None:
2218+
if isinstance(value, self.document_type):
2219+
value = LazyReference(self.document_type, value.pk, passthrough=self.passthrough)
2220+
elif isinstance(value, DBRef):
2221+
value = LazyReference(self.document_type, value.id, passthrough=self.passthrough)
2222+
else:
2223+
# value is the primary key of the referenced document
2224+
value = LazyReference(self.document_type, value, passthrough=self.passthrough)
2225+
instance._data[self.name] = value
2226+
2227+
return super(LazyReferenceField, self).__get__(instance, owner)
2228+
2229+
def to_mongo(self, value):
2230+
if isinstance(value, LazyReference):
2231+
pk = value.pk
2232+
elif isinstance(value, self.document_type):
2233+
pk = value.pk
2234+
elif isinstance(value, DBRef):
2235+
pk = value.id
2236+
else:
2237+
# value is the primary key of the referenced document
2238+
pk = value
2239+
id_field_name = self.document_type._meta['id_field']
2240+
id_field = self.document_type._fields[id_field_name]
2241+
pk = id_field.to_mongo(pk)
2242+
if self.dbref:
2243+
return DBRef(self.document_type._get_collection_name(), pk)
2244+
else:
2245+
return pk
2246+
2247+
def validate(self, value):
2248+
if isinstance(value, LazyReference):
2249+
if not issubclass(value.document_type, self.document_type):
2250+
self.error('Reference must be on a `%s` document.' % self.document_type)
2251+
pk = value.pk
2252+
elif isinstance(value, self.document_type):
2253+
pk = value.pk
2254+
elif isinstance(value, DBRef):
2255+
# TODO: check collection ?
2256+
collection = self.document_type._get_collection_name()
2257+
if value.collection != collection:
2258+
self.error("DBRef on bad collection (must be on `%s`)" % collection)
2259+
pk = value.id
2260+
else:
2261+
# value is the primary key of the referenced document
2262+
id_field_name = self.document_type._meta['id_field']
2263+
id_field = getattr(self.document_type, id_field_name)
2264+
pk = value
2265+
try:
2266+
id_field.validate(pk)
2267+
except ValidationError:
2268+
self.error(
2269+
"value should be `{0}` document, LazyReference or DBRef on `{0}` "
2270+
"or `{0}`'s primary key (i.e. `{1}`)".format(
2271+
self.document_type.__name__, type(id_field).__name__))
2272+
2273+
if pk is None:
2274+
self.error('You can only reference documents once they have been '
2275+
'saved to the database')
2276+
2277+
def prepare_query_value(self, op, value):
2278+
if value is None:
2279+
return None
2280+
super(LazyReferenceField, self).prepare_query_value(op, value)
2281+
return self.to_mongo(value)
2282+
2283+
def lookup_member(self, member_name):
2284+
return self.document_type._fields.get(member_name)
2285+
2286+
2287+
class GenericLazyReferenceField(GenericReferenceField):
2288+
"""A reference to *any* :class:`~mongoengine.document.Document` subclass
2289+
that will be automatically dereferenced on access (lazily).
2290+
Unlike the :class:`~mongoengine.fields.GenericReferenceField` it must be
2291+
manually dereferenced using it ``fetch()`` method.
2292+
2293+
.. note ::
2294+
* Any documents used as a generic reference must be registered in the
2295+
document registry. Importing the model will automatically register
2296+
it.
2297+
2298+
* You can use the choices param to limit the acceptable Document types
2299+
2300+
.. versionadded:: 0.15
2301+
"""
2302+
2303+
def __init__(self, *args, **kwargs):
2304+
self.passthrough = kwargs.pop('passthrough', False)
2305+
super(GenericLazyReferenceField, self).__init__(*args, **kwargs)
2306+
2307+
def _validate_choices(self, value):
2308+
if isinstance(value, LazyReference):
2309+
value = value.document_type
2310+
super(GenericLazyReferenceField, self)._validate_choices(value)
2311+
2312+
def __get__(self, instance, owner):
2313+
if instance is None:
2314+
return self
2315+
2316+
value = instance._data.get(self.name)
2317+
if isinstance(value, LazyReference):
2318+
if value.passthrough != self.passthrough:
2319+
instance._data[self.name] = LazyReference(
2320+
value.document_type, value.pk, passthrough=self.passthrough)
2321+
elif value is not None:
2322+
if isinstance(value, (dict, SON)):
2323+
value = LazyReference(get_document(value['_cls']), value['_ref'].id, passthrough=self.passthrough)
2324+
elif isinstance(value, Document):
2325+
value = LazyReference(type(value), value.pk, passthrough=self.passthrough)
2326+
instance._data[self.name] = value
2327+
2328+
return super(GenericLazyReferenceField, self).__get__(instance, owner)
2329+
2330+
def validate(self, value):
2331+
if isinstance(value, LazyReference) and value.pk is None:
2332+
self.error('You can only reference documents once they have been'
2333+
' saved to the database')
2334+
return super(GenericLazyReferenceField, self).validate(value)
2335+
2336+
def to_mongo(self, document):
2337+
if document is None:
2338+
return None
2339+
2340+
if isinstance(document, LazyReference):
2341+
return SON((
2342+
('_cls', document.document_type._class_name),
2343+
('_ref', document)
2344+
))
2345+
else:
2346+
return super(GenericLazyReferenceField, self).to_mongo(document)

0 commit comments

Comments
 (0)