0

8 نکته از جنگو برای کار با پایگاه داده (قسمت اول)

ORM ها ابزارهای بسیار خوبی را برای توسعه دهندگان ارائه می دهند اما دسترسی انتزاعی(abstract) به پایگاه داده هزینه های زیادی دارد.

در این مقاله قصد دارم 8 نکته برای کار با پایگاه داده در جنگو را به اشتراک بگذارم.

اجتماع با فیلتر

قبل از جنگو ورژن 2 اگر می خواستیم چیزی مانند تعداد کل کاربران و تعداد کل کاربران فعال را بدست آوریم ، مجبور به استفاده از عبارات شرطی بودیم:

from django.contrib.auth.models import Userfrom django.db.models import (
Count,
Sum,
Case,
When,
Value,
IntegerField,
)User.objects.aggregate(
total_users=Count('id'),
total_active_users=Sum(Case(
When(is_active=True, then=Value(1)),
default=Value(0),
output_field=IntegerField(),
)),
)

در Django 2.0 یک آرگومان فیلتر برای جمع کردن توابع اضافه شد تا این کار را بسیار راحت تر کند:

from django.contrib.auth.models import Userfrom django.db.models import Count, FUser.objects.aggregate(
total_users=Count('id'),
total_active_users=Count('id', filter=F('is_active')),
)

 کوتاه وشیرین 🙂

 

اگر از PostgreSQL استفاده می کنید ، دو کوئری به این شکل ساخته  می شوند:

SELECT COUNT(id) AS total_users,
 SUM(CASE WHEN is_active THEN 1 ELSE 0 END) AS total_active_users
FROM
auth_users;SELECT
COUNT(id) AS total_users,
 COUNT(id) FILTER (WHERE is_active) AS total_active_users
FROM
auth_users;

نتایج QuerySet را به عنوان tuple های نامگذاری شده تنظیم کنید

در Django 2.0 ویژگی جدیدی به values_list اضافه شد به نام named. مقداردهی named به مقدار true ، مجموعه کوئری  را به عنوان لیستی از namedtuples برمی گرداند:

> user.objects.values_list( 'first_name',
'last_name',
)[0]
(‘Haki’, ‘Benita’)> user_names = User.objects.values_list(
'first_name',
'last_name',
 named=True,
)> user_names[0]
Row(first_name='Haki', last_name='Benita')> user_names[0].first_name
'Haki'> user_names[0].last_name
'Benita'
 

 

توابع سفارشی

Django ORM بسیار قدرتمند و دارای ویژگی های غنی است اما احتمالاً نمی تواند با  گام به گام  همراه تمام توسعه دهندگان پایگاه داده همراه باشد. خوشبختانه ORM به ما اجازه می دهد تا آن را با توابع سفارشی گسترش دهیم.

فرض کنید ما یک مدل گزارش با فیلد مدت زمان داریم. ما می خواهیم مدت زمان متوسط همه گزارش ها را پیدا کنیم:

from django.db.models import AvgReport.objects.aggregate(avg_duration=Avg(‘duration’))> {'avg_duration': datetime.timedelta(0, 0, 55432)}

 

این عالی است ، اما متوسط به تنهایی اطلاعات زیادی به ما نمیدهد. بیایید سعی کنیم انحراف استاندارد را نیز بدست آوریم:

from django.db.models import Avg, StdDevReport.objects.aggregate( avg_duration=Avg('duration'),
 std_duration=StdDev('duration'),
)ProgrammingError: function stddev_pop(interval) does not exist
LINE 1: SELECT STDDEV_POP("report"."duration") AS "std_dura...
^
HINT: No function matches the given nam
 

لعنتی 🙁 … PostgreSQL از stddev در  فیلدهای درونی خودش پشتیبانی نمی کند – قبل از استفاده از STDDEV_POP باید این فیلد را به یک عدد تبدیل کنیم.

یک گزینه استخراج دوره از مدت زمان است:

SELECT AVG(duration),
 STDDEV_POP(EXTRACT(EPOCH FROM duration))
FROM 
report; avg | stddev_pop 
----------------+------------------
00:00:00.55432 | 1.06310113695549(1 row)
 

بنابراین چگونه می توانیم این را در جنگو پیاده سازی کنیم؟  درست حدس زدید – یک عملکرد سفارشی:

# common/db.pyfrom django.db.models 
import Funcclass Epoch(Func): function = 'EXTRACT'
template = "%(function)s('epoch' from %(expressions)s)"
 

و از عملکرد جدید ما مانند این کد استفاده کنید:

from django.db.models import Avg, StdDev, Ffrom common.db import EpochReport.objects.aggregate(
avg_duration=Avg('duration'), 
std_duration=StdDev(Epoch(F('duration'))),
){'avg_duration': datetime.timedelta(0, 0, 55432),
'std_duration': 1.06310113695549}
 

 

LIMIT

بعضی اوقات می خواهیم به کاربران اجازه دهیم گزارش هایی تولید کنند و شاید آنها را به spreadsheet صادر کنیم.

غیر معمول نیست که با کاربری روبرو شوید که فکر می کند خرپجی گرفتن  همه فروش ها در اواسط روز کاری منطقی است.

اینجا جایی است که LIMIT وارد می شود.

بیایید یک کوئری خاص را به بیش از 100 ردیف محدود کنیم:

# bad example
data = list(Sale.objects.all())[:100]

این بدترین کاری است که می توانید انجام دهید. شما فقط تمام ردیف ها را به حافظه وصل کرده اید تا 100 مورد اول را برگردانید.

بیایید دوباره امتحان کنیم:

data = Sale.objects.all()[:100]

بهتر شد. جنگو از عبارت limit در SQL برای گرفتن فقط 100 ردیف استفاده خواهد کرد.

حالا بگذارید بگوییم ما limit را اضافه کردیم ، کاربران تحت کنترل هستند و همه چیز خوب است. ما هنوز یک مشکل داریم – کاربر تمام فروش ها را درخواست کرده و ما 100 مورد از آنها را به آنها داده ایم. کاربر اکنون فکر می کند فقط 100 فروش انجام شده است –

این اشتباه است.

به جای اینکه 100 ردیف اول را کورکورانه برگردانید ، مطمئن شوید که اگر بیش از 100 ردیف وجود دارد (به طور معمول پس از فیلتر) یک استثنا را پرتاب می کنیم:

LIMIT = 100
if Sales.objects.count() > LIMIT: raise ExceededLimit(LIMIT)return Sale.objects.all()[:LIMIT]
 

این کد کار می کند اما ما فقط یک کوئری دیگر اضافه کردیم.

آیا می توانیم بهتر عمل کنیم؟ من فکر می کنم ما می توانیم:

 

LIMIT = 100data = Sale.objects.all()[:(LIMIT + 1)]if len(data) > LIMIT:
raise ExceededLimit(LIMIT)return data
 

ما به جای واکشی 100 ردیف ، 100 ردیف  101=100+1 ردیف را واکشی می کنیم. اگر ردیف 101 وجود داشته باشد ، کافی است که بدانیم بیش از 100 ردیف وجود دارد. یا به عبارت دیگر ، واکشی LIMIT + 1 ردیف حداقل چیزی است که برای اطمینان از وجود بیش از LIMIT ردیف در نتیجه درخواست ، به آن نیاز نداریم.

ترفند LIMIT + 1 را به خاطر بسپارید ، گاهی اوقات می تواند بسیار مفید باشد.

 

 

ارسال دیدگاه

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *