|
26 | 26 | Int64 = long |
27 | 27 |
|
28 | 28 | from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField, |
29 | | - GeoJsonBaseField, ObjectIdField, get_document) |
| 29 | + GeoJsonBaseField, LazyReference, ObjectIdField, |
| 30 | + get_document) |
30 | 31 | from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db |
31 | 32 | from mongoengine.document import Document, EmbeddedDocument |
32 | 33 | from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError |
|
46 | 47 | 'GenericEmbeddedDocumentField', 'DynamicField', 'ListField', |
47 | 48 | 'SortedListField', 'EmbeddedDocumentListField', 'DictField', |
48 | 49 | 'MapField', 'ReferenceField', 'CachedReferenceField', |
| 50 | + 'LazyReferenceField', 'GenericLazyReferenceField', |
49 | 51 | 'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy', |
50 | 52 | 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', |
51 | 53 | 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', |
@@ -953,6 +955,15 @@ class ReferenceField(BaseField): |
953 | 955 | """A reference to a document that will be automatically dereferenced on |
954 | 956 | access (lazily). |
955 | 957 |
|
| 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 | +
|
956 | 967 | Use the `reverse_delete_rule` to handle what should happen if the document |
957 | 968 | the field is referencing is deleted. EmbeddedDocuments, DictFields and |
958 | 969 | MapFields does not support reverse_delete_rule and an `InvalidDocumentError` |
@@ -1087,8 +1098,8 @@ def prepare_query_value(self, op, value): |
1087 | 1098 |
|
1088 | 1099 | def validate(self, value): |
1089 | 1100 |
|
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') |
1092 | 1103 |
|
1093 | 1104 | if isinstance(value, Document) and value.id is None: |
1094 | 1105 | self.error('You can only reference documents once they have been ' |
@@ -1263,6 +1274,12 @@ class GenericReferenceField(BaseField): |
1263 | 1274 | """A reference to *any* :class:`~mongoengine.document.Document` subclass |
1264 | 1275 | that will be automatically dereferenced on access (lazily). |
1265 | 1276 |
|
| 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 | +
|
1266 | 1283 | .. note :: |
1267 | 1284 | * Any documents used as a generic reference must be registered in the |
1268 | 1285 | document registry. Importing the model will automatically register |
@@ -2141,3 +2158,189 @@ class MultiPolygonField(GeoJsonBaseField): |
2141 | 2158 | .. versionadded:: 0.9 |
2142 | 2159 | """ |
2143 | 2160 | _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