Triggers are "deferrable" if their execution can be postponed until the end of the transaction. This behavior can be desirable for certain situations.
For example, here we ensure a Profile
model always exists for every User
:
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
class UserProxy(User):
class Meta:
proxy = True
triggers = [
pgtrigger.Trigger(
name="profile_for_every_user",
when=pgtrigger.After,
operation=pgtrigger.Insert,
timing=pgtrigger.Deferred,
func=f"""
IF NOT EXISTS (SELECT FROM {Profile._meta.db_table} WHERE user_id = NEW.id) THEN
RAISE EXCEPTION 'Profile does not exist for user %', NEW.id;
END IF;
RETURN NEW;
"""
)
]
This trigger ensures that any creation of a User
will fail if a Profile
does not exist. Note that we must create
them both in a transaction:
# This will succeed since the user has a profile when # the transaction completes with transaction.atomic(): user = User.objects.create() Profile.objects.create(user=user) # This will fail since it is not in a transaction user = User.objects.create() Profile.objects.create(user=user)
Deferrable triggers can be ignored, but remember that they execute at the very end of a transaction. If pgtrigger.ignore does not wrap the transaction, the deferrable trigger will not be ignored.
Here is a correct way of ignoring the deferrable trigger from the initial example:
with pgtrigger.ignore("my_app.UserProxy:profile_for_every_user"):
# Use durable=True, otherwise we may be wrapped in a parent
# transaction
with transaction.atomic(durable=True):
# We no longer need a profile for a user...
User.objects.create(...)
Here's an example of code that will fail:
with transaction.atomic():
# This ignore does nothing for this trigger. ``pgtrigger.ignore`` will no longer
# be in effect by the time the trigger runs at the end of the transaction.
with pgtrigger.ignore("my_app.UserProxy:profile_for_every_user"):
# The trigger will raise an exception
User.objects.create(...)
When a deferrable trigger is declared, the timing
attribute can be adjusted at runtime
using pgtrigger.constraints. This function mimics Postgres's SET CONSTRAINTS
statement. Check the Postgres docs for more info.
pgtrigger.constraints takes the new timing value and a list of trigger URIs over which to apply the value. The value is in effect until the end of the transaction.
Let's take our original example. We can set the trigger to immediately run, causing it to throw an error:
with transaction.atomic():
user = User.objects.create(...)
# Make the deferrable trigger fire immediately. This will cause an exception
# because a profile has not yet been created for the user
pgtrigger.constraints(pgtrigger.Immediate, "auth.User:profile_for_every_user")
Keep in mind that the constraint settings stay in effect until the end of the transaction. If a parent transaction wraps our code, timing overrides will persist.
Tip
You can do the opposite of our example, creating triggers with
timing=pgtrigger.Immediate
and deferring their execution dynamically.
Note
In a multi-schema setup, only triggers in the schema search path will be overridden with pgtrigger.constraints.