i have the following tables:
tenants
:
- tenantId (UUID)
- invoiceAddress (TEXT)
packages
:
- packageId
- needsInvoice
tenants_and_packages
:
- tenantsAndPackagesId
- tenantId
- packageId
This is a M2N-Mapping between Tenants and Packages.
What is the problem?
I want to achieve the following "Check"-Logic. I want to make sure that when I insert/update a tenant or package, it will be checked whether the tenant needs to have the column invoiceAddress
set or not (it depends from the needsInvoice
column of the Packages-Table.
Now, because sometimes I only edit the tenant, sometimes only the package and sometime both right after each other. But every time it runs in the context of a migration. Therefore, the "check" must be executed right before the transaction would finish (commit). Because, when I want to edit a tenant not to have the invoiceAddress
set, because in the next step (same transaction-context) I want to set the package’s needsInvoice
to false
, this kind of "check" cannot be executed right after the tenant has changed, since it would immediatly abort the transaction.
Thanks in advance! If any help for understanding is required, lmk!
-
Phynae
-
Hence the fact, that constraints do not allow subqueries or any dynamic logic, it won’t work with them as far as I know.
-
Using triggers and functions is also not working since they are executed right before or after the row was inserted/updated.
2
Answers
You could use a deferred constraint trigger:
Such a trigger will run right before the commit of the transaction, just like you want.
But you should be aware that such a trigger will always suffer from race conditions if there are concurrent data modifications, because data modifications by a concurrent statement are not visible in the trigger function.
As detailed in my article, you either need the
SERIALIZABLE
isolation level or locking to serialize the operations if you want such triggers to work reliably.Perhaps you can redesign your data model so that there are no constraints between columns of different tables…
You can achieve the desired behavior using a PL/pgSQL block that encapsulates the logic within a transaction. By using BEGIN, COMMIT, and ROLLBACK, you can control the transaction flow based on whether the needsInvoice condition is met. This approach allows you to perform the necessary checks without the need for triggers.
Example PL/pgSQL Block
Here’s how you can implement this logic in a PL/pgSQL block:
Explanation
Transaction Block:
The BEGIN block starts a transaction. If any part of the transaction fails, it will trigger the EXCEPTION block.
Check Logic:
The code checks the needsInvoice value for the specified package.
If needsInvoice is true, it checks if the invoice_address is set. If not, it raises an exception.
Insert/Update Operations:
You can perform your INSERT or UPDATE operations for the tenants and packages tables as needed.
Commit/Rollback:
If all checks pass and the operations are successful, the transaction is committed.
If any exception occurs (including the raised exception for missing invoice address), the transaction is rolled back to maintain data integrity.
Benefits of This Approach
Control: You have full control over the transaction flow without relying on triggers.
Flexibility: You can easily adapt the logic to handle various scenarios within the same transaction context.
Error Handling: You can customize the error handling and logging as needed.
This approach effectively ensures that the invoiceAddress is set according to the needsInvoice requirement and rolls back the entire transaction if the conditions are not met.