Checking for many-to-many relation OR a property - django

How can I check whether a many-to-many relationship exists or another property is fulfilled? When I try the query, it returns some rows twice!
Given a model
from django.db import models
class Plug(models.Model):
name = models.CharField(primary_key=True, max_length=99)
class Widget(models.Model):
name = models.CharField(primary_key=True, max_length=99)
shiny = models.BooleanField()
compatible = models.ManyToManyField(Plug)
I have the following items in my database:
from django.db.models import Q
schuko = Plug.objects.create(name='F')
uk = Plug.objects.create(name='G')
Widget.objects.create(name='microwave', shiny=True).compatible.set([uk])
Widget.objects.create(name='oven', shiny=False).compatible.set([uk])
Widget.objects.create(name='pc', shiny=True).compatible.set([uk, schuko])
Now I want all names of widgets that are shiny and/or compatible with Schuko:
shiny_or_schuko = sorted(
Widget.objects.filter(Q(shiny=True) | Q(compatible=schuko))
.values_list('name', flat=True))
But to my surprise, this does not return ['microwave', 'pc']. Instead, 'pc' is listed twice, i.e. shiny_or_schuko is ['microwave', 'pc', 'pc'].
Is this a Django bug? If not, how can I set up the query that I get 'pc' just once?

Is this a Django bug?
No. You simply perform a LEFT OUTER JOIN with the many-to-many table. If two or more related objects match, it will be included multiple times. This can be wanted behavior, for example if you add extra annotations to the elements that takes values from these related objects.
You can make use of .distinct() [Django-doc] to return only distinct elements:
Widget.objects.filter(
Q(shiny=True) | Q(compatible=schuko)
).values_list('name', flat=True).distinct()

Related

Django Conditional update based on Foreign key values / joined fields

I'm trying to do a conditional update based on the value of a field on a foreign key. Example:
Model Kid: id, parent (a foreign key to Parent), has_rich_parent
Model Parent: id, income
So say I have a query set of A. I wanna update each item's has_guardian in A based on the value of age on the Kid's parent in one update. What I was trying to do is
queryset_of_kids.update(
has_rich_parent=Case(
When(parent__income__gte=10, then=True)
default=False
)
)
But this is giving me an error Joined field references are not permitted in this query. Which I am understanding it as joined fields / pursuing the foreignkey relationships aren't allowed in updates.
I'm wondering if there's any other way to accomplish the same thing, as in updating this queryset within one update call? My situation has a couple more fields that I'd like to verify instead of just income here so if I try to do filter then update, the number of calls will be linear to the number of arguments I'd like to filter/update.
Thanks in advance!
Here are the models that I assume you're using:
from django.db import models
class Kid(models.Model):
parent = models.ForeignKey('Parent', on_delete=models.CASCADE)
has_rich_parent = models.BooleanField(default=False)
class Parent(models.Model):
income = models.IntegerField()
You can use a Subquery to update the has_rich_parent field.
The subquery filters on the primary key pk of the surrounding query using .filter(pk=OuterRef('pk')).
It uses a Q query object to obtain whether the parent income is >= 10.
from .models import Kid, Parent
from django.db.models import Q, Subquery, OuterRef
Kid.objects.update(has_rich_parent=Subquery(
Kid.objects.filter(pk=OuterRef('pk'))
.values_list(Q(parent__income__gte=10))))
That command produces the following SQL query:
UPDATE "more_kids_kid"
SET "has_rich_parent" = (
SELECT (U1."income" >= 10) AS "q1"
FROM "more_kids_kid" U0
INNER JOIN "more_kids_parent" U1 ON (U0."parent_id" = U1."id")
WHERE U0."id" = ("more_kids_kid"."id")
)
This query isn't as efficient as a SELECT-then-UPDATE query. However, your database may be able to optimize it.

Double reverse filtering in Django ORM

I am facing one issue while filtering the data .
I am having three models ...
class Product:
size = models.CharField(max_length=200)
class Make(models.Model):
name = models.ForeignKey(Product, related_name='product_set')
class MakeContent(models.Model):
make = models.ForeignKey(Make, related_name='make_set')
published = models.BooleanField()
I can generate a queryset that contains all Makes and each one's related MakeContents where published = True.
Make.objects.filter(make_set__published=True)
I'd like to know if it's possible (without writing SQL directly) for me to generate a queryset that contains all Product and each one's related MakeContents where published = True.
I have tried this
Product.objects.filter(product_set__make_set__published=True)
But it's not working
A subquery can solve the problem.
from django.db.models import Subquery
sub_query = MakeContent.objects.filter(published=True)
Product.objects.filter(
pk__in=Subquery(sub_query.values('make__name__pk'))
)
Use this
Product.objects.filter(make__makecontent__published=True).distinct()
Filtering through ForeignKey works both forward and backwards in Django. You can use the model name in lower case for backward filtering.
https://docs.djangoproject.com/en/3.1/topics/db/queries/#lookups-that-span-relationships
Finally .distinct() is required to remove the multiple values produced due to SQL joins

Prefetch or annotate Django model with the foreign key of a related object

