fatiherikli

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.

Bunların tüm listesine ve açıklamalarına Django ORM dökümantasyonunun When Querysets are Evaluated bölümünden ulaşabilirsiniz.

Örnek:

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ı

Bir başka örnek:

posts = Post.objects.all()
published_posts = posts.filter(is_published=True)
for post in published_posts: # iterate islemini gerçekleştirdiğimiz 
                             # için sql sorgusu burada çalıştı
    pass

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.

class Category(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=255)
    user = models.ForeignKey(User)
    categories = models.ManyToManyField(Category)

Bir sorgu yapalım. Post nesnelerini listeleyip, post'un bağlı olduğu user'ı yazdıralım.

posts = Post.objects.all()
for post in posts:
    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()
for post in posts:
    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.

Inner Join
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.

Left Outer Join
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 kullanılabilmektedir.

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

Many-to-Many ilişkiler

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()
for post in posts:
    print post.categories.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")
for post in posts:
    print post.categories.all()

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.

class Post(models.Model):
    title = models.CharField(max_length=255)
    user = models.ForeignKey(User)
    categories = models.ManyToManyField(Category)

    tags = TaggableManager()

Sorgumuzu aşağıdaki gibi optimize edebiliriz.

queryset = Post.objects.all()

content_type = ContentType.objects.get_for_model(queryset.model)
tagged_items = TaggedItem.objects.\
select_related("tag").filter(
    content_type=content_type,
    object_id__in=queryset.values_list("pk", flat=True))

for post in queryset:
    post.cached_tags = [tagged_item.tag
                        for tagged_item in tagged_items
                        if tagged_item.object_id == post.pk]

for post in queryset:
    print post.cached_tags

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")
comments powered by Disqus