Django ORM'i etkili bir şekilde kullanmak
Django ORM'in bize development hızı konusunda katkısı oldukça büyük. Ancak nasıl olsa ORM hallediyor diye Database ile iletişimimizi kurarken bazı hususları dikkate almazsak development sürecinde bize kazandırdığı zamanı, production ortamına geçtiğimizde kat kat geri alabiliyor. Tahmin edersiniz ki genelde bizden aldığı sadece zaman değil, aynı şekilde paramız da. Müşteriye ya da işverene karşı vermek zorunda olduğunuz cevaplar da cabası.
İlk önce Django ORM hakkında birkaç şeyden bahsetmek istiyorum. Django ORM mümkün olduğunca en performanslı sorguları hazırlamaya çalışıyor. Bizim dikkat etmemiz gereken nokta ise ORM'den ne istediğimizi biliyor olmamız. Biraz soyut bir cümle olduğunun farkındayım. Yazının ilerleyen kısımlarında bahsettiğim daha da açacağım.
Sorgular ne zaman çalışıyor
Queryset metodlarının çoğu siz aşağıdaki işlemlere yapana dek herhangi bir SQL sorgusu çalıştırmamaktadır. __iter__ magic method'unu tetikleyecek bir işlem (örnek: for döngüsu, list, map)
__repr__ magic method'unu tetikleyen bir işlem (örnek: print)
__slice__ magic method'unu tetikleyen bir işlem (örnek: [:30] şeklindeki slice işlemleri)
Bunların tüm listesine ve açıklamalarına Django ORM dökümantasyonunun When Querysets are Evaluated bölümünden ulaşabilirsiniz.
posts = Post.objects.all() # herhangi bir sorgu çalışmadı
print posts # queryset'in __repr__ method'unu çağırdığımız
# için sql sorgusu burada çalıştı
posts = Post.objects.all()
published_posts = posts.filter(is_published=True)
for post in published_posts: # iterate islemini gerçekleştirdiğimiz
Bu bilgi sorgularımızı optimize etmek için oldukça önemlidir.
Related (birbiriyle ilişkili) modeller
Sorgularımızı optimize ederken dikkat etmemiz gereken en önemli noktalardan birisi de related modellerdir. Django ORM'de eğer ekstra bir müdahale'de bulunmazsanız Foreign Key'lerde uygun olan JOIN işlemini yapmak yerine her Foreign Key için ayrı sql sorgusu yapacaktır. Aşağıdaki gibi modellerimizin olduğunu düşünelim.
Bir sorgu yapalım. Post nesnelerini listeleyip, post'un bağlı olduğu user'ı yazdıralım.
print post.user # her post için burada ayrı sorgu çalışmaktadır
Burada herhangi bir JOIN işlemi kullanılmadı. User'ı yazdırmak için eğer 100 adet post varsa 100, bir de postları listeleyen sorgu ile birlikte tam 101 tane sorgu çalıştırıldı. Bu problem sadece Django ORM'ye özgü bir problem olmamakla birlikte genelde N+1 query problem diye bilinmektedir.
Bunun gibi işlemi optimize etmek için Queryset üzerinde select_related adında bir method bulunmaktadır. Bu method ile model üzerindeki Foreign Key ile belirtiğimiz modeldeki verileri JOIN işlemi yaparak getirebiliriz.
posts = Post.objects.select_related()
print post.user # burada sorgu çalışmadı.
Bu sayede select_related metodu ile işlemimizi 101 sorgudan tek sorguya indirmiş olduk. Bu metodun aşağıdaki gibi kullanımları mümkündür.
Post.objects.select_related() # tüm ForeignKey field'larını takip eder
Post.objects.select_related(depth=1) # sadece 1. seviyedekileri takip eder
Post.objects.select_related("user") # sadece user field'ini takip eder
Select_related'in metodunu ForeignKey'in null parametresinin değerine uygun bir şekilde çağırmanız gerekir. Yoksa JOIN işlemi gerçekleşmez. Bunun nedeni ise ORM'nin uygun olan JOIN tipini bu değere göre yapmasıdır.
Foreign Key'lerde hangi JOIN tipini kullanılıyor
Eğer Foreign Key'iniz zorunlu bir field ise, yani null değeri default değerinde ise Inner Join kullanılmaktadır.
Eğer Foreign Key'in değeri NULL ise ya da ilişkili tablo eşleşmiyorsa o satır getirilmez.
Foreign Key'inizin NULL değer alabiliyorsa (yani null=True şeklinde tanımladıysanız) Left Outer Join kullanılmaktadır.
Belirtiğiniz Foreign Key'deki veri ilişkili tablo ile uyuşmuyorsa ilişkili tablodaki veri NULL olarak getirilir. Outer keyword'u opsiyoneldir. Left Outer Join yerine Left Join de
Eğer Foreign Key'iniz null=True şeklinde ise select_related methodu boş olarak ya da depth vererek çağıramazsınız. Foreign Key'inizin field adını parametre olarak vermeniz gerekir.
# şu şekildeki bir foreign key için
user = ForeignKey(User, null=True)
Post.objects.select_related() # çalışmaz
Post.objects.select_related(depth=1) # çalışmaz
Post.objects.select_related("user") # çalışır
Foreign Key'in null değeri False ise select_related metodunu her şekilde çağırabilirsiniz
ForeignKey ve OneToOneField'larda select_related method'u işimizi tek sorgu ile sql tarafında halledebilmekte. Peki ManyToManyField sorgularını nasıl optimize edeceğiz?
Bir önceki örneği post'un bağlı olduğu categories ManyToManyField'ını yazdırarak yapalım.
posts = Post.object.all()
Sorgu sayımız tekrar 101'e çıktı. Bu işlemi optimize etmek için Django 1.4 ile gelen prefetch_related method'unu kullanabiliriz.
posts = Post.objects.prefetch_related("categories")
ForeignKey ve OneToOneField'larda sorgu sayısını 1'e indirebilmiştik. Ancak ManyToMany ilişkileri maalesef tek sorgu olacak şekilde optimize edemiyoruz. Bu ORM'nin değil, SQL'in bir problemidir. SQL tarafında bir kayıta denk gelen birden çok kayıtı aynı satır içinde almak gibi bir şey söz konusu değildir.
Bunun gibi durumlarda Django ORM iki adet sorgu çalıştırmaktadır. Örnekte çalışan 1. sorgu tüm post nesnelerini alırken, ikinci sorgu sadece dönen post nesnelerinin id'lerine göre category nesnelerini çekmektedir. Bu iki sonuç SQL tarafından döndükten sonra Python tarafında map edilmektedir.
Generic Foreign Key'lerde prefecth_related kullanımı
Maalesef Django ORM'de böyle bir şey şu an için mümkün değil. Çünkü Generic Foreign Key'lerde veriler ilişkisel bir şekilde tutulmuyor. Ancak bunu django'nun prefetch_related methoduna benzer bir yöntemle çözebiliriz. Örnek django-taggit'i verebilirim. Post modelimize django-taggit'in tags manager'ini ekleyip optimize etmeye çalışalım.
Sorgumuzu aşağıdaki gibi optimize edebiliriz.
content_type = ContentType.objects.get_for_model(queryset.model)
tagged_items = TaggedItem.objects.\
for post in queryset:
post.cached_tags = [tagged_item.tag
for tagged_item in tagged_items
if tagged_item.object_id == post.pk]
Tag'ları ayrı bir şekilde çekip bunları Python tarafında Post objesine map ettik. Django ORM'deki prefetch_related metodu da benzer bir şekilde çalışmakta.
Gereksiz field'ları sorgumuzdan çıkarmak
Bazen bir modelden sadece belirli field'ları almak isteyebilirsiniz. Örneğin Post modelindeki sadece id'ler ya da id ve başlıklar gibi. Bunun gibi durumlarda modeldeki tüm field'ları sorguda getirmek yerine Queryset'teki values ve values_list metodlarını kullanabilirsiniz.
for id, title in Post.objects.values_list("id", "title"):
print id, title
Eğer tek bir alan çekmek istiyorsanız values_list metoduna flat=True ekleyebilirsiniz.
print Post.objects.values_list("pk", flat=True)
# sonuç [1, 2, 3, 4, ..10] şeklinde olacaktır.
Sonuçların liste değil de dictionary şeklinde gelmesini istiyorsanız da values metodunu kullanabilirsiniz.
for post in Post.objects.values("title", "pk"):
print post.get("pk"), post.get("title")