From c0857ff4917052b60d9c29fc152b797c01893007 Mon Sep 17 00:00:00 2001 From: Preston Date: Wed, 28 Jan 2026 10:11:14 -0600 Subject: [PATCH 1/4] Added documentation on new jsonKey alias feature. --- docs/06-concepts/02-models.md | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/06-concepts/02-models.md b/docs/06-concepts/02-models.md index cabcabce..2b778635 100644 --- a/docs/06-concepts/02-models.md +++ b/docs/06-concepts/02-models.md @@ -64,6 +64,46 @@ fields: Serverpod's models can easily be saved to or read from the database. You can read more about this in the [Database](database/models) section. ::: +### JSON key aliasing + +By default, fields are serialized to JSON using their Dart field name as the key. The `jsonKey` property allows you to specify a different key name for JSON serialization and deserialization, which is useful when integrating with external APIs that use different naming conventions. + +```yaml +class: User +fields: + displayName: String, jsonKey=display_name + emailAddress: String, jsonKey=email + createdAt: DateTime, jsonKey=created_at +``` + +This generates a class where the Dart field names remain camelCase, but the JSON representation uses the specified keys: + +```dart +// Dart field names +var user = User( + displayName: 'John Doe', + emailAddress: 'john@example.com', + createdAt: DateTime.now(), +); + +// Serializes to JSON with custom keys +// { +// "display_name": "John Doe", +// "email": "john@example.com", +// "created_at": "2024-01-15T10:30:00.000Z" +// } +``` + +This is particularly helpful when: + +- Consuming external APIs that use snake_case or other naming conventions +- Working with legacy systems that have specific JSON field requirements +- Integrating with third-party services like MongoDB (e.g., mapping `id` to `_id`) + +:::info +The `jsonKey` property only affects JSON serialization. It does not affect the database column name. To customize the database column name, use the `column` property instead (see [Database models](database/models)). +::: + ### Immutable classes By default, generated classes in Serverpod are mutable, meaning their fields can be changed after creation. However, you can make a class immutable by setting the `immutable` property to `true`. Immutable classes are especially useful when working with state management solutions or when you need value-based equality. @@ -782,6 +822,7 @@ fields: | [**type (fields)**](#class) | Denotes the data type for a field. | ✅ | ✅ | | | [**required**](#required-fields) | Makes the field as required. This keyword can only be used for **nullable** fields. | ✅ | ✅ | | | [**scope**](#limiting-visibility-of-a-generated-class) | Denotes the scope for a field. | ✅ | | | +| [**jsonKey**](#json-key-aliasing) | Sets a custom key name for JSON serialization and deserialization. | ✅ | | | | [**persist**](database/models) | A boolean flag if the data should be stored in the database or not can be negated with `!persist` | ✅ | | | | [**relation**](database/relations/one-to-one) | Sets a relation between model files, requires a table name to be set. | ✅ | | | | [**name**](database/relations/one-to-one#bidirectional-relations) | Give a name to a relation to pair them. | ✅ | | | From 303301a72f65535ca9c8cd431e25248c7ed7dd27 Mon Sep 17 00:00:00 2001 From: Preston Date: Wed, 28 Jan 2026 10:46:50 -0600 Subject: [PATCH 2/4] Updated experemental documenation link for column name. Added actual DateTime value. --- docs/06-concepts/02-models.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/06-concepts/02-models.md b/docs/06-concepts/02-models.md index 2b778635..d9b5c2cd 100644 --- a/docs/06-concepts/02-models.md +++ b/docs/06-concepts/02-models.md @@ -83,7 +83,7 @@ This generates a class where the Dart field names remain camelCase, but the JSON var user = User( displayName: 'John Doe', emailAddress: 'john@example.com', - createdAt: DateTime.now(), + createdAt: DateTime.parse('2024-01-15T10:30:00.000Z'), ); // Serializes to JSON with custom keys @@ -101,7 +101,7 @@ This is particularly helpful when: - Integrating with third-party services like MongoDB (e.g., mapping `id` to `_id`) :::info -The `jsonKey` property only affects JSON serialization. It does not affect the database column name. To customize the database column name, use the `column` property instead (see [Database models](database/models)). +The `jsonKey` property affects JSON serialization and deserialization. It does not affect the database column name. To customize the database column name, use the `column` property instead. This is an experimental feature; see the [Experimental documentation](experimental#column-name-override) under *Column name override* for details. ::: ### Immutable classes From 0d402651694e830003b1cf2788895943b45ebe5c Mon Sep 17 00:00:00 2001 From: Preston Date: Thu, 12 Feb 2026 22:41:07 -0600 Subject: [PATCH 3/4] Added documentation for database row locking functionality. --- .../06-concepts/06-database/13-row-locking.md | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/06-concepts/06-database/13-row-locking.md diff --git a/docs/06-concepts/06-database/13-row-locking.md b/docs/06-concepts/06-database/13-row-locking.md new file mode 100644 index 00000000..917b36b9 --- /dev/null +++ b/docs/06-concepts/06-database/13-row-locking.md @@ -0,0 +1,113 @@ +# Row locking + +Row-level locking allows you to lock specific rows in the database to prevent other transactions from modifying them while you work. This is essential for safely handling concurrent updates, such as processing payments, managing inventory, or any scenario where two transactions might conflict. + +All row locking operations require a [transaction](transactions). + +For the following examples we will use this model: + +```yaml +class: Company +table: company +fields: + name: String +``` + +## Locking rows with a read + +You can lock rows as part of a read operation by passing the `lockMode` parameter to `find`, `findFirstRow`, or `findById`. The locked rows are returned and held until the transaction completes. + +```dart +await session.db.transaction((transaction) async { + var companies = await Company.db.find( + session, + where: (t) => t.name.equals('Serverpod'), + lockMode: LockMode.forUpdate, + transaction: transaction, + ); + + // Rows are locked — safe to update without conflicts. + for (var company in companies) { + company.name = 'Updated name'; + await Company.db.updateRow(session, company, transaction: transaction); + } +}); +``` + +The `findFirstRow` and `findById` methods also support locking: + +```dart +await session.db.transaction((transaction) async { + var company = await Company.db.findById( + session, + companyId, + lockMode: LockMode.forUpdate, + transaction: transaction, + ); + + if (company != null) { + company.name = 'Updated name'; + await Company.db.updateRow(session, company, transaction: transaction); + } +}); +``` + +## Locking rows without fetching data + +If you only need to lock rows without reading their data, use the `lockRows` method. This acquires locks with less overhead since no row data is transferred. + +```dart +await session.db.transaction((transaction) async { + await Company.db.lockRows( + session, + where: (t) => t.name.equals('Serverpod'), + lockMode: LockMode.forUpdate, + transaction: transaction, + ); + + // Rows are locked — perform updates using other methods. +}); +``` + +## Lock modes + +The `lockMode` parameter determines the type of lock acquired. Different lock modes allow varying levels of concurrent access. + +| Lock Mode | Constant | Description | +|---|---|---| +| For update | `LockMode.forUpdate` | Exclusive lock that blocks all other locks. Use when you intend to update or delete the locked rows. | +| For no key update | `LockMode.forNoKeyUpdate` | Exclusive lock that allows `forKeyShare` locks. Use when updating non-key columns only. | +| For share | `LockMode.forShare` | Shared lock that blocks exclusive locks but allows other shared locks. Use when you need to ensure rows don't change while reading. | +| For key share | `LockMode.forKeyShare` | Weakest lock that only blocks changes to key columns. | + +For a detailed explanation of how lock modes interact, see the [PostgreSQL documentation](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS). + +## Lock behavior + +The `lockBehavior` parameter controls what happens when a requested row is already locked by another transaction. If not specified, the default behavior is to wait. + +| Behavior | Constant | Description | +|---|---|---| +| Wait | `LockBehavior.wait` | Wait until the lock becomes available. This is the default. | +| No wait | `LockBehavior.noWait` | Throw an exception immediately if any row is already locked. | +| Skip locked | `LockBehavior.skipLocked` | Skip rows that are currently locked and return only the unlocked rows. | + +```dart +await session.db.transaction((transaction) async { + var companies = await Company.db.find( + session, + where: (t) => t.id < 100, + lockMode: LockMode.forUpdate, + lockBehavior: LockBehavior.skipLocked, + transaction: transaction, + ); + + // Only unlocked rows are returned. +}); +``` + +:::info + +`LockBehavior.skipLocked` is particularly useful for implementing job queues or work distribution, where multiple workers can each grab unlocked rows without waiting on each other. + +::: From f5cd413dd42d4d5b7e7c46284d7f752a06494f1d Mon Sep 17 00:00:00 2001 From: Preston Date: Fri, 13 Feb 2026 09:14:00 -0600 Subject: [PATCH 4/4] Reverted models addition, applied PR comments. --- docs/06-concepts/02-models.md | 41 ------------------- .../06-concepts/06-database/13-row-locking.md | 10 +++-- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/docs/06-concepts/02-models.md b/docs/06-concepts/02-models.md index d9b5c2cd..cabcabce 100644 --- a/docs/06-concepts/02-models.md +++ b/docs/06-concepts/02-models.md @@ -64,46 +64,6 @@ fields: Serverpod's models can easily be saved to or read from the database. You can read more about this in the [Database](database/models) section. ::: -### JSON key aliasing - -By default, fields are serialized to JSON using their Dart field name as the key. The `jsonKey` property allows you to specify a different key name for JSON serialization and deserialization, which is useful when integrating with external APIs that use different naming conventions. - -```yaml -class: User -fields: - displayName: String, jsonKey=display_name - emailAddress: String, jsonKey=email - createdAt: DateTime, jsonKey=created_at -``` - -This generates a class where the Dart field names remain camelCase, but the JSON representation uses the specified keys: - -```dart -// Dart field names -var user = User( - displayName: 'John Doe', - emailAddress: 'john@example.com', - createdAt: DateTime.parse('2024-01-15T10:30:00.000Z'), -); - -// Serializes to JSON with custom keys -// { -// "display_name": "John Doe", -// "email": "john@example.com", -// "created_at": "2024-01-15T10:30:00.000Z" -// } -``` - -This is particularly helpful when: - -- Consuming external APIs that use snake_case or other naming conventions -- Working with legacy systems that have specific JSON field requirements -- Integrating with third-party services like MongoDB (e.g., mapping `id` to `_id`) - -:::info -The `jsonKey` property affects JSON serialization and deserialization. It does not affect the database column name. To customize the database column name, use the `column` property instead. This is an experimental feature; see the [Experimental documentation](experimental#column-name-override) under *Column name override* for details. -::: - ### Immutable classes By default, generated classes in Serverpod are mutable, meaning their fields can be changed after creation. However, you can make a class immutable by setting the `immutable` property to `true`. Immutable classes are especially useful when working with state management solutions or when you need value-based equality. @@ -822,7 +782,6 @@ fields: | [**type (fields)**](#class) | Denotes the data type for a field. | ✅ | ✅ | | | [**required**](#required-fields) | Makes the field as required. This keyword can only be used for **nullable** fields. | ✅ | ✅ | | | [**scope**](#limiting-visibility-of-a-generated-class) | Denotes the scope for a field. | ✅ | | | -| [**jsonKey**](#json-key-aliasing) | Sets a custom key name for JSON serialization and deserialization. | ✅ | | | | [**persist**](database/models) | A boolean flag if the data should be stored in the database or not can be negated with `!persist` | ✅ | | | | [**relation**](database/relations/one-to-one) | Sets a relation between model files, requires a table name to be set. | ✅ | | | | [**name**](database/relations/one-to-one#bidirectional-relations) | Give a name to a relation to pair them. | ✅ | | | diff --git a/docs/06-concepts/06-database/13-row-locking.md b/docs/06-concepts/06-database/13-row-locking.md index 917b36b9..9a932921 100644 --- a/docs/06-concepts/06-database/13-row-locking.md +++ b/docs/06-concepts/06-database/13-row-locking.md @@ -2,7 +2,9 @@ Row-level locking allows you to lock specific rows in the database to prevent other transactions from modifying them while you work. This is essential for safely handling concurrent updates, such as processing payments, managing inventory, or any scenario where two transactions might conflict. -All row locking operations require a [transaction](transactions). +:::warning +All row locking operations require a [transaction](transactions). An exception will be thrown if you attempt to acquire a lock without one. +::: For the following examples we will use this model: @@ -34,7 +36,9 @@ await session.db.transaction((transaction) async { }); ``` -The `findFirstRow` and `findById` methods also support locking: +When a row is locked, other transactions that attempt to acquire a conflicting lock on the same rows will wait until the lock is released. Regular reads without a `lockMode` are not affected and can still read the rows freely. If waiting is not desired, you can configure the [lock behavior](#lock-behavior) to either throw an exception immediately or skip locked rows. + +The `findFirstRow` and `findById` methods also support locking. Here's an example using `findById`: ```dart await session.db.transaction((transaction) async { @@ -107,7 +111,5 @@ await session.db.transaction((transaction) async { ``` :::info - `LockBehavior.skipLocked` is particularly useful for implementing job queues or work distribution, where multiple workers can each grab unlocked rows without waiting on each other. - :::