Skip to content

Commit 5e66c91

Browse files
committed
edits
1 parent c5bc46b commit 5e66c91

File tree

5 files changed

+181
-262
lines changed

5 files changed

+181
-262
lines changed

django_mongodb_backend/fields/embedded_model_array.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,17 @@ def as_mql(self, compiler, connection):
8686

8787
class _EmbeddedModelArrayOutputField(ArrayField):
8888
"""
89-
Represents the output of an EmbeddedModelArrayField when traversed in a query path.
89+
Represent the output of an EmbeddedModelArrayField when traversed in a
90+
query path.
9091
91-
This field is not meant to be used directly in model definitions. It exists solely to
92-
support query output resolution; when an EmbeddedModelArrayField is accessed in a query,
93-
the result should behave like an array of the embedded model's target type.
92+
This field is not meant to be used in model definitions. It exists solely
93+
to support query output resolution. When an EmbeddedModelArrayField is
94+
accessed in a query, the result should behave like an array of the embedded
95+
model's target type.
9496
95-
While it mimics ArrayField's lookups behavior, the way those lookups are resolved
96-
follows the semantics of EmbeddedModelArrayField rather than native array behavior.
97+
While it mimics ArrayField's lookup behavior, the way those lookups are
98+
resolved follows the semantics of EmbeddedModelArrayField rather than
99+
ArrayField.
97100
"""
98101

