Flutter数据库操作插件simple_mysql_orm的使用

Flutter数据库操作插件simple_mysql_orm的使用

simple_mysql_orm 提供了一个轻量级的包装器,用于 galileo_mysql 包,添加了一个对象关系映射(ORM)层。以下是该库的一些特性:

  • 完整的事务支持
  • 数据库连接池
  • 简单的代码生成器(有待改进)

代码生成器

Simple ORM 现在有一个非常粗糙的代码生成器。

它可以从数据库表生成模型和DAO类。

要使用生成器,请执行以下命令:

dart pub global activate simple_mysql_orm
build_dao --host <host> --port <port> --database <db> --user <user> --password <password> --table <table>

上述命令将在当前目录下生成文件:

  • <table>.dart
  • dao_<table>.dart

如果这两个文件已存在,生成将失败。

你可以通过传递 --no-dao 参数来排除DAO文件的生成。

你可以通过传递 --file 参数来控制输出文件的名称。

示例用法

这是一个示例用法。完整的实现可以在示例目录中找到。

对于每个表,你需要创建一个数据访问对象(DAO),其中包含所有业务规则,以及一个实体,其中仅包含实体的字段。

DAO示例

import 'package:simple_mysql_orm/simple_mysql_orm.dart';

import '../../test_dao/model/system.dart';

class DaoSystem extends Dao<System> {
  DaoSystem() : super(tablename);
  DaoSystem.withDb(Db db) : super.withDb(db, tablename);

  static String get tablename => 'system';

  @override
  System fromRow(Row row) => System.fromRow(row);

  Future<String?> getByKey(String keyName) async =>
      (await getByField('key', keyName)).value;

  Future<String?> tryByKey(String keyName) async =>
      (await tryByField('key', keyName))?.value;
}

实体示例

import 'package:simple_mysql_orm/simple_mysql_orm.dart';

class System extends Entity<System> {
  factory System({required String key, required String value}) =>
      System._internal(
          key: key,
          value: value,
          createdAt: DateTime.now(),
          updatedAt: DateTime.now(),
          id: -1);

  factory System.fromRow(Row row) {
    final id = row.asInt('id');
    final key = row.asString('key');
    final value = row.tryAsString('value');
    final createdAt = row.asDateTime('createdAt');
    final updatedAt = row.asDateTime('updatedAt');

    return System._internal(
        id: id,
        key: key,
        value: value,
        createdAt: createdAt,
        updatedAt: updatedAt);
  }

  System._internal({
    required int id,
    required this.key,
    required this.value,
    required this.createdAt,
    required this.updatedAt,
  }) : super(id);

  late String key;
  late String? value;

  late DateTime createdAt;
  late DateTime updatedAt;

  @override
  FieldList get fields => [
        'key',
        'value',
        'createdAt',
        'updatedAt',
      ];

  @override
  ValueList get values => [
        key,
        value,
        createdAt,
        updatedAt,
      ];
}

使用DAO和实体

import 'package:logging/logging.dart';
import 'package:settings_yaml/settings_yaml.dart';
import 'package:simple_mysql_orm/simple_mysql_orm.dart';

import 'dao/package_dao.dart';
import 'model/package.dart';

Future<void> main() async {
  /// 配置日志记录以输出每条SQL命令。
  Logger.root.level = Level.INFO;
  Logger.root.onRecord.listen((record) {
    print('${record.level.name}: ${record.time}: ${record.message}');
  });

  /// 创建设置文件。
  await SettingsYaml.fromString(
          content: settingsYaml, filePath: 'settings.yaml')
      .save();

  /// 从设置初始化数据库连接池。
  DbPool.fromSettings(pathToSettings: 'settings.yaml');

  /// 在事务中运行一组查询。
  await withTransaction<void>(action: () async {
    final dao = PackageDao();

    /// 创建并保存一个包。
    final package = Package(name: 'dcli', private: false);
    await dao.persist(package);

    /// 更新包为公开。
    package.private = false;
    await dao.update(package);

    /// 使用非常基础且不完整的生成器查询包。
    var rows = await dao.select().where().eq('name', 'dcli').run();
    for (final row in rows) {
      print('name: ${row.name} private: ${row.private}');
    }

    /// 执行自定义查询。
    rows = await dao.query('select * from package where id = ?', [package.id]);
    for (final row in rows) {
      print('name: ${row.name} private: ${row.private}');
    }

    // 删除包。
    await dao.remove(package);

    /// 改变主意。
    await Transaction.current.rollback();
  });
}

const settingsYaml = '''
mysql_user: root
mysql_password: my root password
mysql_host: localhost
mysql_port: 3306
mysql_db: some_db_name
''';

多租户

SMO 支持多租户数据库的概念。

这是一些简单的实现,并且需要你确保所有的查询都是完全多租户的。

模式

每个多租户表必须有一个租户ID。

