Custom data types topic
While package:typed_sql
has a limited number of data-types that can be used
in columns when defining tables in a database schema, it is possible to create
custom types that serialize to a database column. This is done by creating a
class that implements CustomDataType<T>
, where T
is one of:
bool
,int
,double
,String
,DateTime
, or,Uint8List
.
Note
A custom types does not give rise to a User Defined Type (UDT) in SQL. Implementations of CustomDataType serves exclusively to facilitate convenient serialization and deserialization of custom types.
Implementing a CustomDataType
Implemeting a CustomDataType<T>
is straight forward, you pick a T
which must
be one of the existing columns types supported by package:typed_sql
. Then you
write a class that implements CustomDataType<T>
and can be deserialized from
T
using a fromDatabase
factory constructor.
Below we have an example of how to implement a Color
as a custom type that
can be stored in a table with package:typed_sql
.
@immutable
final class Color implements CustomDataType<int> {
final int red;
final int green;
final int blue;
Color(this.red, this.green, this.blue);
Color.red() : this(255, 0, 0);
Color.green() : this(0, 255, 0);
Color.blue() : this(0, 0, 255);
/// Factory constructor `fromDatabase(T value)` is required by code-generator!
factory Color.fromDatabase(int value) => Color(
(value >> 16) & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF,
);
/// `toDatabase` serialization method is also required!
@override
int toDatabase() => (red << 16) | (green << 8) | blue;
}
The Color
class above implements CustomDataType<int>
, which means it is
stored as an int
in the database. Thus, the toDatabase
method must
return an int
, and the fromDatabase
constructor must take an int
.
Note
The fromDatabase
constructor must be defined, and it must be a
factory constructor taking a single parameter of type T
.
You may implement additional methods on your custom type. For example, it's
often useful to implement ==
and hashCode
for immutable data types, for
details see Effective Dart.
Tip
You custom type is not required to be immutable, but using an immutable type
is strongly recommended. The analyzer can help with you this if you use the
@immutable
annotation from package:meta
.
Finally, it's important to note that if your custom type implements
Comparable<T>
from dart:core
, then the serialized value should satisfy
this ordering! As Expr<Comparable>
can be used for .min
and .max
aggregate functions in .groupBy
.
Database schema using a CustomDataType
With Color
implementing CustomDataType<int>
as in the example above we can
use Color
as column type in a database schema. The following example
shows how to use Color
in a database schema:
abstract final class Dealership extends Schema {
Table<Car> get cars;
}
@PrimaryKey(['id'])
abstract final class Car extends Row {
@AutoIncrement()
int get id;
String get model;
@Unique()
String get licensePlate;
// We can use our custom type as column type
Color get color;
}
The custom type Color
does not need be define in the same library as the
database schema.
Inserting a row with a CustomDataType
We can insert a row into the cars
table with a Color
as follows:
await db.cars
.insert(
model: toExpr('Beetle'),
licensePlate: toExpr('ABC-001'),
color: Color.red().asExpr,
)
.execute();
To insert a Color
we must constructor an Expr<Color>
object wrapping the
value we want to insert. We can obtain such an object using the .asExpr
extension method. This extension method will be created by the
code-generator when it sees a field with the type Color
.
Fetching a rows with a CustomDataType
We can fetch rows from the cars
table with a Color
as follows:
final List<Car> cars = await db.cars.fetch();
This gives us a list of Car
objects, where the color
field is deserialized
using Color.fromDatabase
. We can also use .select
to fetch only a subset
of fields from the database. The following example shows how to fetch distinct
model
and color
combinations:
final List<(String, Color)> modelAndColor = await db.cars
.select((car) => (
car.model,
car.color,
))
.distinct()
.fetch();
Expressions with a Expr<CustomDataType>
In the previous section we saw that we can use Car.color
as any other field.
When using extension methods like .select
or .where
we can access
Expr<Color>
using the .color
property on Expr<Car>
. However, since
the Color
type is purely a Dart concept, package:typed_sql
doesn't have many
extension methods.
The only extension method on Expr<Color>
is asEncoded()
, which casts to
the underlying serialized expression type. Thus, asEncoded()
on Expr<Color>
will return an Expr<int>
. We can use this to make our own extension methods
for Expr<Color>
as follows:
extension ColorExprExt on Expr<Color> {
// We know black is encoded as zero
Expr<bool> get isBlack => asEncoded().equalsValue(0);
// We can make `.equals` and `.equalsValue` for `Color` if we want
Expr<bool> equals(Expr<Color> other) => asEncoded().equals(other.asEncoded());
Expr<bool> equalsValue(Color other) => equals(other.asExpr);
// We can make our own utility methods too
Expr<bool> get isRed => equalsValue(Color.red());
Expr<bool> get isGreen => equalsValue(Color.green());
Expr<bool> get isBlue => equalsValue(Color.blue());
}
These extension methods operate on the serialized Color
, as serialized by
Color.toDatabase
. Implementing a .equals
might not make sense, if your
custom type doesn't have a canonical encoding. When writing queries we can use
these extension methods in methods like
If we loaded the following rows into the database:
final initialCars = [
(model: 'Beetle', licensePlate: 'ABC-123', color: Color.red()),
(model: 'Cooper', licensePlate: 'DEF-456', color: Color.blue()),
(model: 'Beetle', licensePlate: 'GHI-789', color: Color.blue()),
];
Then we could query for blue cars as follows:
final modelsAndLicense = await db.cars
.where((car) => car.color.isBlue)
.select((car) => (
car.model,
car.licensePlate,
))
.fetch();
check(modelsAndLicense).unorderedEquals([
('Beetle', 'GHI-789'),
('Cooper', 'DEF-456'),
]);
Classes
-
CustomDataType<
T extends Object?> Schema definition Custom data types - Interface to be implemented by custom types that can be stored in a Row for automatic (de)-serialization.