Implementing Django 2.2 database constraint across multiple columns

时间秒杀一切 提交于 2021-02-10 14:23:03

问题


I have a Django model with start date/time and end date/time where all four components may (independently) be a null value (and there is a semantic difference between a null/unknown value and a known value). I am trying to implement a database constraint [1, 2] to check that if they are non-null that the start date/time is before the end date/time.

I have implemented the constraint in two different ways (commented as Option 1, a single constraint, and Option 2, as two constraints) below:

from django.db import models

class Event( models.Model ):
  start_date = models.DateField( blank = True, null = True )
  start_time = models.TimeField( blank = True, null = True )
  end_date   = models.DateField( blank = True, null = True )
  end_time   = models.TimeField( blank = True, null = True )

  class Meta:
    constraints = [
# Option 1
      models.CheckConstraint(
        check = ( models.Q( start_date__isnull = True )
                | models.Q( end_date__isnull = True )
                | models.Q( start_date__lt = models.F( 'end_date' ) )
                | ( ( models.Q( start_time__isnull = True )
                    | models.Q( end_time__isnull = True )
                    | models.Q( start_time__lte = models.F( 'end_time' ) )
                    )
                    & models.Q( start_date = models.F( 'end_date' ) ) # This line
                  )
                ),
        name  = 'start_date_and_time_lte_end_date_and_time'
      ),

# Option 2
      models.CheckConstraint(
        check = ( models.Q( start_date__isnull = True )
                | models.Q( end_date__isnull = True )
                | models.Q( start_date__lte = models.F( 'end_date' ) )
                ),
        name  = 'start_date_lte_end_date'
      ),
      models.CheckConstraint(
        check = ~( models.Q( start_date = models.F( 'end_date' ) )
                 & models.Q( start_time_gt = models.F( 'end_time' ) )
                 ),
        name  = 'not_start_date_eq_end_date_and_start_time_gt_end_time'
      ),
    ]

When I run makemigrations both options succeed.

With Option 1, when I try to use the model in a test:

class EventModelTest( TestCase ):
  def test_simple(self):
    obj = Event.objects.create()
    self.assertTrue( isinstance( obj, Event ) )

I get the error:

django.db.utils.DatabaseError: malformed database schema (packagename_event) - no such column: new_packagename_event.start_time

This error goes away if I comment out the line marked # this line (but doing that would make the constraint function incorrectly).

Option 2 appears to work perfectly but is less obvious that it is going to consider null values correctly.

  1. Are the check constraints in option 1 and option 2 equivalent?
  2. Why does option 1 fail and what can be done to fix it? Is it because I am trying to compare the value of the same column(s) in two places in the same constraint or is there another reason?

回答1:


Found a 3rd way to implement the constraints so that it is more obvious that NULL values are being considered correctly using two constraints:

from django.db import models

class Event( models.Model ):
  start_date = models.DateField( blank = True, null = True )
  start_time = models.TimeField( blank = True, null = True )
  end_date   = models.DateField( blank = True, null = True )
  end_time   = models.TimeField( blank = True, null = True )

  class Meta:
    constraints = [
# Option 3
      models.CheckConstraint(
        check = ( models.Q( start_date__isnull = True )
                | models.Q( end_date__isnull = True )
                | models.Q( start_date__lte = models.F( 'end_date' ) )
                ),
        name  = 'start_date_lte_end_date'
      ),
      models.CheckConstraint(
        check = ( models.Q( start_date__isnull = True )
                | models.Q( end_date__isnull = True )
                | models.Q( start_date__lt = models.F( 'end_date' ) )
                | models.Q( start_time__isnull = True )
                | models.Q( end_time__isnull = True )
                | models.Q( start_time__lte = models.F( 'end_time' ) )
                ),
        name  = 'not_start_date_eq_end_date_and_start_time_gt_end_time'
      ),
    ]

The two constraints will overlap exactly in the cases when:

  • start_date is null;
  • end_date is null; or
  • start_date < end_date

The remaining ways the checks can pass are when the first constraint is start_date = end_date and the second constraint is:

  • start_time is null;
  • end_time is null; or
  • start_time <= end_time

Which matches all the possible cases in Option 1.


On further testing, the model with constraints below demonstrates the same issue:

class SimpleModel( models.Model ):
  value = models.IntegerField()

  class Meta:
    constraints = [
      models.CheckConstraint(
        check = ( models.Q( value__gte = 0 )
                & ( models.Q( value__gte = 0 )
                  | models.Q( value__gte = 0 ) # this line
                  )
                ),
        name  = "simplemodel_check1"
      ),
      models.CheckConstraint(
        check = ( models.Q( value__gte = 0 )
                & ( models.Q( value__gte = 0 )
                  & models.Q( value__gte = 0 )
                  )
                ),
        name  = "simplemodel_check2"
      ),
      models.CheckConstraint(
        check = ( models.Q( value__gte = 0 ) | models.Q( value__gte = 0 ) ),
        name  = "simplemodel_check3"
      ),
      models.CheckConstraint(
        check = ( models.Q( value__gte = 0 ) & models.Q( value__gte = 0 ) ),
        name  = "simplemodel_check4"
      ),
    ]

The 2nd, 3rd and 4th constraints work without issue but the 1st constraint causes an exception when trying to create an instance of the model with an error:

django.db.utils.DatabaseError: malformed database schema (packagename_event) - no such column: new_packagename_simplemodel.value

It appears to be an issue with the combination of & and |.



来源:https://stackoverflow.com/questions/58541957/implementing-django-2-2-database-constraint-across-multiple-columns

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!