From 5f8ab421893c77c3ea1c4de7ea61d3397e3a3c75 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 23 Jul 2019 22:16:21 +0200 Subject: [PATCH] Start to work on MySQL example --- docs/docs/custom_backend.md | 16 +- examples/mysql/.gitignore | 11 ++ examples/mysql/README.md | 1 + examples/mysql/docker-compose.yaml | 21 +++ examples/mysql/lib/database.dart | 48 ++++++ examples/mysql/lib/database.g.dart | 160 ++++++++++++++++++ examples/mysql/lib/mysql.dart | 99 +++++++++++ examples/mysql/main.dart | 6 + examples/mysql/pubspec.yaml | 21 +++ .../runtime/executor/helpers/delegates.dart | 6 + 10 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 examples/mysql/.gitignore create mode 100644 examples/mysql/README.md create mode 100644 examples/mysql/docker-compose.yaml create mode 100644 examples/mysql/lib/database.dart create mode 100644 examples/mysql/lib/database.g.dart create mode 100644 examples/mysql/lib/mysql.dart create mode 100644 examples/mysql/main.dart create mode 100644 examples/mysql/pubspec.yaml diff --git a/docs/docs/custom_backend.md b/docs/docs/custom_backend.md index 9ef386e8..f53bfcf4 100644 --- a/docs/docs/custom_backend.md +++ b/docs/docs/custom_backend.md @@ -2,7 +2,21 @@ layout: guide title: Custom databases nav_order: 8 +since: 2.0 permalink: /custom_backend --- -TBD \ No newline at end of file +_Note_: This feature is available starting from Moor `2.0`. + +# Custom databases +Moor has builtin support for Flutter using the `sqflite` package - it also supports the [web]({{"web" | absolute_url}}) +and desktop apps. However, you can also use moor with a different database of your choice. In this guide, you'll learn how +to use Moor with a `mysql` database running on a server and with an encrypted database on a mobile device! + +## MySQL +We'll connect to a MySQL server with the `sqljockey5` library, so let's first add that library to the `pubspec.yaml`: +```yaml +dependencies: + // you'll also need moor, of course + sqljocky5: ^2.2.0 +``` \ No newline at end of file diff --git a/examples/mysql/.gitignore b/examples/mysql/.gitignore new file mode 100644 index 00000000..50602ac6 --- /dev/null +++ b/examples/mysql/.gitignore @@ -0,0 +1,11 @@ +# Files and directories created by pub +.dart_tool/ +.packages +# Remove the following pattern if you wish to check in your lock file +pubspec.lock + +# Conventional directory for build outputs +build/ + +# Directory created by dartdoc +doc/api/ diff --git a/examples/mysql/README.md b/examples/mysql/README.md new file mode 100644 index 00000000..89e165db --- /dev/null +++ b/examples/mysql/README.md @@ -0,0 +1 @@ +Example that uses moor with an mysql server. \ No newline at end of file diff --git a/examples/mysql/docker-compose.yaml b/examples/mysql/docker-compose.yaml new file mode 100644 index 00000000..51eb185f --- /dev/null +++ b/examples/mysql/docker-compose.yaml @@ -0,0 +1,21 @@ +version: "3.7" + +services: + database: + image: mariadb + environment: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: example + ports: + - "3306:3306" + phpmyadmin: + depends_on: + - database + image: phpmyadmin/phpmyadmin + ports: + - "8080:80" + environment: + PMA_HOST: database + PMA_USER: root + PMA_PASSWORD: password + PMA_PORT: 3306 \ No newline at end of file diff --git a/examples/mysql/lib/database.dart b/examples/mysql/lib/database.dart new file mode 100644 index 00000000..84a6a94d --- /dev/null +++ b/examples/mysql/lib/database.dart @@ -0,0 +1,48 @@ +import 'package:moor/moor.dart'; +import 'package:sqljocky5/connection/settings.dart'; + +import 'mysql.dart'; + +part 'database.g.dart'; + +class Users extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); +} + +@UseMoor(tables: [Users]) +class Database extends _$Database { + Database._(QueryExecutor e) : super(e); + + factory Database() { + final settings = ConnectionSettings( + user: 'root', + password: 'password', + host: 'localhost', + port: 3306, + db: 'example', + ); + + return Database._(MySqlBackend(settings)); + } + + @override + int get schemaVersion => 1; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + beforeOpen: (engine, details) async { + // we don't have migrations in mysql, so let's run them manually here! + final migrator = Migrator(this, engine.customStatement); + // will emit a "IF NOT EXISTS" statement, so its safe to run this + // on every open + await migrator.createAllTables(); + }, + ); + } + + Future insertUser(String name) async { + await into(users).insert(UsersCompanion(name: Value(name))); + } +} diff --git a/examples/mysql/lib/database.g.dart b/examples/mysql/lib/database.g.dart new file mode 100644 index 00000000..71d983cd --- /dev/null +++ b/examples/mysql/lib/database.g.dart @@ -0,0 +1,160 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ************************************************************************** +// MoorGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps +class User extends DataClass implements Insertable { + final int id; + final String name; + User({@required this.id, @required this.name}); + factory User.fromData(Map data, GeneratedDatabase db, + {String prefix}) { + final effectivePrefix = prefix ?? ''; + final intType = db.typeSystem.forDartType(); + final stringType = db.typeSystem.forDartType(); + return User( + id: intType.mapFromDatabaseResponse(data['${effectivePrefix}id']), + name: stringType.mapFromDatabaseResponse(data['${effectivePrefix}name']), + ); + } + factory User.fromJson(Map json, + {ValueSerializer serializer = const ValueSerializer.defaults()}) { + return User( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + ); + } + @override + Map toJson( + {ValueSerializer serializer = const ValueSerializer.defaults()}) { + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + }; + } + + @override + T createCompanion>(bool nullToAbsent) { + return UsersCompanion( + id: id == null && nullToAbsent ? const Value.absent() : Value(id), + name: name == null && nullToAbsent ? const Value.absent() : Value(name), + ) as T; + } + + User copyWith({int id, String name}) => User( + id: id ?? this.id, + name: name ?? this.name, + ); + @override + String toString() { + return (StringBuffer('User(') + ..write('id: $id, ') + ..write('name: $name') + ..write(')')) + .toString(); + } + + @override + int get hashCode => $mrjf($mrjc($mrjc(0, id.hashCode), name.hashCode)); + @override + bool operator ==(other) => + identical(this, other) || + (other is User && other.id == id && other.name == name); +} + +class UsersCompanion extends UpdateCompanion { + final Value id; + final Value name; + const UsersCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + }); +} + +class $UsersTable extends Users with TableInfo<$UsersTable, User> { + final GeneratedDatabase _db; + final String _alias; + $UsersTable(this._db, [this._alias]); + final VerificationMeta _idMeta = const VerificationMeta('id'); + GeneratedIntColumn _id; + @override + GeneratedIntColumn get id => _id ??= _constructId(); + GeneratedIntColumn _constructId() { + return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true); + } + + final VerificationMeta _nameMeta = const VerificationMeta('name'); + GeneratedTextColumn _name; + @override + GeneratedTextColumn get name => _name ??= _constructName(); + GeneratedTextColumn _constructName() { + return GeneratedTextColumn( + 'name', + $tableName, + false, + ); + } + + @override + List get $columns => [id, name]; + @override + $UsersTable get asDslTable => this; + @override + String get $tableName => _alias ?? 'users'; + @override + final String actualTableName = 'users'; + @override + VerificationContext validateIntegrity(UsersCompanion d, + {bool isInserting = false}) { + final context = VerificationContext(); + if (d.id.present) { + context.handle(_idMeta, id.isAcceptableValue(d.id.value, _idMeta)); + } else if (id.isRequired && isInserting) { + context.missing(_idMeta); + } + if (d.name.present) { + context.handle( + _nameMeta, name.isAcceptableValue(d.name.value, _nameMeta)); + } else if (name.isRequired && isInserting) { + context.missing(_nameMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + User map(Map data, {String tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : null; + return User.fromData(data, _db, prefix: effectivePrefix); + } + + @override + Map entityToSql(UsersCompanion d) { + final map = {}; + if (d.id.present) { + map['id'] = Variable(d.id.value); + } + if (d.name.present) { + map['name'] = Variable(d.name.value); + } + return map; + } + + @override + $UsersTable createAlias(String alias) { + return $UsersTable(_db, alias); + } +} + +abstract class _$Database extends GeneratedDatabase { + _$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e); + $UsersTable _users; + $UsersTable get users => _users ??= $UsersTable(this); + @override + List get allTables => [users]; +} diff --git a/examples/mysql/lib/mysql.dart b/examples/mysql/lib/mysql.dart new file mode 100644 index 00000000..2810e232 --- /dev/null +++ b/examples/mysql/lib/mysql.dart @@ -0,0 +1,99 @@ +import 'package:moor/backends.dart'; +import 'package:moor/moor.dart'; +import 'package:sqljocky5/connection/connection.dart'; +import 'package:sqljocky5/sqljocky.dart'; + +class MySqlBackend extends DelegatedDatabase { + MySqlBackend(ConnectionSettings settings) + : super(_MySqlDelegate(settings), logStatements: true); +} + +mixin _MySqlExecutor on QueryDelegate { + Querier get _querier; + + @override + Future runBatched(List statements) async { + for (var stmt in statements) { + await _querier.preparedMulti(stmt.sql, stmt.variables); + } + } + + @override + Future runCustom(String statement, List args) async { + await _querier.prepared(statement, args); + } + + @override + Future runInsert(String statement, List args) async { + final result = await _querier.prepared(statement, args); + return result.insertId; + } + + @override + Future runSelect(String statement, List args) async { + final result = await _querier.prepared(statement, args); + + final columns = [for (var field in result.fields) field.name]; + final rows = result.toList(); + + return QueryResult(columns, rows); + } + + @override + Future runUpdate(String statement, List args) async { + final result = await _querier.prepared(statement, args); + + return result.affectedRows; + } +} + +class _MySqlDelegate extends DatabaseDelegate with _MySqlExecutor { + final ConnectionSettings _settings; + + MySqlConnection _connection; + + _MySqlDelegate(this._settings); + + @override + Future get isOpen async => _connection != null; + + @override + Querier get _querier => _connection; + + @override + final DbVersionDelegate versionDelegate = const NoVersionDelegate(); + + @override + TransactionDelegate get transactionDelegate => _TransactionOpener(this); + + @override + Future open([GeneratedDatabase db]) async { + _connection = await MySqlConnection.connect(_settings); + } + + @override + Future close() async { + await _connection.close(); + } +} + +class _TransactionOpener extends SupportedTransactionDelegate { + final _MySqlDelegate _delegate; + + _TransactionOpener(this._delegate); + + @override + void startTransaction(Future Function(QueryDelegate) run) { + _delegate._connection.transaction((transaction) async { + final executor = _TransactionExecutor(transaction); + await run(executor); + }); + } +} + +class _TransactionExecutor extends QueryDelegate with _MySqlExecutor { + @override + final Querier _querier; + + _TransactionExecutor(this._querier); +} diff --git a/examples/mysql/main.dart b/examples/mysql/main.dart new file mode 100644 index 00000000..d0e2a20e --- /dev/null +++ b/examples/mysql/main.dart @@ -0,0 +1,6 @@ +import 'lib/database.dart'; + +void main() async { + final database = Database(); + await database.insertUser('MySql test'); +} diff --git a/examples/mysql/pubspec.yaml b/examples/mysql/pubspec.yaml new file mode 100644 index 00000000..1b60e50a --- /dev/null +++ b/examples/mysql/pubspec.yaml @@ -0,0 +1,21 @@ +name: mysql +description: A sample command-line application. +# version: 1.0.0 +# homepage: https://www.example.com +# author: Simon Binder + +environment: + sdk: '>=2.4.0 <3.0.0' + +dependencies: + moor: ^1.6.0 + sqljocky5: ^2.2.0 + +dev_dependencies: + test: ^1.6.0 + moor_generator: ^1.6.0 + build_runner: + +dependency_overrides: + moor: + path: ../../moor \ No newline at end of file diff --git a/moor/lib/src/runtime/executor/helpers/delegates.dart b/moor/lib/src/runtime/executor/helpers/delegates.dart index 3d46da1b..14e523b8 100644 --- a/moor/lib/src/runtime/executor/helpers/delegates.dart +++ b/moor/lib/src/runtime/executor/helpers/delegates.dart @@ -42,6 +42,12 @@ abstract class DatabaseDelegate implements QueryDelegate { /// [UseMoor]. It might be useful to read the [GeneratedDatabase.schemaVersion] /// if that information is required while opening the database. Future open([GeneratedDatabase db]); + + /// Closes this database. When the future completes, all resources used + /// by this database should have been disposed. + Future close() async { + // default no-op implementation + } } /// An interface which can execute sql statements.