File: deferrable.md

package info (click to toggle)
python-django-pgtrigger 4.15.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 956 kB
  • sloc: python: 4,412; makefile: 114; sh: 8; sql: 2
file content (96 lines) | stat: -rw-r--r-- 3,788 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# Deferrable Triggers

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`:

```python
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 NULL;
                """
            )
        ]
```

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:

```python
# 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)
```

## Ignoring deferrable triggers

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:

```python
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:

```python
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(...)
```

## Adjusting runtime behavior

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](https://www.postgresql.org/docs/current/sql-set-constraints.html).

[pgtrigger.constraints][] takes the new timing value and (optionally) 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:

```python
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][].