question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Proposal: onDelete and onUpdate relation properties

See original GitHub issue

Prisma 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 the fields property of @relation. In the example above, this would be author_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 Posts 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 to null 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: Unchanged
  • Restrict: Unchanged
  • SetNull: 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:closed
  • Created 2 years ago
  • Reactions:96
  • Comments:15 (10 by maintainers)

github_iconTop GitHub Comments

4reactions
pimeyscommented, Jun 22, 2021

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

3reactions
tomhoulecommented, May 12, 2021

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Cascade Delete - EF Core | Microsoft Learn
Configuring cascading behaviors triggered when an entity is deleted or severed from its principal/parent.
Read more >
Special rules for referential actions in SQL Server and MongoDB
As the relation fields are required, the default referential action for onDelete is NoAction but for onUpdate it is Cascade , which causes...
Read more >
c# - Specify ON DELETE NO ACTION or ON UPDATE NO ...
By proper I mean passing the corresponding navigation property when exists. Failing to do so would lead to unexpected additional relationships / ...
Read more >
Basic Relationships - Propel, The Blazing Fast Open-Source ...
Propel also supports the ON UPDATE and ON DELETE aspect of foreign keys. These properties can be specified in the <foreign-key> tag using...
Read more >
PostgreSQL connector | LoopBack Documentation
Connection Pool Settings; Configuration options; Connecting to UNIX ... The onDelete and onUpdate properties are optional and will default to NO ACTION ....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Hashnode Post

No results found