你可以选择列名,并且可以为每个表不同,但最好使用相同的列名。

租户ID通常是定义租户的表的主键。

例如:

Company
  int id
  String name

Staff Member
  int id
  int companyId
  String name

Team
  int id
  int companyId
  String name

DAO租户

对于每个具有租户ID的表,你必须创建一个 DaoTenant 而不是 Dao

DaoTenant 需要你提供该表的租户字段。

class DaoMember extends DaoTenant<Member> {
  DaoMember() : super(tableName: tablename, tenantFieldName: 'companyId');
  DaoMember.withDb(Db db)
      : super.withDb(db, tableName: tablename, tenantFieldName: 'companyId');

  /// 如果 [email] 不属于租户的一个成员,则抛出 [UnknownMemberException]。
  Future<Member> getByEmail({required String email}) async {
    final member = await trySingle(
        await query('select * from $tablename '
        'where email = ? '
        'and companyId = ?', [email, Tenant.tenantId]));
    if (member == null) {
      throw UnknownMemberException(
          'The email address $email is not for a know member');
    }
    return member;
  }
}

Company 表应该从 Dao 而不是 DaoTenant 衍生。

你也可能有一些非租户表,例如用于存储全局值(如SMTP服务器主机地址)的系统表。

这些表应使用 Dao 并不需要租户ID。

租户实体

租户实体就像普通的实体一样,只是它们不会暴露租户ID字段(而普通实体应该暴露所有字段)。

租户ID不应该在租户实体中提及,因为它会被注入。

import 'package:date_time/date_time.dart';
import 'package:simple_mysql_orm/simple_mysql_orm.dart';

/// What unpud refers to as an uploader
class Member extends EntityTenant<Member> {
  factory Member({
    required String email,
  }) =>
      Member._internal(
          email: email,
          startDate: DateExtension.now(),
          enabled: true,
          createdAt: DateTime.now(),
          updatedAt: DateTime.now(),
          id: -1);

  factory Member.fromRow(Row row) {
    final id = row.asInt('id');
    final email = row.asString('email');
    final startDate = row.asDate('startDate');
    final enabled = row.asBool('enabled');
    final createdAt = row.asDateTime('createdAt');
    final updatedAt = row.asDateTime('updatedAt');

    return Member._internal(
        email: email,
        startDate: startDate,
        enabled: enabled,
        createdAt: createdAt,
        updatedAt: updatedAt,
        id: id);
  }

  Member._internal({
    required int id,
    required this.email,
    required this.startDate,
    required this.enabled,
    required this.createdAt,
    required this.updatedAt,
  }) : super(id);

  late String email;

  late Date startDate;
  late bool enabled;

  late DateTime createdAt;
  late DateTime updatedAt;

  @override
  FieldList get fields => [
        'email',
        'startDate',
        'enabled',
        'createdAt',
        'updatedAt'
      ];

  @override
  ValueList get values => [email, startDate, enabled, createdAt, updatedAt];
}

typedef MemberId = int;

访问租户表

要访问租户表,你需要将所有对这些表的访问放在租户范围中。

我们通过 withTenant 方法来实现这一点。

/// 公司不是租户,因此我们可以在 `withTenant` 之外访问它。
await DaoCompany().getByName('noojee');
await withTenant(
        tenantId: company.id,
        action: () async {

          /// 插入一个成员,租户ID将自动设置。
          const noojeeEmail = 'sales@noojee.com.au';
          final noojeeMember =
              Member(publisherId: noojeeId, email: noojeeEmail);
          final noojeeMemberId = await daoMember.persist(noojeeMember);

          /// 获取一个成员,租户ID将被添加到WHERE子句中。
          final member = await daoMember.getByEmail(email: noojeeEmail);

          /// 系统表是非租户表。
          /// 我们可以在或不在作用域内访问它。
          await DaoSystem().getByKey('smtp')

        });

绕过租户访问

默认情况下,SMO 将尝试检查你是否总是过滤查询以包含租户ID。

这种方法并不完全可靠(见下文)。

但在某些情况下,你可能希望访问实现 DaoTenant 的表而不使用租户ID来限制结果。

例如,系统管理员需要访问所有租户的员工表。

你也很可能需要在租户模式和绕过租户模式下访问用户表。

在登录过程中,直到你查询用户表之前,你都不知道租户。但在正常操作中,用户表应始终作为租户表访问。

对于这些场景,你可以使用 withTenantBypass

当你使用 withTenantBypass 时,DaoTenant 将不会将租户ID注入你的查询。

