Django annotation ve aggregation
Sep 20, 2018 · 3 minute read · Commentsprogrammingpython
Django’da annotate
ve aggregate
kullanarak, veritabanı tablosundaki satırların toplamı, ortalaması gibi, birden çok satırdan veya ilgili diğer tablolardaki değerler üzerinden database seviyesinde hesaplama yapıp tek sorguda sonucu alabiliriz.
Aşağıdaki gibi Flat
ve FlatDebit
adında iki modelimiz olduğunu varsayalım. Yazıyı bu modeller üzerinden örnekler vererek anlatmaya çalışacağım.
class Flat(models.Model):
name = models.CharField(max_length=30)
class FlatDebit(models.Model):
flat = models.ForeignKey('myapp.Flat', related_name='flat_debits')
amount = models.DecimalField(decimal_places=2, max_digits=9)
Aggregation
Aggregate result olarak tek satır veri döndürür.
Aggragete kullanımına örnekler:
# Dairenin borçlarının toplamı
result = FlatDebit.objects.filter(flat_id=1).aggregate(
total_amount=Sum('amount')
)['total_amount']
# Dairenin kaç adet borcu var?
result = Flat.objects.filter(flat_id=1).aggregate(
debit_count=Count('flat_debits')
)['debit_count']
Dikkat ettiyseniz aggragation sorgusundan queryset yerine tek satır veri dict
olarak donduruyor.
Sunuda unutmamak gerekir ki aggregate sorgusundan bir sonuç dönmeyedebilir. Mesela yukarıdaki sorgulardan birinde herhangi bir sonuç dönmez ise dictteki elemana ulaşmaya çalıştığımda hata verecektir.
Sonuç dönmemesi durumunda default bir değer döndürmek güzel bir yöntemdir.
Bu sayede ekstradan kontrol etmemize gerek kalmaz. SQL sorgusu sonucunda bir sonuç dönmemesi durumunda default bir değer dönmesi için Coalesce
kullanılır.
Annotation
Mesela dairelerin borçlarının listelendiği bir sayfamız olduğunu varsayalım. Sayfada her dairenin ismini ve o dairenin nekadar borcu olduğunu yan yana göstermek istiyoruz diyelim.
Django annotation kullanmadan bunu yapmak istediğimizde önce daireleri çekip daha sonra her dairenin nekadar borcu olduğunu çekeriz:
flats = Flat.objects.all()
for flat in flats:
total_debit = flat.flat_debits.aggregate(
total=Sum('amount')
)['total']
print(flat.name, total_debit)
Bu şekilde kullandığımızda ilk başta bütün daireleri çekmek için daha sonra ise her daireye ait borçların toplamını için database’e istek yapıyoruz. Sistemde toplamda 100 adet daire varsa 101 defa database’e istek yapıcaktır. Bu işlem çok maliyetli bir işlem. Bunu tek sorguda yapmak için annotation kullanılabilir.
flats = Flat.objects.all().annotate(total_debit=Sum('flat_debits__amount'))
for flat in flats:
print(flat.name, flat.total_debit)
Burda daireleri getirirken her dairenin üzerinde o daireye ait borçların toplamını getirmiş oluyoruz, sadece 1 query.
Annotate ile hesaplanan fieldlari kullanmak
Dairelerin borçlarının listelendiği sayfaya dairelerin borç tutarına göre filtreleme eklemek istediğimizi varsayalım. Kullanıcı minimum 1000TL borcu olan daireleri listelemek istiyor diyelim.
Bunun için önce her dairenin borçlarının toplamını alıp daha sonra dairenin borcu kullanıcının girdiği tutardan büyükse onları listeleyelim.
flats = Flat.objects.annotate(total_debit=Sum('flat_debits__amount')).filter(total_debit__gte=1000
)
Burda once daireleri cekerken borclarinin toplamini total_debit
adinda bir fielda atiyoruz. Daha sonra da bu fielda gore filtreleme yapiyoruz.
Annotate ile hesapladigimiz fieldi filter
, annotate
veya aggragate
icerisinde kullanabilecegimizi unutmamak gerekir.
Mesela her flatin kendi üzerinde borçlarını annotate
ile hesaplıyoruz. Raporun altında toplam borcu göstermek için ise annotate
ile hesapladığımız fieldi aggregate
e veriyoruz.
flats = Flat.objects.all().annotate(
total_debit=Sum('flat_debits__amount')
)
Yukarıdaki annotation içerisinnde zaten her flat için yaptığımız hesaplanan değerleri toplayarak toplam borcu alabiliyoruz.
flats.aggregate(
total=Coalesce(Sum('total_debit'), 0)
)['total']
Hatalı sonuca sebep olabilecek annotation/aggragation kullanımı
Bir queryset üzerinde birden fazla tablo ile ilişkili annotation olduğu durumda hatalı sonuç dönebiliyor. Mesela borçları toplam tutarları ve faiz tutarlarını biryerde göstermek istiyoruz. Bir borca birden fazla faiz işlemiş olabilir.
Aşağıdaki queryseti incelersek ann_debit_amount
fieldini hesaplamak için accrual
relation’ına gidiyoruz.
Daha sonra aynı queryset içerisinde ann_interest_amount
fieldi için flat_interests
relationina gidiyoruz. Eğer ilgili objenin birtane flat_interest
‘i varsa sıkıntı çıkmaz ama birden fazla varsa aynı flat_interest iki defa gelicektir. Yani 4TL+6TL olmak üzere 10TLlik faiz varken faizi 20TL olarak hesaplayacaktır.
# Borç hesaplamaları başlangıç
flat_debits = flat_debits.annotate(
ann_debit_amount=Coalesce(Sum(
Case(When(accrual__due_date__lte=due_date, then='amount'))
), 0),
ann_interest_amount=Coalesce(Sum('flat_interests__amount'), 0),
).annotate(
ann_total_amount=F('ann_debit_amount') + F('ann_interest_amount'),
)
Bunu çözmek için faizler için ekstra bir query atarak farklı querysetleri in_bulk
ile birleştirerek çözebiliyoruz.
flat_debits = flat_debits.annotate(
ann_debit_amount=Coalesce(Sum(
Case(When(due_date__lte=due_date, then='amount'))
), 0),
)
flat_interests = flat_debits.annotate(
ann_interest_amount=Coalesce(Sum('flat_interests__amount'), 0),
).in_bulk()
for debit in flat_debits:
debit.dyn_interest_amount = flat_interests[debit.pk].ann_interest_amount