|
| 1 | +## Django's Atomic |
| 2 | +Django's `atomic` ensures database changes are committed together-or-not-at-all. It creates a savepoint or a transaction depending on two factors: |
| 3 | + |
| 4 | +- The arguments passed to it (`durable` and `savepoint`). |
| 5 | +- If a database transaction is already open. |
| 6 | + |
| 7 | +Specifically, the **Behaviours** which `atomic` exhibits are: |
| 8 | + |
| 9 | +| | `durable=False` (default) | `durable=True` | |
| 10 | +| --- | --- | --- | |
| 11 | +| `savepoint=True` (default) | **A**. Begin a transaction if needed. Creates a savepoint if already in a transaction. | **B**. Begin a transaction, or throw an error if one is already open. Never creates a savepoint. (The `savepoint` flag is ignored.) | |
| 12 | +| `savepoint=False` | **C**. Begin a transaction if needed. Never creates a savepoint. | **D**. Same as **B**. | |
| 13 | + |
| 14 | +Uses of `atomic` fall into three broad **Categories**: |
| 15 | + |
| 16 | +1. Create a *transaction* to wrap multiple changes (such as in durable use-cases). |
| 17 | +2. Create a *savepoint* so we can roll back to in order to continue with a transaction after failure. |
| 18 | +3. Changes to be committed *atomically*, but not specific about where the transaction is created, as long as there is one (Such as domain operations.) |
| 19 | + |
| 20 | +## Problems |
| 21 | +1. Django's atomic creates many savepoints that causes significant database-pressure issues. |
| 22 | +2. The majority of the savepoints are never used. There are a couple of main causes: |
| 23 | + 1. We create savepoints with decorators. *Linting for this is possible, but each existing case requires investigation.* |
| 24 | + 2. `atomic` creates savepoints by default. The default arguments (*Behaviour* **A**) are an [attractive nuisance](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) because they make us create savepoints when we don't need them. |
| 25 | + > … if you have two ways to accomplish a task and one is a simple way that *looks* like the right thing but is subtly wrong, and the other is correct but |
| 26 | + more complicated, the majority of people will end up doing the wrong |
| 27 | + thing. |
| 28 | + — [**Attractive nuisances in software design](https://blog.ganssle.io/articles/2023/01/attractive-nuisances.html) -** [Paul Ganssle](https://blog.ganssle.io/author/paul-ganssle.html) |
| 29 | + > |
| 30 | +3. We have no easy way to indicate the creation of a savepoint that doesn't have the potential to create a transaction instead. The only tool we have to create a savepoint is *Behaviour* **A**, which can create a transaction. |
| 31 | + |
| 32 | +## What Subatomic implements |
| 33 | +- `transaction()`. Begin a transaction, or throw an error if a transaction is already open. Just like *Behaviour* **B**. |
| 34 | +- `savepoint()`. Create a savepoint, or throw an error if we're not already in a transaction. This is not in the table of *Behaviours* (the closest we have is *Behaviour* **A**, but that can create transactions). |
| 35 | +- `transaction_if_not_already()`. Begin a transaction if we're not already in one. Just like *Behaviour* **C**. This has a bit of a clunky name. This is deliberate, and reflects that it's a bit of a clunky thing to do. To be used with caution because the creation of a transaction is implicit. For a stricter alternative, see `transaction_required()` below. |
| 36 | +- `transaction_required()`. Throw an error if we're not already in a transaction. Does not create savepoints *or* transactions. *Most likely to be useful in the domain layer, which should not be responsible for the creation of transactions.* |
0 commit comments