Future<User?> loginUser(String username) async  {
   return  await withTenantByPass(
        action: () async {

          /// 获取一个成员,租户ID将被添加到WHERE子句中。
          return  await daoMember.tryByEmail(email: username);
        });

为了使这有效,你需要确保用户名在所有租户中是唯一的。

你也可以在 withTenantBypass 中嵌套 withTenant 到任意级别。

租户查询验证

SMO 允许你编写自定义查询。当你写自定义查询时,由你负责确保遵循租户规则。

SMO 尝试验证所有查询是否包含租户字段,但这只是一个粗略的检查(我们只是查找列名的存在)。

编写自定义查询

当你编写自定义查询时,你通常知道你是在租户模式还是非租户模式,因为你的自定义代码应该存在于继承自 DaoTenantDao 的类中。

对于那些可能不是这种情况的情况,你可以使用以下代码:

if (Tenant.inTenantScope)
{
  /// 被调用在 [withTenant] 调用中。
}

if (Tenant.inTenantBypassScope)
{
  /// 被调用在 [withTenantBypass] 中。
}

if (dao is DaoTenant)
{

}

日志记录

要输出发送到数据库的每个查询,请将日志记录设置为 FINE

要获取所有数据库交互,请将日志记录设置为 FINER

测试

单元测试期望一个预存在的模式。要创建模式,请运行:

dmysql restore smo < schema\createv3.sql

更多关于Flutter数据库操作插件simple_mysql_orm的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter数据库操作插件simple_mysql_orm的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用simple_mysql_orm插件进行数据库操作的示例代码。请注意,simple_mysql_orm通常用于与MySQL数据库交互,而Flutter是一个用于构建跨平台移动应用的框架。因此,在Flutter应用中与MySQL数据库通信通常涉及网络请求。

首先,确保你已经在pubspec.yaml文件中添加了simple_mysql_orm依赖项:

dependencies:
  flutter:
    sdk: flutter
  simple_mysql_orm: ^最新版本号  # 替换为实际最新版本号

然后,运行flutter pub get来安装依赖项。

以下是一个简单的示例,展示了如何使用simple_mysql_orm在Flutter应用中与MySQL数据库进行交互。

1. 配置MySQL连接

你需要配置MySQL数据库的连接信息。这通常在应用的某个初始化部分完成。

import 'package:simple_mysql_orm/simple_mysql_orm.dart';

// 配置MySQL连接信息
final MySQLConnectionInfo connectionInfo = MySQLConnectionInfo(
  host: '你的数据库主机地址',
  port: 3306, // MySQL默认端口
  user: '你的数据库用户名',
  password: '你的数据库密码',
  database: '你的数据库名称',
);

// 创建MySQL客户端
final MySQLClient client = MySQLClient(connectionInfo);

2. 定义数据模型

使用@Table@Column注解定义你的数据模型。

import 'package:simple_mysql_orm/annotations.dart';

@Table('users')
class User {
  @Column(primaryKey: true, autoIncrement: true)
  int id;

  @Column()
  String name;

  @Column()
  String email;

  // 构造函数和其他方法...
  User({required this.name, required this.email});

  // 从数据库结果映射到对象
  User.fromMap(Map<String, dynamic> map) {
    id = map['id'] as int;
    name = map['name'] as String;
    email = map['email'] as String;
  }

  // 将对象转换为Map,用于插入或更新数据库
  Map<String, dynamic> toMap() {
    return {
      'name': name,
      'email': email,
    };
  }
}

3. 执行数据库操作

现在,你可以使用MySQLClient实例执行数据库操作,如插入、查询、更新和删除。

void main() async {
  // 初始化MySQL客户端(通常在实际应用中,这会在某个服务或依赖注入容器中完成)
  final MySQLClient client = MySQLClient(connectionInfo);

  // 打开连接
  await client.open();

  // 插入数据
  User user = User(name: 'John Doe', email: 'john.doe@example.com');
  await client.insert(user.toMap(), into: 'users');

  // 查询数据
  List<Map<String, dynamic>> results = await client.selectAllFrom('users');
  List<User> users = results.map((e) => User.fromMap(e)).toList();
  print('Users: $users');

  // 更新数据
  User userToUpdate = users.firstWhere((user) => user.id == 1);
  userToUpdate.email = 'new.email@example.com';
  await client.update(userToUpdate.toMap(), where: 'id = ?', whereArgs: [userToUpdate.id], table: 'users');

  // 删除数据
  await client.deleteFrom('users', where: 'id = ?', whereArgs: [userToUpdate.id]);

  // 关闭连接
  await client.close();
}

注意事项

  1. 异步操作:所有数据库操作都是异步的,因此使用asyncawait关键字。
  2. 错误处理:在实际应用中,添加适当的错误处理逻辑来捕获和处理可能发生的异常。
  3. 连接管理:确保正确管理数据库连接的生命周期,特别是在应用进入后台或关闭时关闭连接。
  4. 安全性:不要在客户端代码中硬编码敏感信息,如数据库密码。考虑使用环境变量或安全的配置管理工具。

由于simple_mysql_orm是一个第三方库,其API和用法可能会随着版本的更新而发生变化。因此,建议查阅最新的官方文档以获取最准确的信息。

回到顶部