Let's say we have the following models:
class Author(Model):
...
class Serie(Model):
...
class Book(Model):
authors = ManyToManyField(Author, related_name="books")
serie = ForeignKey(Serie)
...
How can I get the list of authors, with their series ?
I tried different combinations of annotate and prefetch:
list_authors = Author.objects.prefetch(Prefetch("books__series", queryset=Serie.objects.all(), to_attr="series"))
Trying to use list_authors[0].series throws an exception because Author has no series field
list_authors = Author.objects.annotate(series=FilteredExpression("books__series", condition=Q(...))
Trying to use list_authors[0].series throws an exception because Author has no series field
list_authors = Author.objects.annotate(series=F('books__series'))
returns all possible combinations of (author, serie) that have a book in common
As I'm using PostgreSQL for my database, I tried:
from django.contrib.postgres.aggregates import ArrayAgg
...
list_authors = Author.objects.annotate(series=ArrayAgg('books__serie', distinct=True, filter=Q(...)))
It works fine, but returns only the id of the related objects.
list_authors = Author.objects.annotate(series=ArrayAgg(
Subquery(
Serie.objects.filter(
livres__auteurs=OuterRef('pk'),
...
).prefetch_related(...)
)
))
fails because it needs an output_field, and a Model is not a valid value for output_field
BUT
I can get the number of series for an author, so why not the actual list of them:
list_authors = Author.objects.annotate(nb_series=Count("books__series", filter=Q(...), distinct=True)
list_authors[0].nb_series
>>> 2
Thus I assume that what I try to do is possible, but I am at a loss regarding the "How"...
I don't think you can do this with an annotation on the Author queryset - as you've already found you can do F('books__series') but that will not return distinct results. Annotations generally only make sense if the result is a single value per row.
What you could do instead is have a method on the Author model that fetches all the series for that author with a relatively simple query. This will mean one additional query per author, but I can't see any alternative. Something like this:
class Author:
def get_series(self):
return Serie.objects.filter(book__authors=self).distinct()
Then you just do:
list_authors = Author.objects.all()
list_authors[0].get_series()

Django - Update multiple integer fields at once

I'd like to update multiple integer fields at once in following model.
class Foo(models.Model):
field_a = models.PositiveIntegerField()
field_b = models.PositiveIntegerField()
field_c = models.PositiveIntegerField()
Originally, it can be done like following code with two queries.
foo = Foo.objects.get(id=1)
foo.field_a += 1
foo.field_b -= 1
foo.field_c += 2
foo.save()
I'd like make it more simpler with update in one query.
However, following attempts raised error.
# 1st attempt
Foo.objects.filter(id=1).update(
field_a=F('field_a')+1,
field_b=F('field_a')-1,
field_c=F('field_a')+2)
# 2nd attempt
Foo.objects.filter(id=1).\
update(field_a=F('field_a')+1).\
update(field_b=F('field_b')-1) ).\
update(field_c=F('field_c')+2)
How can I solve this ?
Form the django docs:
Calls to update can also use F expressions to update one field based on the value of another field in the model. This is especially useful for incrementing counters based upon their current value. For example, to increment the pingback count for every entry in the blog:
>>> from django.db.models import F
>>> Entry.objects.all().update(n_pingbacks=F('n_pingbacks') + 1)
You have to have an instance of Foo or a queryset before you can update. You should do something like this:
Foo.objects.get(id=1)update(field_a=F('field_a')+1).\
update(field_b=F('field_b')-1) ).\
update(field_c=F('field_c')+2)
or
Foo.objects.filter(id__in=[1,3,6,7]).update(field_a=F('field_a')+1).\
update(field_b=F('field_b')-1) ).\
update(field_c=F('field_c')+2)
Reference: https://docs.djangoproject.com/en/1.8/topics/db/queries/#updating-multiple-objects-at-once
If save() is passed a list of field names in keyword argument update_fields, only the fields named in that list will be updated. This may be desirable if you want to update just one or a few fields on an object. There will be a slight performance benefit from preventing all of the model fields from being updated in the database. For example:
product.name = 'Name changed again'
product.save(update_fields=['name'])
see more docs [here]:https://docs.djangoproject.com/en/dev/ref/models/instances/#specifying-which-fields-to-save

Counting objects within a Foreign Key and ManyToMany in Django

I'd like to count the objects I have listed.
I would like to count the number of issues within each title.
This is how my models.py are setup:
# Edited out
Views.py:
def titles (request):
all_titles = Title.objects.all().order_by('title')
num_titles = Title.objects.all().count()
latest_titles = Title.objects.order_by('-date_added')
title_a = Title.objects.filter(title__startswith='A')
title_b = Title.objects.filter(title__startswith='B')
......etc......
I need to do this for creators and characters too, except they are in a Many to Many relationship with Issue and the views essentially look the same as the def titles
Thank you.
To get the related issues for each title you can use the backwards relationship lookup. Since you did not specify a related name in the Issue model where you created the relationship, the lookup is performed using _set appended to the lower cased name of the related model. In your case issue_set.
some_title = Title.object.get(pk=1)
some_title.issue_set.count()
However, this is going to perform a db hit for every title you want to count.
You probably want to annotate the titles qs with the count. docs
from django.db.models import Count
titles_with_counts = Title.objects.all().annotate(issue_count=Count('issue__id'))
now each title in that qs has count accessible by using .issue_count
for title in titles_with_counts:
print title.title, title.issue_count