Proposal: onDelete and onUpdate relation properties
See original GitHub issuePrisma on delete and on update behavior
A fundamental feature of Prisma is expressing relations between models. A relation can express many different concepts and can have many different meanings depending on the application domain: inclusion, association, dependence, state, etc. They are an essential tool of relational data model design.
As such, there is no single right answer to the question of what should happen when one side of a relation is changed or deleted. This issue is a concrete proposal to enable that functionality and the result of multiple internal design discussions. Prisma Schema Language (PSL) syntax and client/migration/introspection behavior to consider will be outlined.
Please note that this proposal is only valid for SQL connectors for now. The behavior for other connectors will be considered separately in the future.
We are looking forward to any feedback on this proposal.
PSL Syntax Proposal
Without further ado, the following is an example of the syntax the internal working group settled on:
model User {
id String @id
posts Post[]
}
model Post {
id String @id
author_id String
author User @relation(fields: [author_id], onDelete: Cascade, onUpdate: Cascade)
}
A note on terminology:
- “Relation scalar fields”: The fields that store the relation between two records, ie. the fields defined in
fields
of@relation
. For 1-m and 1-1 relations, this is the fields in thefields
property of@relation
. In the example above, this would beauthor_id
. M-n relations do not have relation scalar fields in the same sense as other relations, they only have their IDs (on both sides) as “referenced scalar fields” due to the underlying join table (see below). - “Referenced scalar fields”: The fields that the relation scalar fields reference, ie. the field defined in
references
of@relation
. For m-n relations, it’s currently always the IDs of both models (which are stored as tuples in an implicit join table).
The semantics of onDelete
and onUpdate
are almost exactly how SQL expresses on update and on delete. For the example above: If the related author (User
) of a Post
is deleted (onDelete
), delete all Post
rows that are related to the deleted User
(Cascade
). If the id
field of the related User
is updated, also update author_id
of all Post
s that references that User
.
Possible keywords for onDelete
and onUpdate
are:
Cascade
: Deletes record if dependent record is deleted. Updates relation scalar fields if referenced scalar fields of the dependent record are updated.Restrict
: Prevents operation (both updates and deletes) from succeeding if any records are connected. This behavior will always result in a runtime error for required relations.NoAction
: Behavior is database specific. Either defers throwing an integrity check error until the end of the transaction or errors immediately. If deferred, this makes it possible to temporarily violate integrity in a transaction while making sure that subsequent operations in the transaction restore integrity.SetNull
: Sets relation scalar fields tonull
if the relation is deleted or updated. This will always result in a runtime error if one or more of the relation scalar fields are required.SetDefault
: Sets relation scalar fields to their default values on update or delete of relation. Will always result in a runtime error if no defaults are provided for any relation scalar fields.
Note that the availability of these keywords and the exact behavior depends on the provider. For example, SQL Server does not support Restrict
, but the semantics of NoAction
are identical to Restrict
.
Limitation: Implicit Many-to-many
Prisma offers an implicit many-to-many relation syntax, for example:
model User {
id String @id
posts Post[]
}
model Post {
id String @id
authors User[]
}
Internally, this maps to a join table with two foreign keys. Providing onDelete
/onUpdate
behavior for m-n relations would be inconsistent with 1-1 or 1-m relations:
- The definition would be inverted compared to other relation types.
- Allowing
onDelete
/onUpdate
on m-n misleads people into thinking that related records are deleted, when in fact they are only disconnected.
Elaborating on the first point, this is an example of an m-n relation written out with an explicit join table and onDelete
:
model User {
id String @id
posts JoinTable[]
}
model JoinTable {
post_id String
user_id String
post Post @relation(fields: [post_id], references: [id], onDelete: Cascade)
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@id([post_id, user_id])
}
model Post {
id String @id
authors JoinTable[]
}
To be consistent with other relations, the definition of delete or update behavior must happen on the join table as this is the place where the relations are actually defined and follow the logic of “if x happens with the related record, then something happens with this record”. This leads into the second point, which is that having the syntax on implicit m-n would suggest different behavior to what it does. Let’s write out a hypothetical onDelete
example for implicit m-n:
model User {
id String @id
posts Post[] @relation(onDelete: Cascade)
}
model Post {
id String @id
authors User[] @relation(onDelete: Cascade)
}
To have consistent semantics with the other relations types, it would be expected that if a query deletes either a User or a Post record, dependent other records would be deleted. However, under the hood, because of the hidden join table, they would be disconnected, as the foreign keys are actually defined on the join table and only the join table records would be deleted. Even more, the construct has unclear implications, even if it would behave as expected - a Post may have many authors, deleting a post that triggers a user delete would in turn trigger deletes of more users, triggering more deletes of posts, …
In conclusion, we feel that consistency of the schema is more important than expanding the Prisma-specific construct of implicit m-n relations. With this proposal, we will require users that want to customize their join table behavior to use explicit join tables. We understand that this will cause inconveniences, but we have m-n relation changes planned in the future that will greatly improve the query ergonomics of explicit join tables.
Emulation
For connectors without referential action support (e.g. MongoDB), we want to provide an emulated subset of actions for onDelete
and onUpdate
. The emulated versions try to be as close as possible to the semantics of those provided by the other connectors:
Cascade
: UnchangedRestrict
: UnchangedSetNull
: Unchanged.SetDefault
: Unchanged.NoAction
: Signals that nothing will be done by Prisma. Will result in inconsistent data if not taken care of by the application.
We will gradually introduce these, the initial versions for MongoDB and co. may only support a few.
Defaults
This section discusses the current and desired default for onDelete
/onUpdate
for Prisma tools if the behavior is not specified explicitly in the Prisma schema. Note that the Prisma client emulates database behavior on the application layer, due to the fact that it can’t know what the database behavior is without an annotation in the schema, so the current implementation chose to emulate sensible defaults.
Required relations:
When | Migrate | Client |
---|---|---|
onUpdate |
Cascade |
Cascade (assumed) |
onDelete |
Cascade |
Restrict (emulated) |
Optional relations:
When | Migrate | Client |
---|---|---|
onUpdate |
Cascade |
Cascade (assumed) |
onDelete |
SetNull |
SetNull (emulated) |
Note for Migrate and many-to-many relations: M-n is set to Cascade
for both deletes and updates on the join tables, except for many-to-many self-relations on SQL Server, where we will use NoAction
to avoid cycles.
Database Management Systems (DBMS): All SQL DBMS Prisma supports have different defaults (if one creates schemas manually in SQL) than the ones Prisma currently uses:
onUpdate
:NoAction
(Restrict
for MySQL)onDelete
:NoAction
(Restrict
for MySQL)
The Prisma schema follows the principle of optional complexity, which means that we strive to provide sensible defaults if a property is not defined in the schema. The above raises the question which defaults we want to follow when introducing onDelete
and onUpdate
.
We settled on the current Prisma defaults for referential actions (connectors without direct support emulate these):
When | Optional Relation | Required Relation |
---|---|---|
onUpdate |
Cascade |
Cascade |
onDelete |
SetNull |
Restrict |
We believe this is the best compromise to offer a smooth path forward for the following reasons:
- It’s more defensive about deleting data (ie. no delete cascade by default), reducing potential data loss errors.
- It unifies migrate and client behavior. Users of migrate will see drift in their schema after update, which is easy to fix either via reintrospecting their database to keep the current behavior or accepting the change.
- We keep the default query behavior as close as possible to the current state for developers already accustomed to how Prisma handles relations.
Concrete Feedback Questions
- If you’re not accustomed to how SQL handles onX behavior: Do you feel that you can easily understand the meaning of
@relation(onDelete: ..., onUpdate: ...)
? If not, would you prefer a naming scheme like@relation(onDeleteOther: ...)
or@relation(onDeleteUser: ...)
? - Do you feel that the suggested default behavior aligns with your expectations?
Issue Analytics
- State:
- Created 2 years ago
- Reactions:96
- Comments:15 (10 by maintainers)
Top GitHub Comments
Proposal implemented in https://github.com/prisma/prisma-engines/pull/1947
Instructions on how to use this, and a place to give feedback: https://github.com/prisma/prisma/issues/7816
For required relations, my preference would be to align the default Migrate behaviour with the default at the database level (RESTRICT). One big downside is that it would be a breaking change.