Skip to content

Commit 774dd92

Browse files
docs: Update model inheritance documentation (serverpod#340)
1 parent 80365e1 commit 774dd92

File tree

4 files changed

+194
-79
lines changed

4 files changed

+194
-79
lines changed

docs/06-concepts/02-models.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,185 @@ print(user3.name); // Bob
101101
print(user3.email); // alice@example.com
102102
```
103103

104+
## Inheritance
105+
106+
Serverpod models support inheritance, which allows you to define class hierarchies that share fields between parent and child classes. This simplifies class structures and promotes consistency by avoiding duplicate field definitions. Generated classes will maintain the same type hierarchy as the model files.
107+
108+
:::warning
109+
Adding a new subtype to a class hierarchy may introduce breaking changes for older clients. Ensure client compatibility when expanding class hierarchies to avoid deserialization issues.
110+
:::
111+
112+
### Extending a Class
113+
114+
To inherit from a class, use the `extends` keyword in your model files, as shown below:
115+
116+
```yaml
117+
class: ParentClass
118+
fields:
119+
name: String
120+
```
121+
122+
```yaml
123+
class: ChildClass
124+
extends: ParentClass
125+
fields:
126+
age: int
127+
```
128+
129+
This will generate a class with both `name` and `age` fields.
130+
131+
#### Inheritance on table models
132+
133+
Inheritance can also be used with table models. However, **only one class** in an inheritance hierarchy can have a `table` property defined. The table can be placed at any level in the inheritance chain (top, middle or bottom).
134+
135+
:::info
136+
This is a current limitation due to the parent class implementing the `table` getter and other table-related fields, so classes that `extends` the parent cannot override such properties with different types. This might be lifted with a future implementation of `interface` support for table models.
137+
:::
138+
139+
When a class in the hierarchy has a table, all inherited fields are stored as columns in that table. The `id` field is automatically added to table classes and inherited by child classes. You can customize the `id` type in a parent class, and children will inherit it.
140+
141+
A common use case for inheritance on table models is to have a base class that defines a custom `id` type, audit fields and other common properties that must be present on several table models. Below is an example:
142+
143+
```yaml
144+
class: BaseClass
145+
fields:
146+
id: UuidValue?, defaultPersist=random_v7
147+
createdAt: DateTime, default=now
148+
updatedAt: DateTime, default=now
149+
```
150+
151+
```yaml
152+
class: ChildClass
153+
extends: BaseClass
154+
table: child_table
155+
fields:
156+
name: String
157+
158+
indexes:
159+
created_at_index:
160+
fields: createdAt # Index on inherited field
161+
```
162+
163+
**ServerOnly Inheritance**: If a parent class is marked as `serverOnly`, all child classes must also be marked as `serverOnly`. A non-serverOnly class cannot extend a serverOnly class, but a serverOnly child can extend a non-serverOnly parent.
164+
165+
**Additional Restrictions**:
166+
167+
- You can only extend classes from your own project, not from modules.
168+
- Child classes cannot redefine fields that exist in parent classes.
169+
170+
Indexes can be defined on inherited fields in a child class with a table, and relations work normally with inherited table classes.
171+
172+
### Sealed Classes
173+
174+
In addition to the `extends` keyword, you can also use the `sealed` keyword to create sealed class hierarchies, enabling exhaustive type checking. With sealed classes, the compiler knows all subclasses, ensuring that every possible case is handled when working with the model.
175+
176+
:::info
177+
If a class is sealed, it cannot have a table property. This is because a sealed class is abstract and cannot be instantiated, so it cannot represent a table row.
178+
:::
179+
180+
```yaml
181+
class: ParentClass
182+
sealed: true
183+
fields:
184+
name: String
185+
```
186+
187+
```yaml
188+
class: ChildClass
189+
extends: ParentClass
190+
fields:
191+
age: int
192+
```
193+
194+
This will generate the following classes:
195+
196+
```dart
197+
sealed class ParentClass {
198+
String name;
199+
}
200+
201+
class ChildClass extends ParentClass {
202+
String name;
203+
int age;
204+
}
205+
```
206+
207+
## Polymorphism Support
208+
209+
Serverpod supports polymorphism for models that use inheritance. When you define a class hierarchy you can use parent types as parameters and return types in your endpoints, and Serverpod will automatically serialize and deserialize the correct subtype based on the runtime type.
210+
211+
Below is an example of a polymorphic model hierarchy. The `EmailNotification` and `SMSNotification` classes extend the `Notification` sealed class. Each notification type has its own table and specific fields for delivery. Note that it is not possible to define relations towards the `Notification` class, since it does not have a table.
212+
213+
```yaml
214+
class: Notification
215+
sealed: true
216+
fields:
217+
title: String
218+
message: String
219+
createdAt: DateTime, default=now
220+
sentAt: DateTime?
221+
```
222+
223+
```yaml
224+
class: EmailNotification
225+
extends: Notification
226+
table: email_notification
227+
fields:
228+
recipientEmail: String
229+
subject: String
230+
```
231+
232+
```yaml
233+
class: SMSNotification
234+
extends: Notification
235+
table: sms_notification
236+
fields:
237+
phoneNumber: String
238+
provider: String?
239+
```
240+
241+
### Using Polymorphic Types in Endpoints
242+
243+
Polymorphic types can be used as parameters and return types in endpoint methods and streaming endpoints. The runtime type is preserved through serialization and deserialization:
244+
245+
```dart
246+
class NotificationEndpoint extends Endpoint {
247+
Future<Notification> sendNotification(
248+
Session session, {
249+
required Notification notification,
250+
}) async {
251+
final sentNotification = switch (notification) {
252+
EmailNotification email => await _sendEmail(session, email),
253+
SMSNotification sms => await _sendSMS(session, sms),
254+
};
255+
256+
return sentNotification.copyWith(sentAt: DateTime.now());
257+
}
258+
259+
/// Save to database and send email
260+
Future<EmailNotification> _sendEmail(
261+
Session session,
262+
EmailNotification notification,
263+
) async {
264+
final saved = await EmailNotification.db.insertRow(session, notification);
265+
// ... email sending logic
266+
return saved;
267+
}
268+
269+
/// Save to database and send SMS
270+
Future<SMSNotification> _sendSMS(
271+
Session session,
272+
SMSNotification notification,
273+
) async {
274+
final saved = await SMSNotification.db.insertRow(session, notification);
275+
// ... SMS sending logic
276+
return saved;
277+
}
278+
}
279+
```
280+
281+
Polymorphic types also work seamlessly in Lists, Maps, Sets, Records, and nullable contexts.
282+
104283
## Exception
105284

106285
The Serverpod models supports creating exceptions that can be thrown in endpoints by using the `exception` keyword. For more in-depth description on how to work with exceptions see [Error handling and exceptions](exceptions).
@@ -499,3 +678,5 @@ fields:
499678
| [**default**](#default-values) | Sets the default value for both the model and the database. This keyword cannot be used with **relation**. | ✅ | | |
500679
| [**defaultModel**](#default-values) | Sets the default value for the model side. This keyword cannot be used with **relation**. | ✅ | | |
501680
| [**defaultPersist**](#default-values) | Sets the default value for the database side. This keyword cannot be used with **relation** and **!persist**. | ✅ | | |
681+
| [**extends**](#inheritance) | Specifies a parent class to inherit from. | ✅ | ✅ | |
682+
| [**sealed**](#inheritance) | Boolean flag to create a sealed class hierarchy, enabling exhaustive type checking. | ✅ | | |

docs/06-concepts/21-experimental.md

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -33,72 +33,6 @@ The current options you can pass are:
3333
| **Feature** | Description |
3434
| :-------------- | :---------------------------------------------------------------------------------- |
3535
| **all** | Enables all available experimental features. |
36-
| **inheritance** | Allows using the `extends` keyword in your model files to create class hierarchies. |
37-
38-
## Inheritance
39-
40-
:::warning
41-
Adding a new subtype to a class hierarchy may introduce breaking changes for older clients. Ensure client compatibility when expanding class hierarchies to avoid deserialization issues.
42-
:::
43-
44-
Inheritance allows you to define class hierarchies in your model files by sharing fields between parent and child classes, simplifying class structures and promoting consistency by avoiding duplicate field definitions.
45-
46-
### Extending a Class
47-
48-
To inherit from a class, use the `extends` keyword in your model files, as shown below:
49-
50-
```yaml
51-
class: ParentClass
52-
fields:
53-
name: String
54-
```
55-
56-
```yaml
57-
class: ChildClass
58-
extends: ParentClass
59-
fields:
60-
int: age
61-
```
62-
63-
This will generate a class with both `name` and `age` field.
64-
65-
```dart
66-
class ChildClass extends ParentClass {
67-
String name
68-
int age
69-
}
70-
```
71-
72-
### Sealed Classes
73-
74-
In addition to the `extends` keyword, you can also use the `sealed` keyword to create sealed class hierarchies, enabling exhaustive type checking. With sealed classes, the compiler knows all subclasses, ensuring that every possible case is handled when working with the model.
75-
76-
```yaml
77-
class: ParentClass
78-
sealed: true
79-
fields:
80-
name: String
81-
```
82-
83-
```yaml
84-
class: ChildClass
85-
extends: ParentClass
86-
fields:
87-
age: int
88-
```
89-
90-
This will generate the following classes:
91-
92-
```dart
93-
sealed class ParentClass {
94-
String name;
95-
}
96-
97-
class ChildClass extends ParentClass {
98-
String name;
99-
int age;
100-
}
101-
```
10236

10337
## Exception monitoring
10438

docs/07-deployments/01-deployment-strategy.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ The main two options are running Serverpod on a cluster of servers or on a serve
66

77
Here are some pros and cons for the different options:
88

9-
| | Server cluster | Serverless |
10-
| :--- | :--------| :--------- |
9+
| | Server cluster | Serverless |
10+
| :--- | :---------------------------------------------------------------------------------------- | :------------------------------------------------------------------ |
1111
| Pros | All features are supported. Great for real time communication. Cost efficient at scale. | Minimal starting cost. Easier configuration. Minimal maintenance. |
12-
| Cons | Slightly higher starting cost. More complex to set up. | Limited feature set. The server cannot have a state. |
12+
| Cons | Slightly higher starting cost. More complex to set up. | Limited feature set. The server cannot have a state. |
1313

1414
The features that currently are not supported by the serverless option are:
1515

docs/07-deployments/05-general.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ By default, Serverpod will listen on ports 8080, 8081, and 8082. The ports are u
2626

2727
Serverpod can assume different roles depending on your configuration. If you run Serverpod on a single server or a cluster of servers, you typically will want to use the default `monolith` role. If you use a serverless service, use the `serverless` role. When Serverpod runs as a monolith, it will handle all maintenance tasks, such as health checks and future calls. If you run it serverless, you will need to schedule a cron job to start Serverpod in the `maintenance` role once every minute if you need support for future calls and health checks.
2828

29-
| Role | Function |
30-
| :------------ | :------- |
31-
| `monolith` | Handles incoming connections and maintenance tasks. Allows the server to contain a state. Default role. |
32-
| `serverless` | Only handles incoming connections. |
33-
| `maintenance` | Runs the maintenance tasks once, then exits. |
29+
| Role | Function |
30+
| :------------ | :------------------------------------------------------------------------------------------------------- |
31+
| `monolith` | Handles incoming connections and maintenance tasks. Allows the server to contain a state. Default role. |
32+
| `serverless` | Only handles incoming connections. |
33+
| `maintenance` | Runs the maintenance tasks once, then exits. |
3434

3535
You can specify the role of your server when you launch it by setting the `--role` argument.
3636

@@ -44,9 +44,9 @@ Running Serverpod through a Docker container is often the best option as it prov
4444

4545
You will get a `Dockerfile` created in your server directory when you set up a new project. The file works out of the box but can be tailored to your needs. The file has no build options, but you can define environment variables when running it. The following variables are supported.
4646

47-
| Environment variable | Meaning |
48-
| :------------------- | :------ |
49-
| `runmode` | The run mode to start the server in, possible values are `development` (default), `staging`, or `production`. |
50-
| `serverid` | Identifier of your server, default is `default` |
47+
| Environment variable | Meaning |
48+
| :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------ |
49+
| `runmode` | The run mode to start the server in, possible values are `development` (default), `staging`, or `production`. |
50+
| `serverid` | Identifier of your server, default is `default` |
5151
| `logging` | Logging mode at startup, default is `normal`, but you can specify `verbose` to get more information during startup which can help with debugging. |
52-
| `role` | The role that the server will assume, possible values are `monolith` (default), `serverless`, or `maintenance`. |
52+
| `role` | The role that the server will assume, possible values are `monolith` (default), `serverless`, or `maintenance`. |

0 commit comments

Comments
 (0)