mirror of
https://github.com/django/django.git
synced 2025-08-04 02:48:35 +00:00
Fixed #29824 -- Added support for database exclusion constraints on PostgreSQL.
Thanks to Nick Pope and Mariusz Felisiak for review. Co-Authored-By: Mariusz Felisiak <felisiak.mariusz@gmail.com>
This commit is contained in:
parent
7174cf0b00
commit
a3417282ac
9 changed files with 589 additions and 4 deletions
151
docs/ref/contrib/postgres/constraints.txt
Normal file
151
docs/ref/contrib/postgres/constraints.txt
Normal file
|
@ -0,0 +1,151 @@
|
|||
========================================
|
||||
PostgreSQL specific database constraints
|
||||
========================================
|
||||
|
||||
.. module:: django.contrib.postgres.constraints
|
||||
:synopsis: PostgreSQL specific database constraint
|
||||
|
||||
PostgreSQL supports additional data integrity constraints available from the
|
||||
``django.contrib.postgres.constraints`` module. They are added in the model
|
||||
:attr:`Meta.constraints <django.db.models.Options.constraints>` option.
|
||||
|
||||
``ExclusionConstraint``
|
||||
=======================
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
.. class:: ExclusionConstraint(*, name, expressions, index_type=None, condition=None)
|
||||
|
||||
Creates an exclusion constraint in the database. Internally, PostgreSQL
|
||||
implements exclusion constraints using indexes. The default index type is
|
||||
`GiST <https://www.postgresql.org/docs/current/gist.html>`_. To use them,
|
||||
you need to activate the `btree_gist extension
|
||||
<https://www.postgresql.org/docs/current/btree-gist.html>`_ on PostgreSQL.
|
||||
You can install it using the
|
||||
:class:`~django.contrib.postgres.operations.BtreeGistExtension` migration
|
||||
operation.
|
||||
|
||||
If you attempt to insert a new row that conflicts with an existing row, an
|
||||
:exc:`~django.db.IntegrityError` is raised. Similarly, when update
|
||||
conflicts with an existing row.
|
||||
|
||||
``name``
|
||||
--------
|
||||
|
||||
.. attribute:: ExclusionConstraint.name
|
||||
|
||||
The name of the constraint.
|
||||
|
||||
``expressions``
|
||||
---------------
|
||||
|
||||
.. attribute:: ExclusionConstraint.expressions
|
||||
|
||||
An iterable of 2-tuples. The first element is an expression or string. The
|
||||
second element is a SQL operator represented as a string. To avoid typos, you
|
||||
may use :class:`~django.contrib.postgres.fields.RangeOperators` which maps the
|
||||
operators with strings. For example::
|
||||
|
||||
expressions=[
|
||||
('timespan', RangeOperators.ADJACENT_TO),
|
||||
(F('room'), RangeOperators.EQUAL),
|
||||
]
|
||||
|
||||
.. admonition:: Restrictions on operators.
|
||||
|
||||
Only commutative operators can be used in exclusion constraints.
|
||||
|
||||
``index_type``
|
||||
--------------
|
||||
|
||||
.. attribute:: ExclusionConstraint.index_type
|
||||
|
||||
The index type of the constraint. Accepted values are ``GIST`` or ``SPGIST``.
|
||||
Matching is case insensitive. If not provided, the default index type is
|
||||
``GIST``.
|
||||
|
||||
``condition``
|
||||
-------------
|
||||
|
||||
.. attribute:: ExclusionConstraint.condition
|
||||
|
||||
A :class:`~django.db.models.Q` object that specifies the condition to restrict
|
||||
a constraint to a subset of rows. For example,
|
||||
``condition=Q(cancelled=False)``.
|
||||
|
||||
These conditions have the same database restrictions as
|
||||
:attr:`django.db.models.Index.condition`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The following example restricts overlapping reservations in the same room, not
|
||||
taking canceled reservations into account::
|
||||
|
||||
from django.contrib.postgres.constraints import ExclusionConstraint
|
||||
from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
class Room(models.Model):
|
||||
number = models.IntegerField()
|
||||
|
||||
|
||||
class Reservation(models.Model):
|
||||
room = models.ForeignKey('Room', on_delete=models.CASCADE)
|
||||
timespan = DateTimeRangeField()
|
||||
cancelled = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
ExclusionConstraint(
|
||||
name='exclude_overlapping_reservations',
|
||||
expressions=[
|
||||
('timespan', RangeOperators.OVERLAPS),
|
||||
('room', RangeOperators.EQUAL),
|
||||
],
|
||||
condition=Q(cancelled=False),
|
||||
),
|
||||
]
|
||||
|
||||
In case your model defines a range using two fields, instead of the native
|
||||
PostgreSQL range types, you should write an expression that uses the equivalent
|
||||
function (e.g. ``TsTzRange()``), and use the delimiters for the field. Most
|
||||
often, the delimiters will be ``'[)'``, meaning that the lower bound is
|
||||
inclusive and the upper bound is exclusive. You may use the
|
||||
:class:`~django.contrib.postgres.fields.RangeBoundary` that provides an
|
||||
expression mapping for the `range boundaries <https://www.postgresql.org/docs/
|
||||
current/rangetypes.html#RANGETYPES-INCLUSIVITY>`_. For example::
|
||||
|
||||
from django.contrib.postgres.constraints import ExclusionConstraint
|
||||
from django.contrib.postgres.fields import (
|
||||
DateTimeRangeField,
|
||||
RangeBoundary,
|
||||
RangeOperators,
|
||||
)
|
||||
from django.db import models
|
||||
from django.db.models import Func, Q
|
||||
|
||||
|
||||
class TsTzRange(Func):
|
||||
function = 'TSTZRANGE'
|
||||
output_field = DateTimeRangeField()
|
||||
|
||||
|
||||
class Reservation(models.Model):
|
||||
room = models.ForeignKey('Room', on_delete=models.CASCADE)
|
||||
start = models.DateTimeField()
|
||||
end = models.DateTimeField()
|
||||
cancelled = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
ExclusionConstraint(
|
||||
name='exclude_overlapping_reservations',
|
||||
expressions=(
|
||||
(TsTzRange('start', 'end', RangeBoundary()), RangeOperators.OVERLAPS),
|
||||
('room', RangeOperators.EQUAL),
|
||||
),
|
||||
condition=Q(cancelled=False),
|
||||
),
|
||||
]
|
Loading…
Add table
Add a link
Reference in a new issue