99102
ALLOWED_LOOKUPS = {
@@ -115,19 +118,20 @@ def process_rhs(self, compiler, connection):
115118
value = self.rhs
116119
if not self.get_db_prep_lookup_value_is_iterable:
117120
value = [value]
118-
# Value must be serialized based on the query target.
119-
# If querying a subfield inside the array (i.e., a nested KeyTransform), use the output
120-
# field of the subfield. Otherwise, use the base field of the array itself.
121+
# Value must be serialized based on the query target. If querying a
122+
# subfield inside the array (i.e., a nested KeyTransform), use the
123+
# output field of the subfield. Otherwise, use the base field of the
124+
# array itself.
121125
get_db_prep_value = self.lhs._lhs.output_field.get_db_prep_value
122126
return None, [
123127
v if hasattr(v, "as_mql") else get_db_prep_value(v, connection, prepared=True)
124128
for v in value
125129
]
126130

127131
def as_mql(self, compiler, connection):
128-
# Querying a subfield within the array elements (via nested KeyTransform).
129-
# Replicates MongoDB's implicit ANY-match by mapping over the array and applying
130-
# `$in` on the subfield.
132+
# Querying a subfield within the array elements (via nested
133+
# KeyTransform). Replicate MongoDB's implicit ANY-match by mapping over
134+
# the array and applying $in on the subfield.
131135
lhs_mql = process_lhs(self, compiler, connection)
132136
inner_lhs_mql = lhs_mql["$ifNull"][0]["$map"]["in"]
133137
values = process_rhs(self, compiler, connection)
@@ -183,7 +187,7 @@ def __init__(self, key_name, array_field, *args, **kwargs):
183187
self.key_name = key_name
184188
# The iteration items begins from the base_field, a virtual column with
185189
# base field output type is created.
186-
column_target = array_field.embedded_model._meta.get_field(key_name).clone()
190+
column_target = array_field.base_field.embedded_model._meta.get_field(key_name).clone()
187191
column_name = f"$item.{key_name}"
188192
column_target.db_column = column_name
189193
column_target.set_attributes_from_name(column_name)
@@ -199,17 +203,18 @@ def get_lookup(self, name):
199203

200204
def get_transform(self, name):
201205
"""
202-
Validate that `name` is either a field of an embedded model or a
203-
lookup on an embedded model's field.
206+
Validate that `name` is either a field of an embedded model or am
207+
allowed lookup on an embedded model's field.
204208
"""
205-
# Once the sub lhs is a transform, all the filter are applied over it.
206-
# Otherwise get transform from EMF.
209+
# Once the sub-lhs is a transform, all the filters are applied over it.
210+
# Otherwise get the transform from EMF.
207211
if transform := self._lhs.get_transform(name):
208212
if isinstance(transform, KeyTransformFactory):
209213
raise ValueError("Cannot perform multiple levels of array traversal in a query.")
210214
self._sub_transform = transform
211215
return self
212216
output_field = self._lhs.output_field
217+
# The lookup must be allowed AND a valid lookup for the field.
213218
allowed_lookups = self.output_field.ALLOWED_LOOKUPS.intersection(
214219
set(output_field.get_lookups())
215220
)

docs/source/ref/models/fields.rst

Lines changed: 5 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ We will use the following example model::
9191
def __str__(self):
9292
return self.name
9393

94-
.. fieldlookup:: arrayfield.contains
94+
.. fieldlookup:: mongo-arrayfield.contains
9595

9696
``contains``
9797
^^^^^^^^^^^^
@@ -134,7 +134,7 @@ passed. It uses the ``$setIntersection`` operator. For example:
134134
>>> Post.objects.filter(tags__contained_by=["thoughts", "django", "tutorial"])
135135
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>
136136
137-
.. fieldlookup:: arrayfield.overlap
137+
.. fieldlookup:: mongo-arrayfield.overlap
138138

139139
``overlap``
140140
~~~~~~~~~~~
@@ -154,7 +154,7 @@ uses the ``$setIntersection`` operator. For example:
154154
>>> Post.objects.filter(tags__overlap=["thoughts", "tutorial"])
155155
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>
156156
157-
.. fieldlookup:: arrayfield.len
157+
.. fieldlookup:: mongo-arrayfield.len
158158

159159
``len``
160160
^^^^^^^
@@ -170,7 +170,7 @@ available for :class:`~django.db.models.IntegerField`. For example:
170170
>>> Post.objects.filter(tags__len=1)
171171
<QuerySet [<Post: Second post>]>
172172
173-
.. fieldlookup:: arrayfield.index
173+
.. fieldlookup:: mongo-arrayfield.index
174174

175175
Index transforms
176176
^^^^^^^^^^^^^^^^
@@ -196,7 +196,7 @@ array. The lookups available after the transform are those from the
196196
197197
These indexes use 0-based indexing.
198198

199-
.. fieldlookup:: arrayfield.slice
199+
.. fieldlookup:: mongo-arrayfield.slice
200200

201201
Slice transforms
202202
^^^^^^^^^^^^^^^^
@@ -299,155 +299,6 @@ These indexes use 0-based indexing.
299299
As described above for :class:`EmbeddedModelField`,
300300
:djadmin:`makemigrations` does not yet detect changes to embedded models.
301301

302-
Querying ``EmbeddedModelArrayField``
303-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
304-
305-
There are a number of custom lookups and a transform for
306-
:class:`EmbeddedModelArrayField`, similar to those available
307-
for :class:`ArrayField`.
308-
We will use the following example model::
309-
310-
from django.db import models
311-
from django_mongodb_backend.fields import EmbeddedModelArrayField
312-
313-
314-
class Tag(EmbeddedModel):
315-
label = models.CharField(max_length=100)
316-
317-
class Post(models.Model):
318-
name = models.CharField(max_length=200)
319-
tags = EmbeddedModelArrayField(Tag)
320-
321-
def __str__(self):
322-
return self.name
323-
324-
Embedded field lookup
325-
^^^^^^^^^^^^^^^^^^^^^
326-
327-
Embedded field lookup for :class:`EmbeddedModelArrayField` allow querying
328-
fields of the embedded model. This is done by composing the two involved paths:
329-
the path to the ``EmbeddedModelArrayField`` and the path within the nested
330-
embedded model.
331-
This composition enables generating the appropriate query for the lookups.
332-
333-
.. fieldlookup:: embeddedmodelarrayfield.in
334-
335-
``in``
336-
^^^^^^
337-
338-
Returns objects where any of the embedded documents in the field match any of
339-
the values passed. For example:
340-
341-
.. code-block:: pycon
342-
343-
>>> Post.objects.create(
344-
... name="First post", tags=[Tag(label="thoughts"), Tag(label="django")]
345-
... )
346-
>>> Post.objects.create(name="Second post", tags=[Tag(label="thoughts")])
347-
>>> Post.objects.create(
348-
... name="Third post", tags=[Tag(label="tutorial"), Tag(label="django")]
349-
... )
350-
351-
>>> Post.objects.filter(tags__label__in=["thoughts"])
352-
<QuerySet [<Post: First post>, <Post: Second post>]>
353-
354-
>>> Post.objects.filter(tags__label__in=["tutorial", "thoughts"])
355-
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>
356-
357-
.. fieldlookup:: embeddedmodelarrayfield.len
358-
359-
``len``
360-
^^^^^^^
361-
362-
Returns the length of the embedded model array. The lookups available afterward
363-
are those available for :class:`~django.db.models.IntegerField`. For example:
364-
365-
.. code-block:: pycon
366-
367-
>>> Post.objects.create(
368-
... name="First post", tags=[Tag(label="thoughts"), Tag(label="django")]
369-
... )
370-
>>> Post.objects.create(name="Second post", tags=[Tag(label="thoughts")])
371-
372-
>>> Post.objects.filter(tags__len=1)
373-
<QuerySet [<Post: Second post>]>
374-
375-
.. fieldlookup:: embeddedmodelarrayfield.exact
376-
377-
``exact``
378-
^^^^^^^^^
379-
380-
Returns objects where **any** embedded model in the array exactly matches the
381-
given value. This acts like an existence filter on matching embedded documents.
382-
383-
.. code-block:: pycon
384-
385-
>>> Post.objects.create(
386-
... name="First post", tags=[Tag(label="thoughts"), Tag(label="django")]
387-
... )
388-
>>> Post.objects.create(name="Second post", tags=[Tag(label="tutorial")])
389-
390-
>>> Post.objects.filter(tags__label__exact="tutorial")
391-
<QuerySet [<Post: Second post>]>
392-
393-
.. fieldlookup:: embeddedmodelarrayfield.iexact
394-
395-
``iexact``
396-
^^^^^^^^^^
397-
398-
Returns objects where **any** embedded model in the array has a field that
399-
matches the given value **case-insensitively**. This works like ``exact`` but
400-
ignores letter casing.
401-
402-
.. code-block:: pycon
403-
404-
405-
>>> Post.objects.create(
406-
... name="First post", tags=[Tag(label="Thoughts"), Tag(label="Django")]
407-
... )
408-
>>> Post.objects.create(name="Second post", tags=[Tag(label="tutorial")])
409-
410-
>>> Post.objects.filter(tags__label__iexact="django")
411-
<QuerySet [<Post: First post>]>
412-
413-
>>> Post.objects.filter(tags__label__iexact="TUTORIAL")
414-
<QuerySet [<Post: Second post>]>
415-
416-
.. fieldlookup:: embeddedmodelarrayfield.gt
417-
.. fieldlookup:: embeddedmodelarrayfield.gte
418-
.. fieldlookup:: embeddedmodelarrayfield.lt
419-
.. fieldlookup:: embeddedmodelarrayfield.lte
420-
421-
``Greater Than, Greater Than or Equal, Less Than, Less Than or Equal``
422-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
423-
424-
These lookups return objects where **any** embedded document contains a value
425-
that satisfies the corresponding comparison. These are typically used on
426-
numeric or comparable fields within the embedded model.
427-
428-
Examples:
429-
430-
.. code-block:: pycon
431-
432-
Post.objects.create(
433-
name="First post", tags=[Tag(label="django", rating=5), Tag(label="rest", rating=3)]
434-
)
435-
Post.objects.create(
436-
name="Second post", tags=[Tag(label="python", rating=2)]
437-
)
438-
439-
Post.objects.filter(tags__rating__gt=3)
440-
<QuerySet [<Post: First post>]>
441-
442-
Post.objects.filter(tags__rating__gte=3)
443-
<QuerySet [<Post: First post>, <Post: Second post>]>
444-
445-
Post.objects.filter(tags__rating__lt=3)
446-
<QuerySet []>
447-
448-
Post.objects.filter(tags__rating__lte=3)
449-
<QuerySet [<Post: First post>, <Post: Second post>]>
450-
451302
``ObjectIdAutoField``
452303
---------------------
453304

docs/source/topics/embedded-models.rst

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,69 @@ Represented in BSON, the post's structure looks like this:
115115
name: 'Hello world!',
116116
tags: [ { name: 'welcome' }, { name: 'test' } ]
117117
}
118+
119+
Querying ``EmbeddedModelArrayField``
120+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
121+
122+
You can query into an embedded model array using the same double underscore
123+
syntax as relational fields. For example, to find posts that have a label with
124+
name "test"::
125+
126+
>>> Post.objects.filter(tags__name="test")
127+
128+
There are a limited set of lookups you can chain after an embedded field:
129+
130+
* :lookup:`exact`, :lookup:`iexact`
131+
* :lookup:`in`
132+
* :lookup:`gt`, :lookup:`gte`, :lookup:`lt`, :lookup:`lte`
133+
134+
For example, to find posts that have tags with name "test", "TEST", "tEsT",
135+
etc::
136+
137+
>>> Post.objects.filter(tags__name__iexact="test")
138+
139+
.. fieldlookup:: embeddedmodelarrayfield.len
140+
141+
``len`` transform
142+
^^^^^^^^^^^^^^^^^
143+
144+
You can also use the ``len`` transform to filter on the length of the array.
145+
The lookups available afterward are those available for
146+
:class:`~django.db.models.IntegerField`. For example, to match posts with one
147+
tag::
148+
149+
>>> Post.objects.filter(tags__len=1)
150+
151+
or at least one tag::
152+
153+
>>> Post.objects.filter(tags__len__gte=1)
154+
155+
Index and slice transforms
156+
^^^^^^^^^^^^^^^^^^^^^^^^^^
157+
158+
Like :class:`~django_mongodb_backend.fields.ArrayField`, you can use
159+
:lookup:`index <mongo-arrayfield.index>` and :lookup:`slice
160+
<mongo-arrayfield.slice>` transforms to filter on particular items in an array.
161+
162+
For example, to find posts where the first tag is named "test"::
163+
164+
>>> Post.objects.filter(tags__0__name="test")
165+
166+
Or to find posts where the one of the first two tags is named "test"::
167+
168+
>>> Post.objects.filter(tags__0_1__name="test")
169+
170+
These indexes use 0-based indexing.
171+
172+
Nested ``EmbeddedModelArrayField``\s
173+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
174+
175+
If your models use nested ``EmbeddedModelArrayField``\s, you can't use double
176+
underscores to query into the the second level.
177+
178+
For example, if the ``Tag`` model had an ``EmbeddedModelArrayField`` called
179+
``colors``:
180+
181+
>>> Post.objects.filter(tags__colors__name="blue")
182+
...
183+
ValueError: Cannot perform multiple levels of array traversal in a query.

0 commit comments

Comments
 (0)