Constraint (kısıtlamalar), ilişkisel veritabanlarında üzerinde çalışılan tablonun sütunlarına uygulanan kurallara verilen isimdir. Bu kısıtlamalar ile tablo üzerinde tutulacak verinin güvenirliğini ve doğruluğunu sağlarız.
Kısıtlamalar, bir sütun seviyesinde veya tablo seviyesinde olabilir. Sütun düzeyindeki kısıtlamalar yalnızca bir sütuna, tablo düzeyindeki sınırlamalar ise tüm tabloya uygulanır.
Django 2.2+ ile modellerimize artık unique together dışında db levelde uygulanması için kısıtlamalar getirebiliyoruz. Bugün bu kısıtlamaların nasıl getirilebileceğine, çalışma mekanizmasına ve neden önemli olduklarını örnekler üzerinden inceleyeceğiz. Kahveler hazırsa başlayalım!
Aşağıdaki gibi etkinlik bilgilerini barındıran bir modelimiz olduğunu varsayalım.
from django.db import models from django.utils.translation import gettext_lazy as _ class EventStatus(models.TextChoices): PUBLIC = "public", _("Public") PRIVATE = "private", _("Private") class Event(models.Model): name = models.CharField(max_length=200) status = models.CharField(choices=EventStatus.choices, max_length=20) start_date = models.DateTimeField() end_date = models.DateTimeField()
Daha sonrasında shelle düşüp bir etkinlik yaratalım.
In [2]: from django.utils import timezone In [3]: from your_app.models import EventStatus In [4]: end_date = timezone.now() In [5]: start_date = timezone.now() In [6]: event = Event.objects.create(name="event name", status=EventStatus.PUBLIC, end_date = end_date, start_date=start_date) In [7]: event.status Out[7]: <EventStatus.PUBLIC: 'public'>
Buraya kadar dikkat çekici bir şey yok gibi gözüküyor, başarılı bir şekilde etkinliği oluşturduk.
status fieldina geçtiğimiz choices parametresinin, Django’nun model validasyonlarında gelen değeri doğrulamak için kullanıldığını da biliyoruz. Örneğin;
In [9]: event.status = "unexpected" In [10]: event.full_clean() --------------------------------------------------------------------------- ValidationError Traceback (most recent call last) <ipython-input-10-1ecdc400ed56> in <module> ----> 1 event.full_clean() ~/blogs/constraints/venv/lib/python3.8/site-packages/django/db/models/base.py in full_clean(self, exclude, validate_unique) 1236 1237 if errors: -> 1238 raise ValidationError(errors) 1239 1240 def clean_fields(self, exclude=None): ValidationError: {'status': ["Value 'unexpected' is not a valid choice."]}
Fakat gerçekten bu beklenmedik veriyi yazmamızın önüne geçecek mi?
In [11]: event.save() In [12]: event.status Out[12]: 'unexpected'
Dahası var. Dikkatli bir okuyucu iseniz (ki hepimiz öyleyizdir 🙂 ) etkinliği oluştururken etkinliğin başlangıç tarihinin, bitiş tarihinden daha sonra olduğunu fark etmişsinizdir. Burada da mantıksal bir hatamız var gibi gözüküyor. Adım adım 2 casei de değerlendirelim, bu davranışı önlemek için neler yapabiliriz, constraintler veri doğruluğunda neden önemlidir kavramaya çalışalım.
İlk case için;
Django’da fieldlara geçtiğimiz choices veya validators parametreleri, model validation işlemi sırasında kullanılır ve değer herhangi bir validatöre uymuyorsa hata raise eder. Bu işlem form veya serializerlar için dizayn edilmiştir. Database seviyesinde bir kısıtlama oluşturulmaz ve default save metodu full_clean metodunu çağırmaz.
Django, uygulamanızı kullanan diğer parçaların birer yetişkin olduğunu ve ne yaptığını bildiğini varsayar. (bknz Python Zen) Dolayısıyla bir validasyondan geçmeyen veri, eğer veritabanında bir kısıtlama yoksa doğrudan içeriye alınacaktır.
İkinci case için ise;
Bu biraz daha mantıksal bir durum. Django evet çok yetenekli ama bir sihirbaz değil ve gireceğiniz 2 tarihin neleri temsil ettiğini ve birbirleriyle olan ilişkisini bilemez. Custom bir validator yazarak bu yeteneği form ve serializer katmanına kazandırmanızı bekler. Ama unutmayın ki hala birer yetişkiniz ve yazılan validator hala veritabanına giden veriyi full_clean çalıştırılmadıkça veya kısıtlama konulmadıkça içeriye kabul edecektir.
Django Database Constraints.
Neyse ki Django 2.2+ ile artık bu işleri built-in olarak yapabilmekteyiz.
Sırası ile 2 adet kısıtlama yazıp, üstüne konuşacağız.
Kısıtlamalar model Meta sınıfında constraints adlı alana CheckConstraint sınıfı kullanılarak aşağıdaki gibi tanımlanır.
from django.db import models class Event(models.Model): name = models.CharField(max_length=200) status = models.CharField(choices=EventStatus.choices, max_length=20) start_date = models.DateTimeField() end_date = models.DateTimeField() class Meta: constraints = [ models.CheckConstraint( name="%(app_label)s_%(class)s_status_not_valid", check=models.Q(status__in=EventStatus.values), ), models.CheckConstraint( name="%(app_label)s_%(class)s_end_date_not_gt_then_start_date", check=models.Q(end_date__gt=models.F("start_date")), ), ]
CheckConstraint sınıfı zorunlu olarak 2 parametre almalıdır. İlki check, bir Q nesnesi kabul ederek, Model.objects.filter () ‘e ileteceğimiz tek bir ifadeyi temsil eder. Model üzerinde her türlü aramayı, alanlar arasındaki karşılaştırmaları ve veritabanı işlevlerini içerebilir.
İkinci ise name, yani isim 😛 Her constraint mutlaka benzersiz bir isme sahip olmalıdır, ve genellikle kısıtlama ile alakalı kullanıcı dostu bir mesaj içerir. app_label ve class değişkenleri ile yine spesifik isimler türetebilirsiniz ve genelde ayrışması için bu şekilde kullanılır.
Tamam gözüküyor, gerekli migrationu oluşturalım.
from django.db import migrations, models import django.db.models.expressions class Migration(migrations.Migration): dependencies = [ ('your_app', '0001_initial'), ] operations = [ migrations.AddConstraint( model_name="event", constraint=models.CheckConstraint( check=models.Q(status__in=["public", "private"]), name="your_app_event_status_not_valid", ), ), migrations.AddConstraint( model_name="event", constraint=models.CheckConstraint( check=models.Q( end_date__gt=django.db.models.expressions.F("start_date") ), name="your_app_event_end_date_not_gt_then_start_date", ), ), ]
Buna yakın bir çıktı elde etmiş olmalısınız. Gerekli migration oluşturuldu fakat bu hali ile bunu işleyemeyiz. Sebebi ise eklenen kısıtlamalara uymayan datanın şu anda veritabanında bulunuyor olması. Data migration yazmamak için shellden tüm kayıtları silelim.
In [1]: Event.objects.all().delete() Out[1]: (1, {'your_app.Event': 1})
Tabi siz data migration yazmak ve hakkında okumak isterseniz. Buyurun 🙂
migrate komutunu çalıştıralım ve test edelim.
# your_app/tests.py from django.db import IntegrityError from django.test import TestCase from django.utils import timezone from your_app.models import Event, EventStatus class EventConstraintsTestCase(TestCase): def test_create_event_with_unexpected_status(self): with self.assertRaisesMessage( IntegrityError, f"{Event._meta.app_label}_{Event._meta.model_name}_status_not_valid", ): start_date = timezone.now() end_date = timezone.now() Event.objects.create( name="test", start_date=start_date, end_date=end_date, status="unexpected", ) def test_create_event_with_invalid_dates(self): with self.assertRaisesMessage( IntegrityError, f"{Event._meta.app_label}_{Event._meta.model_name}_end_date_not_gt_then_start_date", ): end_date = timezone.now() start_date = timezone.now() + timezone.timedelta(days=10) Event.objects.create( name="test", end_date=end_date, start_date=start_date, status=EventStatus.PUBLIC, )
Gayet güzel!
Sonuç.
Constraintler ile kayıt altında tutacağımız verinin doğruluğunu en low levelde kontrol ederek, olası developer ya da uygulamayı kullanan servisin yanlış veriyi içeriye almasının önüne geçiyor ve güvenirliği arttırıyoruz.
Burada 2 case inceledik fakat genel olarak artık nasıl bir constraint tanımlayabileceğiniz ve ne işe yaradığı hakkında bilgi sahibisiniz.
Daha fazla bilgi için şuradan dökümantasyonu,
Unutmadan Django şu anda tanımladığınız kısıtlamaları kullanarak, otomatik validasyonlar yapmıyor fakat bu yeteneği kazandırabilmek için açık bir ticket var. Gün gelir bu feature çıkılırsa bende yazıyı güncelliyor olacağım.
Keyifli ve öğretici bir yazı olmuştur umarım. Görüşmek üzere!
İlk Yorumu Siz Yapın