Update column in Django with child columns - django

I have 2 models Parent, Child
class Parent(models.Model):
id = Base64UUIDField(primary_key=True, editable=False)
cost = models.DateTimeField(default=None, blank=True, null=True)
class Child(models.Model):
id = Base64UUIDField(primary_key=True, editable=False)
cost = models.DateTimeField(default=None, blank=True, null=True)
parent = models.ForeignKey(Parent, related_name= "children", related_query_name= "child")
I need to populate cost column of Parent objects to maximum cost of all children of that parent
I have tried to annotate to a new column new_cost, and its works.
parents.annotate(new_cost=Max('child__cost'))
But I need to populate values to existing column cost. Tried something like this, but not working.
parents.update(cost=Max('child__cost'))

Probably that can be achieved with a Subquery expression:
from django.db.models import OuterRef, Subquery
Parent.objects.update(
cost=Subquery(Child.objects.filter(
parent_id=OuterRef('pk')
).values('cost').order_by('-cost')[:1])
)
I would however advise to simply use .annotate(…) when you need the largest cost of the related Childs.

Use aggregate againist to annotate
test = parents.aggregate(new_cost=Max('child__cost'))
parents.update(cost=test["new_cost"])

Related

Performing a Subquery, Sum and Join in django ORM

I have 2 django models which aren't linked by ForeignKey due to legacy system.
class Parent(model):
name -> CharField()
class Child(model)
parent_name -> CharField()
cost -> IntegerField()
I want to achieve a left join which gives me all parent columns along with sum of cost column from children.
SQL in postgres translates to
select parent.name, sum(child.cost) as cost from parent join child on parent.name = child.parent_name group by parent.name;
Is there any way to achieve this with django ORM
I have tried a lot of things but https://code.djangoproject.com/ticket/28296 might be what is blocking.
Please use a ForeignKey to refer to a parent, not a CharField that joins on the name. This will guarantee referential integrity, avoid data duplication and makes it more convenient to query in Django.
You thus define the models as:
class Parent(models.Model):
name = models.CharField(max_length=128)
class Child(models.Model):
parent = models.ForeignKey(
Parent,
on_delete=models.CASCADE
)
cost = models.IntegerField()
or if the name of the Parent is unique, you can let it refer to the name:
class Parent(models.Model):
name = models.CharField(max_length=128, unique=True)
class Child(models.Model):
parent = models.ForeignKey(
Parent,
to_field='name',
db_column='parent_name',
on_delete=models.CASCADE
)
cost = models.IntegerField()
then you can .annotate(…) [Django-doc] the Parent model with:
from django.db.models import Sum
Parent.objects.annotate(
cost=Sum('child__cost')
)
the Parent objects that arise from this queryset will have an extra attribute .cost that will contain the sum of the costs of the Childs that point to that Parent.

Django: Annotate with field from another table (one-to-many)

Good day.
I wish to annotate my model with information from a different table.
class CompetitionTeam(models.Model):
competition_id = models.ForeignKey('Competition', on_delete=models.CASCADE, to_field='id', db_column='competition_id')
team_id = models.ForeignKey('Team', on_delete=models.CASCADE, to_field='id', null=True, db_column='team_id')
...
class Team(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=30)
teamleader_id = models.ForeignKey('User', on_delete=models.CASCADE, to_field='id', db_column='teamleader_id')
...
class Competition(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=30)
...
Looping through my competitions, I wish to retrieve the list of competitionteam objects to be displayed with the relevant team's name. I tried:
CompetitionTeam.objects.filter(competition_id=_competition.id).filter(team_id__in=joined_team_ids).annotate(name=...)
-where instead of the ellipses I put Subquery expressions in. However, I'm unsure of how to match the team_id variable. eg.
*.anotate(name=Subquery(Team.objects.filter(id=competitionteam.team_id)).values('name'))
Related is the question: Django annotate field value from another model but I am unsure of how to implement that in this case. In that case, in place of mymodel_id, I used team_id but it only had parameters from the Team object, not my competition team object. I didn't really understand OuterRef but here is my attempt that failed:
CompetitionTeam.objects.filter(competition_id=_competition.id).filter(team_id__in=joined_team_ids).annotate(name=Subquery(Team.objects.get(id=OuterRef('team_id'))))
"Error: This queryset contains a reference to an outer query and may only be used in a subquery."
The solution for my question was:
CompetitionTeam.objects.filter(
competition_id=_competition.id,
team_id__in=joined_team_ids
).annotate(
name=Subquery(
Team.objects.filter(
id=OuterRef('team_id')
).values('name')
))
Thanks.

Django - Getting latest of each choice

I have two models: the gas station and the price of a product. The price can up to have 4 choices, one for each product type, not every station has all four products. I want to query the latest entry of each of those products, preferably in a single query:
class GasStation(models.Model):
place_id = models.IntegerField(primary_key=True)
name = models.CharField(max_length=255, null=True, blank=True)
class Price(models.Model):
class Producto(models.TextChoices):
GASOLINA_REGULAR = 'GR', _('Gasolina regular')
GASOINA_PREMIUM = 'GP', _('Gasolina premium')
DIESEL_REGULAR = 'DR', _('Diesel regular')
DIESEL_PREMIUM = 'DP', _('Diesel premium')
product = models.CharField(max_length=2, choices=Producto.choices)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
price = models.FloatField(null=True, blank=True)
estacion = models.ForeignKey(GasStation,
on_delete=models.SET_NULL,
null=True,
related_name='prices')
I've tried with:
station.price.filter(product__in=['GR', 'GP', 'DR', 'DP']).latest()
But it only returns the latest of the whole queryset, not the latest price of each product type. I want to avoid querying for each individual product because some stations don't sell all types .Any advice?
You're looking for annotations and Subquery. Below is what I think might work. Your models aren't fully defined. If you need the whole Price instance, then this won't work for you. Subquery can only annotate a single field.
from django.db.models import OuterRef, Subquery
stations = GasStation.objects.annotate(
latest_regular=Subquery(
Price.objects.filter(station_id=OuterRef("pk"), product="GR").order_by('-updated').values("price")[:1]
),
latest_premium=Subquery(
Price.objects.filter(station_id=OuterRef("pk"), product="GP").order_by('-updated').values("price")[:1]
),
...
)
station = stations.get(something_here)
station.latest_premium, station.latest_regular
You can make this more concise by using a dict comprehension iterating over your Product short codes and then doing .annotate(**annotations)

Annotate over a field of intermediate model django

I have the following scheme:
class User(AbstractUser):
pass
class Task(models.Model):
pass
class Contest(models.Model):
tasks = models.ManyToManyField('Task',
related_name='contests',
blank=True,
through='ContestTaskRelationship')
participants = models.ManyToManyField('User',
related_name='contests_participated',
blank=True,
through='ContestParticipantRelationship')
class ContestTaskRelationship(models.Model):
contest = models.ForeignKey('Contest', on_delete=models.CASCADE)
task = models.ForeignKey('Task', on_delete=models.CASCADE)
cost = models.IntegerField()
class ContestParticipantRelationship(models.Model):
contest = models.ForeignKey('Contest', on_delete=models.CASCADE)
user = models.ForeignKey('User', on_delete=models.CASCADE)
task = models.ForeignKey('Task', on_delete=models.CASCADE, related_name='contests_participants_relationship')
is_solved = models.BooleanField()
Now I get the contest object and need to fetch all tasks over the tasks field, rach annotated with count of users solved it. So, I need to count the number of ContestParticipantRelationship with needed task, needed contest and is_solved set to True. How to make such a query?
Probably something like:
from django.db.models import IntegerField, Value, Sum
from django.db.models.functions import Cast, Coalesce
Task.objects.filter(
contests__contest=some_contest,
).annotate(
nsolved=Cast(Coalesce(
Sum('contests_participants_relationship__is_solved'), Value(0)
),IntegerField())
)
So here we first filter on the fact that the contest of the task is some_contest. And next we perform a Sum(..) over the is_solved column. Since there are corner-cases where this can be NULL (in case there is no user that made an attempt, etc.), then we convert it to a 0, and furthermore we cast it to an IntegerField, since otherwise some instances might be annotated with True, and False in case zero or one user solves it.

Sumproduct using Django's aggregation

Question
Is it possible using Django's aggregation capabilities to calculate a sumproduct?
Background
I am modeling an invoice, which can contain multiple items. The many-to-many relationship between the Invoice and Item models is handled through the InvoiceItem intermediary table.
The total amount of the invoice—amount_invoiced—is calculated by summing the product of unit_price and quantity for each item on a given invoice. Below is the code that I'm currently using to accomplish this, but I was wondering if there is a better way to handle this using Django's aggregation capabilities.
Current Code
class Item(models.Model):
item_num = models.SlugField(unique=True)
description = models.CharField(blank=True, max_length=100)
class InvoiceItem(models.Model):
item = models.ForeignKey(Item)
invoice = models.ForeignKey('Invoice')
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.DecimalField(max_digits=10, decimal_places=4)
class Invoice(models.Model):
invoice_num = models.SlugField(max_length=25)
invoice_items = models.ManyToManyField(Item,through='InvoiceItem')
def _get_amount_invoiced(self):
invoice_items = self.invoiceitem_set.all()
amount_invoiced = 0
for invoice_item in invoice_items:
amount_invoiced += (invoice_item.unit_price *
invoice_item.quantity)
return amount_invoiced
amount_invoiced = property(_get_amount_invoiced)
Yes, it is possible since Django 1.1 where aggregate functions were introduced. Here's a solution for your models:
def _get_amount_invoiced(self):
self.invoiceitem_set.extra(select=("item_total": "quantity * unit_price")
).aggregate(total=Sum("item_total")["total"]
It is, however, highly recommended to store item_total in a database, because it may be subject to discounts, taxes and other changes that make calculating it evety time impractical or even impossible.