Fixed temporal mapping

This commit is contained in:
thomashii@dukefirehawk.com 2022-07-24 12:00:10 +08:00
parent 793a8ac115
commit 572a134c9c
41 changed files with 356 additions and 97 deletions

View file

@ -63,8 +63,9 @@ For more details, checkout [Project Status](https://github.com/dukefirehawk/ange
1. Download and install [Dart](https://dart.dev/get-dart)
2. Clone one of the following starter projects:
* [Angel3 Basic Template](https://github.com/dukefirehawk/boilerplates/tree/angel3-basic)
* [Angel3 ORM Template](https://github.com/dukefirehawk/boilerplates/tree/angel3-orm)
* [Basic Template](https://github.com/dukefirehawk/boilerplates/tree/angel3-basic)
* [ORM for PostgreSQL Template](https://github.com/dukefirehawk/boilerplates/tree/angel3-orm)
* [ORM for MySQL Template](https://github.com/dukefirehawk/boilerplates/tree/angel3-orm-mysql)
* [Angel3 Graphql Template](https://github.com/dukefirehawk/boilerplates/tree/angel3-graphql)
3. Run the project in development mode (*hot-reloaded* is enabled on file changes).
@ -123,12 +124,12 @@ Check out [Migrating to Angel3](https://angel3-docs.dukefirehawk.com/migration/a
The performance benchmark can be found at
[TechEmpower Framework Benchmarks](https://tfb-status.techempower.com/)
[TechEmpower Framework Benchmarks Round 21](https://www.techempower.com/benchmarks/#section=data-r21&test=composite)
The test cases are build using standard `Angel3 ORM` template. This result will be used for fine-tuning Angel3 framework. The following test cases will be progressively added in the upcoming update to benchmark.
1. Angel3 with MongoDB
2. Angel3 with MySQL ORM
2. Angel3 with ORM MySQL
3. Cached queries
## Examples and Documentation

View file

@ -0,0 +1,93 @@
import 'dart:io';
import 'package:mysql_client/mysql_client.dart';
import 'package:mysql1/mysql1.dart';
void main() async {
print("=== Start 'mysql1' driver test");
await testMySQL1Driver().catchError((error, stackTrace) {
print(error);
});
print("=== End test");
print(" ");
print("=== Start 'mysql_client' driver test");
await testMySQLClientDriver().catchError((error, stackTrace) {
print(error);
});
print("=== End test");
print(" ");
//sleep(Duration(seconds: 5));
exit(0);
}
Future<void> testMySQLClientDriver() async {
var connection = await MySQLConnection.createConnection(
host: "localhost",
port: 3306,
databaseName: "orm_test",
userName: "test",
password: "test123",
secure: false);
await connection.connect(timeoutMs: 30000);
print(">Test Select All");
var result = await connection.execute("SELECT * from users");
print("Total records: ${result.rows.length}");
print(">Test Insert");
var params = {
"username": "test",
"password": "test123",
"email": "test@demo.com",
"updatedAt": DateTime.parse("1970-01-01 00:00:00")
};
result = await connection.execute(
"INSERT INTO users (username, password, email, updated_at) VALUES (:username, :password, :email, :updatedAt)",
params);
print("Last inserted ID: ${result.lastInsertID}");
print(">Test Select By ID");
result = await connection.execute(
"SELECT * from users where id=:id", {"id": result.lastInsertID.toInt()});
print("Read record: ${result.rows.first.assoc()}");
}
Future<void> testMySQL1Driver() async {
var settings = ConnectionSettings(
host: 'localhost',
port: 3306,
db: 'orm_test',
user: 'test',
password: 'test123',
timeout: Duration(seconds: 60));
var connection = await MySqlConnection.connect(settings);
print(">Test Select All");
var result = await connection.query("SELECT * from users");
print("Total records: ${result.length}");
print(">Test Insert");
var params = [
"test",
"test123",
"test@demo.com",
DateTime.parse("1970-01-01 00:00:00").toUtc()
];
// DateTime.parse("1970-01-01 00:00:01").toUtc()
result = await connection.query(
"INSERT INTO users (username, password, email, updated_at) VALUES (?, ?, ?, ?)",
params);
print("Last inserted ID: ${result.insertId}");
print(">Test Select By ID");
result = await connection
.query("SELECT * from users where id=?", [result.insertId]);
print("Read record: ${result.first.values}");
var d = DateTime.parse("1970-01-01 00:00:00").toUtc();
print("Local time: ${d.toLocal()}");
}

View file

@ -0,0 +1,12 @@
name: performance_tool
version: 1.0.0
description: Angel3 performance testing tool
publish_to: none
environment:
sdk: '>=2.16.0 <3.0.0'
published_to: none
dependencies:
mysql1: ^0.20.0
mysql_client: ^0.0.24
dev_dependencies:
lints: ^2.0.0

View file

@ -0,0 +1,6 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
The current main maintainer of the code base.

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -0,0 +1,22 @@
import 'dart:isolate';
Future<void> runApp(var message) async {
var stopwatch = Stopwatch()..start();
// Do something
print('Execution($message) Time: ${stopwatch.elapsed.inMilliseconds}ms');
stopwatch.stop();
}
void main() async {
var concurrency = 6000;
for (var i = 0; i < concurrency; i++) {
Isolate.spawn(runApp, 'Instance_$i');
}
await Future.delayed(const Duration(seconds: 10));
//print("Exit");
}

View file

@ -0,0 +1,11 @@
name: performance_tool
version: 1.0.0
description: Angel3 performance testing tool
publish_to: none
environment:
sdk: '>=2.16.0 <3.0.0'
published_to: none
dependencies:
http: ^0.13.4
dev_dependencies:
lints: ^2.0.0

View file

@ -0,0 +1,6 @@
Primary Authors
===============
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
The current main maintainer of the code base.

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View file

@ -291,15 +291,23 @@ abstract class Driver<
Future sendResponse(Request request, Response response, RequestContext req,
ResponseContext res,
{bool ignoreFinalizers = false}) {
//app.logger.fine("Calling SendResponse");
Future<void> _cleanup(_) {
if (!app.environment.isProduction && req.container!.has<Stopwatch>()) {
var sw = req.container!.make<Stopwatch>();
app.logger.info(
app.logger.fine(
"${res.statusCode} ${req.method} ${req.uri} (${sw.elapsedMilliseconds} ms)");
}
return req.close();
}
// TODO: Debugging header
/*
for (var key in res.headers.keys) {
app.logger.fine("Response header key: $key");
}
*/
if (!res.isBuffered) {
//if (res.isOpen) {
return res.close().then(_cleanup);
@ -307,6 +315,8 @@ abstract class Driver<
//return Future.value();
}
//app.logger.fine("Calling finalizers");
var finalizers = ignoreFinalizers == true
? Future.value()
: Future.forEach(app.responseFinalizers, (dynamic f) => f(req, res));
@ -315,6 +325,7 @@ abstract class Driver<
//if (res.isOpen) res.close();
for (var key in res.headers.keys) {
app.logger.fine("Response header key: $key");
setHeader(response, key, res.headers[key] ?? '');
}

View file

@ -18,5 +18,5 @@ Future<String> absoluteSourcePath(Type type) async {
}
}
return uri.toFilePath() + '#' + MirrorSystem.getName(mirror.simpleName);
return '${uri.toFilePath()}#${MirrorSystem.getName(mirror.simpleName)}';
}

View file

@ -397,9 +397,14 @@ abstract class Query<T, Where extends QueryWhere> extends QueryBase<T> {
return executor
.query(tableName, sql, substitutionValues,
returningQuery: returningSql)
.then((it) {
.then((result) {
// Return SQL execution results
return it.isEmpty ? Optional.empty() : deserialize(it.first);
//if (result.isNotEmpty) {
// for (var element in result.first) {
// _log.fine("value: $element");
// }
//}
return result.isEmpty ? Optional.empty() : deserialize(result.first);
});
}
}

View file

@ -5,19 +5,48 @@
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm_mysql/LICENSE)
This package contains the SQL Executor required by Angel3 ORM to work with MySQL and MariaDB respectively. In order to better support the differences in MySQL and MariaDb underlying protocols, two different drives have to be used. For MariaDb 10.2.x, `mysql1` driver provides the best results, while `mysql_client` driver handles MySQL 8.x.x without issues.
This package contains the SQL executor required by Angel3 ORM to work with MySQL and MariaDB respectively. In order to better support both MySQL and MariaDB, two different flavors of drives have been included; `mysql_client` and `mysql1`. They are implmented as `MySqlExecutor` and `MariaDbExecutor` respectively.
* MariaDbExecutor
* MySqlExecutor
## Supported databases
## Supported database version
* MariaDD 10.2.x or greater
* MySQL 8.x or greater
* MariaDb 10.2.x
* MySQL 8.x
**Note** MySQL below version 8.0 and MariaDB below version 10.2.0 are not supported as Angel3 ORM requires common table expressions (CTE) to work.
**Note** MySQL below version 8.0 and MariaDB below version 10.2 are not supported as Angel3 ORM requires common table expressions (CTE).
## MySqlExecutor
## Connecting to MariaDB database 10.2.x
This SQL executor is implemented using [`mysql_client`](https://pub.dev/packages?q=mysql_client) driver. It works with both `MySQL` 8.0+ and `MariaDB` 10.2+ database.
### Connecting to MySQL or MariaDB
```dart
import 'package:mysql_client/mysql_client.dart';
var connection = await MySQLConnection.createConnection(
host: "localhost",
port: 3306,
databaseName: "orm_test",
userName: "test",
password: "test123",
secure: true);
var logger = Logger('orm_mysql');
await connection.connect(timeoutMs: 10000);
var executor = MySqlExecutor(connection, logger: logger);
```
### Known Limitation for MySqlExecutor
* `Blob` data type mapping is not support.
* `timestamp` data type mapping is not supported. Use `datetime` instead.
* UTC datetime is not supported.
## MariaDBExecutor
This SQL executor is implemented using [`mysql1`](https://pub.dev/packages?q=mysql1) driver. It only works with `MariaDB` 10.2+ database. Do not use this for `MySQL` 8.0+ database.
### Connecting to MariaDB
```dart
import 'package:mysql1/mysql1.dart';
@ -34,31 +63,13 @@ This package contains the SQL Executor required by Angel3 ORM to work with MySQL
var executor = MariaDbExecutor(connection, logger: logger);
```
## Connecting to MySQL database 8.x
### Known Limitation for MariaDBExecutor
```dart
import 'package:mysql_client/mysql_client.dart';
* `Blob` type mapping is not supported.
* `timestamp` mapping is not supported. Use `datetime` instead.
* Only UTC datetime is supported. None UTC datetime will be automatically converted into UTC datetime.
var connection = await MySQLConnection.createConnection(
host: "localhost",
port: 3306,
databaseName: "orm_test",
userName: "test",
password: "test123",
secure: false);
var logger = Logger('orm_mysql');
await connection.connect(timeoutMs: 10000);
var executor = MySqlExecutor(connection, logger: logger);
```
### Issues
* Blob
* DateTime value not in UTC
* Transaction is broken
## Creating a new database in MariaDB/MySQL
## Creating a new database in MariaDB or MySQL
1. Login to MariaDB/MySQL database console with the following command.
@ -71,29 +82,27 @@ This package contains the SQL Executor required by Angel3 ORM to work with MySQL
```mysql
create database orm_test;
-- Granting localhost access only
create user 'test'@'localhost' identified by 'test123';
grant all privileges on orm_test.* to 'test'@'localhost';
-- Granting localhost and remote access
create user 'test'@'%' identified by 'test123';
grant all privileges on orm_test.* to 'test'@'%';
```
## Known limitation
## Compatibility Matrix
### Using `mysql1` driver on MariabDb
### MariaDB 10.2+
* Blob
* DateTime value not in UTC
* Transaction is broken
| | Create | Read | Update | Delete |
|-----------------|--------|--------|--------|--------|
| MySqlExecutor | Y | Y | Y | Y |
| MariaDBExecutor | Y | Y | Y | Y |
### Using `mysql1` driver on MySQL
### MySQL 8.0+
* Blob is not supported
### Using `mysql_client` driver on MariabDb
* Blob is not supported
### Using `mysql_client` driver on MySQL
* Blob is not supported
| | Create | Read | Update | Delete |
|-----------------|--------|--------|--------|--------|
| MySqlExecutor | Y | Y | Y | Y |
| MariaDBExecutor | N | N | N | N |

View file

@ -9,6 +9,8 @@ class MariaDbExecutor extends QueryExecutor {
final MySqlConnection _connection;
TransactionContext? _transactionContext;
MariaDbExecutor(this._connection, {Logger? logger}) {
this.logger = logger ?? Logger('MariaDbExecutor');
}
@ -33,6 +35,15 @@ class MariaDbExecutor extends QueryExecutor {
}
var params = substitutionValues.values.toList();
for (var i = 0; i < params.length; i++) {
var v = params[i];
if (v is DateTime) {
if (!v.isUtc) {
params[i] = v.toUtc();
}
}
}
logger.warning('Query: $query');
logger.warning('Values: $params');
@ -66,12 +77,12 @@ class MariaDbExecutor extends QueryExecutor {
@override
Future<T> transaction<T>(FutureOr<T> Function(QueryExecutor) f) async {
T? returnValue = await _connection.transaction((ctx) async {
// TODO: This is broken
var conn = ctx as MySqlConnection;
// TODO: To be relooked at
try {
logger.fine('Entering transaction');
var tx = MariaDbExecutor(conn, logger: logger);
return await f(tx);
//var tx = MariaDbExecutor(conn, logger: logger);
_transactionContext = ctx;
return await f(this);
} catch (e) {
logger.severe('Failed to run transaction', e);
rethrow;

View file

@ -131,7 +131,10 @@ class MySqlExecutor extends QueryExecutor {
// Handle select
return _connection.execute(query, substitutionValues).then((results) {
logger.warning("SELECT");
var tmpData = results.rows;
for (var element in tmpData) {
logger.warning("[Result] : ${element.assoc()}");
}
return results.rows.map((r) => r.typedAssoc().values.toList()).toList();
});

View file

@ -10,25 +10,28 @@ List tmpTables = [];
FutureOr<QueryExecutor> Function() createTables(List<String> schemas) {
// For MySQL
//return () => _connectToMySql(schemas);
return () => _connectToMySql(schemas);
// For MariaDB
return () => _connectToMariaDb(schemas);
//return () => _connectToMariaDb(schemas);
}
// For MySQL
Future<void> dropTables2(QueryExecutor executor) async {
Future<void> dropTables(QueryExecutor executor) async {
var sqlExecutor = (executor as MySqlExecutor);
for (var tableName in tmpTables.reversed) {
await sqlExecutor.rawConnection.execute('drop table $tableName;');
print('DROP TABLE $tableName');
await sqlExecutor.rawConnection.execute('DROP TABLE $tableName;');
}
return sqlExecutor.close();
}
// For MariaDB
Future<void> dropTables(QueryExecutor executor) {
Future<void> dropTables2(QueryExecutor executor) {
var sqlExecutor = (executor as MariaDbExecutor);
for (var tableName in tmpTables.reversed) {
print('DROP TABLE $tableName');
sqlExecutor.query(tableName, 'DROP TABLE $tableName', {});
}
return sqlExecutor.close();
@ -64,7 +67,7 @@ Future<MariaDbExecutor> _connectToMariaDb(List<String> schemas) async {
var data = await File('test/migrations/$s.sql').readAsString();
var queries = data.split(";");
for (var q in queries) {
//print("Table: [$q]");
print("Table: [$q]");
if (q.trim().isNotEmpty) {
//await connection.execute(q);
await connection.query(q);

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS authors (
id serial PRIMARY KEY,
name varchar(255) UNIQUE NOT NULL,
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -3,6 +3,6 @@ CREATE TABLE IF NOT EXISTS books (
author_id int NOT NULL,
partner_author_id int,
name varchar(255),
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS cars (
make varchar(255) NOT NULL,
description TEXT NOT NULL,
family_friendly BOOLEAN NOT NULL,
recalled_at timestamp,
created_at timestamp,
updated_at timestamp
recalled_at datetime,
created_at datetime,
updated_at datetime
);

View file

@ -2,6 +2,6 @@ CREATE TABLE IF NOT EXISTS feet (
id serial PRIMARY KEY,
leg_id int NOT NULL,
n_toes int NOT NULL,
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS fruits (
id serial,
tree_id int,
common_name varchar(255),
created_at timestamp,
updated_at timestamp,
created_at datetime,
updated_at datetime,
PRIMARY KEY(id)
);

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS has_cars (
id serial PRIMARY KEY,
type int not null,
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -2,6 +2,6 @@ CREATE TABLE IF NOT EXISTS has_maps (
id serial PRIMARY KEY,
value jsonb not null,
list jsonb not null,
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS legs (
id serial PRIMARY KEY,
name varchar(255) NOT NULL,
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS numbas (
i int NOT NULL UNIQUE,
parent int,
created_at TIMESTAMP,
updated_at TIMESTAMP,
created_at datetime,
updated_at datetime,
PRIMARY KEY(i)
);

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS roles (
id serial PRIMARY KEY,
name varchar(255),
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS songs (
id serial,
weird_join_id int,
title varchar(255),
created_at TIMESTAMP,
updated_at TIMESTAMP,
created_at datetime,
updated_at datetime,
PRIMARY KEY(id)
);

View file

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS trees (
id serial,
rings smallint UNIQUE,
created_at timestamp,
updated_at timestamp,
created_at datetime,
updated_at datetime,
PRIMARY KEY(id)
);

View file

@ -3,6 +3,6 @@ CREATE TABLE IF NOT EXISTS users (
username varchar(255),
password varchar(255),
email varchar(255),
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -2,6 +2,6 @@ CREATE TABLE IF NOT EXISTS role_users (
id serial PRIMARY KEY,
user_id int NOT NULL,
role_id int NOT NULL,
created_at timestamp,
updated_at timestamp
created_at datetime,
updated_at datetime
);

View file

@ -14,7 +14,7 @@ void main() async {
// if (rec.stackTrace != null) print(rec.stackTrace);
//});
belongsToTests(createTables(['author', 'book']), close: dropTables);
//belongsToTests(createTables(['author', 'book']), close: dropTables);
//hasOneTests(my(['leg', 'foot']), close: closeMy);
//standaloneTests(my(['car']), close: closeMy);

View file

@ -5,4 +5,10 @@
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/master/packages/orm/angel_orm_test/LICENSE)
Common tests for Angel3 ORM. Reference implmentation of generated ORM files.
Common test cases for Angel3 ORM. Reference implmentation of generated ORM files.
## Supported databases
* MariaDb 10.2.x or greater
* MySQL 8.x or greater
* PostreSQL 10.x or greater

View file

@ -17,7 +17,7 @@ abstract class _Boat extends Model {
@Column(defaultValue: false)
bool get familyFriendly;
//@SerializableField(defaultValue: '1970-01-01 00:00:00')
//@SerializableField(defaultValue: '1970-01-01 00:00:01')
DateTime get recalledAt;
@Column(defaultValue: 0.0)