Skip to content

Commit

Permalink
Update dataclass.md
Browse files Browse the repository at this point in the history
  • Loading branch information
simolus3 committed Oct 30, 2024
1 parent d94b243 commit 3feef7e
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 92 deletions.
4 changes: 1 addition & 3 deletions docs/docs/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,4 @@
/docs/platforms/postgres/ /Platforms/postgres/
/docs/advanced-features/daos/ /dart_api/daos/
/api/* https://pub.dev/documentation/drift/latest/index.html
https://drift.simonbinder.eu/custom_row_classes/ /dart_api/dataclass/
https://drift.simonbinder.eu/type_converters/ /dart_api/tables/#custom-types

custom_row_classes/ /dart_api/dataclass/#custom-dataclass
235 changes: 168 additions & 67 deletions docs/docs/dart_api/dataclass.md
Original file line number Diff line number Diff line change
@@ -1,106 +1,147 @@
---
title: Table classes
description: An overview over the classes drift generates to read from and write to database tables.
---

title: Dataclass
description: Dataclass for reading and writing data to the database.
# Generated table classes

---
For each table you define, drift generates two associated classes:

1. A __row class__: This class represents a full row of the table. Drift automatically returns instances of these classes for queries on tables, allowing you to access rows with type safety.
2. A __companion class__: While row classes represent a full row as it appears in the database, sometimes you also need a partial row (e.g. for updates or inserts which don't have values
for auto-incrementing primary keys yet). For this, drift generates a companion class primarily used for inserts and updates.

Drift's row classes come with built-in equality, hashing, and basic serialization support. They also include a `copyWith` method for easy modification.

# Generated Dataclass
## Example

Drift generates a dataclass for each table in your database. These dataclasses represent query results and come with built-in equality, hashing, and serialization support. They also include a `copyWith` method for easy modification.
A simple table to store usernames shows how the generated row and companion classes behave:

**Example:**
{{ load_snippet('table','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}

For a `Users` table, Drift automatically generates a `User` dataclass. This dataclass is used for all read operations from the `Users` table, ensuring type-safe and structured data retrieval.
For this table, drift generates a `User` class which roughly looks like this (with a few
additional convenience methods now shown here):

{{ load_snippet('generated-dataclass','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}
```dart
// Simplified version of the row class generated by drift:
class User {
final int id;
final String username;
## Dataclass Name
const User({required this.id, required this.username});
The dataclass name is derived from the table name.
@override
String toString() {
// ...
}
- If the name ends in `s`, the dataclass name will be the name with `s` removed.
- Example: `Users` -> `User`
- Otherwise, the dataclass name will be the name with `Data` appended.
- Example: `UserInfo` -> `UserInfoData`
@override
int get hashCode => Object.hash(id, username);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is User && other.id == this.id && other.username == this.username);
}
```

To use a custom name use the `@DataClassName` annotation.
Note that `User.id` is a non-nullable field, reflecting that the column is also non-nullable
in the database.
When you're inserting a new `User` however, there's no value you could provide to `id` because the
actual value is determined by the database. For this reason, drift also has companion classes to
represent partial rows:

**Example:**
```dart
class UsersCompanion extends UpdateCompanion<User> {
final Value<int> id;
final Value<String> username;
{{ load_snippet('data-class-name','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}
const UsersCompanion({
this.id = const Value.absent(),
this.username = const Value.absent(),
});
## Json serialization
UsersCompanion.insert({
this.id = const Value.absent(),
required String username,
}) : username = Value(username);
### Key names
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
// ...
}
}
```

When serializing to json, the generated dataclass will use the column name in `snake_case` for the json keys.
### Using row classes

**Example:**
With the two generated classes, database rows can be created and read in a type-safe and structured way:

{{ load_snippet('default-json-keys','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}
=== "Inserts (Manager)"

```json
{
"id": 1,
"title": "Todo 1",
"created_at": "2024-02-29T12:00:00Z"
}
```
{{ load_snippet('simple-inserts-manager','lib/snippets/dart_api/dataclass.dart.excerpt.json', indent=4) }}

### Custom json keys
Note that the manager API doesn't use companions and instead has `required` columns as
`required` parameters on the function used to create new rows.

To use a custom name for JSON serialization, use the `@JsonKey` annotation.
Note that the `@JsonKey` class from `package:drift` is not same as the `@JsonKey` annotation from `package:json_annotation`, and the two are not compatible with each other.
=== "Inserts (Core)"

**Example:**
The special `UsersCompanion.insert` constructor has required parameters for all columns that don't
have a default in the database:

{{ load_snippet('custom-json-keys','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}
{{ load_snippet('simple-inserts-core','lib/snippets/dart_api/dataclass.dart.excerpt.json', indent=4) }}

```json
{
"id": 1,
"title": "Todo 1",
"created": "2024-02-29T12:00:00Z"
}
```
=== "Query (Manager)"

If you prefer to use the actual column name in SQL as the JSON key, set `use_sql_column_name_as_json_key` to `true` in the `build.yaml` file.
{{ load_snippet('simple-select-manager','lib/snippets/dart_api/dataclass.dart.excerpt.json', indent=4) }}

```yaml title="build.yaml"
targets:
$default:
builders:
drift_dev:
options:
use_sql_column_name_as_json_key : true
```
For more details on customizing column names in SQL, refer to the [column name](tables.md#column-names) documentation.
=== "Query (Core)"

## Companions
{{ load_snippet('simple-select-core','lib/snippets/dart_api/dataclass.dart.excerpt.json', indent=4) }}

In addition to the generated dataclass representing a complete row, Drift also generates a companion object for each table, which represents a partial row and can be used to update existing rows.
## Dataclass Name

<div class="annotate" markdown>
By default, the dataclass name is derived from the table name.

- If the name ends in `s`, the dataclass name will be the name with `s` removed.
- Example: `Users` -> `User`
- Otherwise, the dataclass name will be the name with `Data` appended.
- Example: `UserInfo` -> `UserInfoData`

{{ load_snippet('generated-companion','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}
To make drift use a different name, use the `@DataClassName` annotation:

</div>
1. `o()` is just a helper function that creates a `UsersCompanion`.
{{ load_snippet('data-class-name','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}

## Companions and `Value`

Companion classes are used to represent a _partial_ row where not all columns are present.
This class is introduced for two reasons:

1. Dart has a null-safe type system: If we only had a single row class, all values generated by the
database (like `autoIncrement()` columns) would have to be nullable when creating new rows.
That would make the class unfit for queries though.
2. We need a distinction between `NULL` (in SQL) and absent: For updates, setting a column to
`NULL` is not the same thing as not changing it at all. There's only one `null` in Dart though,
so we need a different structure.

### Value object
When using the companion object to update a row, optional fields must be wrapped in a `Value` object. This is used by Drift to distinguish between `null` and not present values.
To solve this problem, companions represent partial rows by using Drift's `Value` class.
`Value`s store a value (which can be nullable) or explicitly indicate that a value is _absent_:

<div class="annotate" markdown>
{{ load_snippet('generated-value','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}
</div>
1. Since the `id` is `autoIncrement()`, the database will pick a value for us and no value
is provided explicitly. Since `Value.absent()` is also the default, this could be omitted.
2. To simplify the common scenarios of inserts, drift generates a `.insert()` constructor
on companions that avoids `Value` wrappers where they are not required.
This insert could be written as `UsersCompanion.insert(username: 'user')`

## Custom dataclass

The generated dataclass works well for most cases, but you might want to use your own class as a dataclass for a table.

For instance, you might want to add a mixin, let it extend another class or interface, or use other builders like `json_serializable` to customize how it gets serialized to json.
The generated dataclass works well for most cases, but you might want to use your own class to
represent rows.
This can be useful when these classes should extend, implement or mix-in other classes, or if you
want to apply other builders like `json_serializable` too.

!!! note "Row Class"

Expand All @@ -111,11 +152,11 @@ To use a custom row class, simply annotate your table definition with `@UseRowCl

{{ load_snippet('start','lib/snippets/custom_row_classes/default.dart.excerpt.json','lib/snippets/custom_row_classes/named.dart.excerpt.json') }}

A row class must adhere to the following requirements:
In the default configuration, row classes must adhere to the following requirements:

- It must have an unnamed constructor
- They must have an unnamed constructor.
- Each constructor argument must have the name of a drift column
(matching the getter name in the table definition)
(matching the getter name in the table definition).
- The type of a constructor argument must be equal to the type of a column,
including nullability and applied type converters.

Expand All @@ -133,7 +174,6 @@ By default, drift will use the default, unnamed constructor to map a row to the
If you want to use another constructor, set the `constructor` parameter on the
`@UseRowClass` annotation:


{{ load_snippet('named','lib/snippets/custom_row_classes/default.dart.excerpt.json','lib/snippets/custom_row_classes/named.dart.excerpt.json') }}

### Custom companions
Expand Down Expand Up @@ -183,7 +223,7 @@ class User {

### Custom dataclass in drift files

To use existing row classes in drift files, use the `WITH` keyword at the end of the
To use existing row classes in [drift files](../sql_api/index.md), use the `WITH` keyword at the end of the
table declaration. Also, don't forget to import the Dart file declaring the row
class into the drift file.

Expand Down Expand Up @@ -347,3 +387,64 @@ If you have questions about existing result classes, or think you have found an
properly handled, please [start a discussion](https://github.com/simolus3/drift/discussions/new) in
the drift repository, thanks!


## JSON serialization

Generated row classes can be converted from and to JSON:

!!! warning "Drift serialization status"

Serialization has been added to drift in a very early version with an unfortunate design that
can only cover simple serialization needs. Advanced JSON options that dedicated packages like
`json_serializable`, `built_value` or `freezed` offer are superior to drift's serialization
capabilities.

Drift implementing serialization capabilities violates separation of concerns, and this feature
will not be expanded.
A better approach is to write your own [custom row classes](#custom-dataclass) and apply another
JSON serialization builder on them.

{{ load_snippet('from-json','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}

### Key names

By default, drift uses column names in `snake_case` as JSON keys:

{{ load_snippet('default-json-keys','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}

```json
{
"id": 1,
"title": "Todo 1",
"created_at": "2024-02-29T12:00:00Z"
}
```

#### Custom json keys

To use a custom name for JSON serialization, use the `@JsonKey` annotation.
Note that the `@JsonKey` class from `package:drift` is not same as the `@JsonKey` annotation from `package:json_annotation`, and the two are not compatible with each other.

##### Example

{{ load_snippet('custom-json-keys','lib/snippets/dart_api/dataclass.dart.excerpt.json') }}

```json
{
"id": 1,
"title": "Todo 1",
"created": "2024-02-29T12:00:00Z"
}
```

If you prefer to use the actual column name in SQL as the JSON key, set `use_sql_column_name_as_json_key` to `true` in the `build.yaml` file.

```yaml title="build.yaml"
targets:
$default:
builders:
drift_dev:
options:
use_sql_column_name_as_json_key : true
```
For more details on customizing column names in SQL, refer to the [column name](tables.md#changing-sql-names) documentation.
4 changes: 3 additions & 1 deletion docs/docs/dart_api/tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,9 @@ By default, Drift translates Dart getter names to `snake_case` to determine the
name of a column to use in SQL.
For example, a column named `createdAt` in Dart would be named `created_at` in the
`CREATE TABLE` statement issued by drift.
By using `named()`, you can set the name of the column explicitly.
By using `named()`, you can set the name of the column explicitly:

{{ load_snippet('named_column','lib/snippets/dart_api/tables.dart.excerpt.json') }}

??? note "Only need alternative casing?"
If you're only using `named()` to change the casing of the column used by
Expand Down
Loading

0 comments on commit 3feef7e

Please sign in to comment.