Flutter - 集成三方库:数据库(sqflite)

数据库

sh 复制代码
$ flutter pub add sqlite
$ flutter pub get
sh 复制代码
$ flutter run

运行失败,看是编译报错,打开Xcode工程 ⌘ + B 编译

对比 GSYGithubAppFlutter 的Xcode工程Build Phases > [CP] Embed Pods Frameworks 有sqfite.framework。本地默认的Flutter工程默认未生成Podfile

然后查看 GSYGithubAppFlutter

ruby 复制代码
...
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
...

看代码是引入了Flutter提供的工具的,从flutter的安装目录下找到podhelper.rb这个文件

ruby 复制代码
# 方法: flutter_install_all_ios_pods
# 安装Flutter在iOS平台上的引擎和插件
def flutter_install_all_ios_pods(ios_application_path = nil)
  # 创建Flutter引擎的.podspec文件
  flutter_install_ios_engine_pod(ios_application_path)
  flutter_install_plugin_pods(ios_application_path, '.symlinks', 'ios')
end
ruby 复制代码
# 方法: flutter_install_plugin_pods
def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, platform)
  # CocoaPods定义了 defined_in_file,获取应用路径,未获取到就中断
  application_path ||= File.dirname(defined_in_file.realpath) if respond_to?(:defined_in_file)
  raise 'Could not find application path' unless application_path

  # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
  # referring to absolute paths on developers' machines.
  # 使用符号链接,避免使用Podfile.lock这个文件
  # Flutter是在ios目录下创建.symlinks目录,里面有软链接指向Flutter下载包的位置,这样只需要一份即可。
  # 先删除,再创建对应的目录
  symlink_dir = File.expand_path(relative_symlink_dir, application_path)
  system('rm', '-rf', symlink_dir) 

  symlink_plugins_dir = File.expand_path('plugins', symlink_dir)
  system('mkdir', '-p', symlink_plugins_dir)

  plugins_file = File.join(application_path, '..', '.flutter-plugins-dependencies')
  dependencies_hash = flutter_parse_plugins_file(plugins_file)
  plugin_pods = flutter_get_plugins_list(dependencies_hash, platform)
  swift_package_manager_enabled = flutter_get_swift_package_manager_enabled(dependencies_hash, platform)

  plugin_pods.each do |plugin_hash|
    plugin_name = plugin_hash['name']
    plugin_path = plugin_hash['path']
    ...
    # 使用path: 的方式本地依赖需要的三方库
    # 手动添加打印确认下
    # print "plugin_name:#{plugin_name}\n"
    pod plugin_name, path: File.join(relative, platform_directory)
  end
end
sh 复制代码
$ pod update --verbose

因此Podfile里的target部分就依赖了sqflite_darwin

ruby 复制代码
target 'Runner' do
  use_frameworks!
  use_modular_headers!
  ...
  pod 'sqflite_darwin', path:.symlinks/plugins/sqflite_darwin/darwin
end

使用

打开/关闭/删除数据库
dart 复制代码
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

var databasesPath = await getDatabasesPath();
String path = join(databasesPath, 'finger.db');

/// 打开数据库
Database database = await openDatabase(path, version: 1,
    onCreate: (Database db, int version) async {
  /// 当创建数据库时创建table
  await db.execute(
      'CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)');
});
dart 复制代码
/// 关闭数据库
await db.close();
dart 复制代码
/// 删除数据库
await deleteDatabase(path);
dart 复制代码
/// 添加表
await database.execute(
      "CREATE TABLE Test2(id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)",
    );
      
/// 删除表
await database.execute('DROP TABLE Test2');
使用SQL语句
dart 复制代码
/// 添加数据
await database.transaction((txn) async {
  int id1 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)');
  int id2 = await txn.rawInsert(
      'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)',
      ['another name', 12345678, 3.1416]);
});
dart 复制代码
/// 删除数据
count = await database
    .rawDelete('DELETE FROM Test WHERE name = ?', ['another name']);
dart 复制代码
/// 更新数据
int count = await database.rawUpdate(
    'UPDATE Test SET name = ?, value = ? WHERE name = ?',
    ['updated name', '9876', 'some name']);
dart 复制代码
/// 查询数据
List<Map> list = await database.rawQuery('SELECT * FROM Test');
print(list)
使用工具方法

使用Sqflite提供的工具方法来执行数据库操作,而不是直接使用SQL语句

dart 复制代码
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

final String tName = 'company';
final String columnId = "_id";
final String columnName = "name";

class Company {
  int? id;
  String? name;
  Company();

  Map<String, Object?> toMap() {
    var map = <String, Object?>{columnName: name};
    if (id != null) {
      map[columnId] = id;
    }
    return map;
  }

  Company.fromMap(Map map) {
    id = map[columnId];
    name = map[columnName];
  }
}

class CompanyProvider {
  Database? db;

  Future<Database?> open() async {
    if (db == null) {
      var databasesPath = await getDatabasesPath();
      String path = join(databasesPath, 'demo.db');
      db = await openDatabase(
        path,
        version: 1,
        onCreate: (Database db, int version) async {
          await db.execute('''
            create table $tName (
            $columnId integer primary key autoincrement,
            $columnName text not null)
        ''');
        },
      );
    }
    return db;
  }

  /// 注册企业
  Future insert(Company company) async {
    /// 工具方法: 传表名 + 列信息添加数据到数据库
    company.id = await db?.insert(tName, company.toMap());
    return company;
  }

  /// 查找企业
  Future findById(int id) async {
    List<Map> maps = await db!.query(
      tName, /// 表名
      columns: [columnId, columnName], /// 查找的列
      where: '$columnId = ?', /// 查找条件
      whereArgs: [id], /// 每个问号填充的值
    );
    if (maps.isNotEmpty) {
      return Company.fromMap(maps.first);
    }
    return null;
  }
  
  /// 查找所有的企业
  Future<List<Company>> find() async {
    List<Company> companys = [];
    List<Map> maps = await db!.query(tName, columns: [columnId, columnName]);
    for (var map in maps) {
      Company c = Company.fromMap(map);
      companys.add(c);
    }
    return companys;
  }

  /// 删除企业
  Future delete(int id) async {
    /// 根据id列删除企业
    return await db?.delete(tName, where: '$columnId = ?', whereArgs: [id]);
  }

  /// 更新企业信息
  Future update(Company company) async {
    return await db?.update(
      tName,
      company.toMap(),
      where: '$columnId = ?',
      whereArgs: [company.id],
    );
  }
}
dart 复制代码
void test() async {
    /// 添加2条测试数据
    CompanyProvider cp = CompanyProvider();
    await cp.open();
    List<Map> maps = [
      {"name": "Google"},
      {"name": "Apple"},
    ];

    /// 新增数据
    int firstId = 0;
    for (int i = 0; i < maps.length; ++i) {
      Company c = Company.fromMap(maps[i]);
      cp.insert(c);
    }

    /// 查找数据
    List<Company> companys = await cp.find();
    if (companys.isNotEmpty) {
      firstId = companys.first.id!;
    }

    if (firstId > 0) {
      Company firstCompany = await cp.findById(firstId);
      print(firstCompany.toMap());

      /// 更新数据
      Company chgCompany = Company();
      chgCompany.id = firstId;
      chgCompany.name = DateTime.now().microsecondsSinceEpoch.toString();
      cp.update(chgCompany);

      firstCompany = await cp.findById(firstId);
      print(firstCompany.toMap());

      /// 删除数据
      cp.delete(firstId);
    }
  }
数据库迁移

随着功能迭代,需要对数据库的表结构进行修改时,比如增加新字段时,需要对表的结构进行更新。

dart 复制代码
Future<Database?> open() async {
    if (db == null) {
      var databasesPath = await getDatabasesPath();
      String path = join(databasesPath, 'demo.db');
      db = await openDatabase(
        path,
        version: 2,

        /// 1.新版本发布时改成2
        onCreate: (db, version) async {
          /// 2.新安装设备触发onCreate,所以这里添加新的字段
          await db.execute('''
            create table $tName (
            $columnId integer primary key autoincrement,
            $columnName text not null,
            $columnDesc text)
        ''');
        },
        onUpgrade: (db, oldVersion, newVersion) async {
          var batch = db.batch();
          /// [onUpgrade] is called if either of 
          /// the following conditions are met:

          /// 1. [onCreate] is not specified
          /// 2. The database already exists and [version] is higher than the last database version
          /// onUpgrade回调在未指定onCreate回调或者数据库已经存在同时version字段高于已安装的版本,执行完onUpgrade回调后应该会更新关联的版本,设置断点让onUpgrade执行中断,下次还会会执行这个方法
          
          /// 3.对旧版本的设备:判断安装设备已创建的数据库版本
          if (oldVersion == 1) {
            _updateTableCompanyV1toV2(batch);
          }
          await batch.commit();
        },
      );
    }
    return db;
  }
dart 复制代码
/// 4.添加description字段
void _updateTableCompanyV1toV2(Batch batch) {
    batch.execute('ALTER TABLE Company ADD description TEXT');
}

/// 其它的一些处理
final String columnDesc = "description";
...

class Company {
  int? id;
  String? name;

  /// 5.模型增加对应字段 + 列
  String? description;
  ...
  
  /// 6. 更新map和对象的转换方法
  Map<String, Object?> toMap() {
    var map = <String, Object?>{columnName: name, columnDesc: description};
    if (id != null) {
    ...
dart 复制代码
/// 调用
...
firstCompany.description = "版本2新增的字段";
print(firstCompany.toMap());
事务

数据库的增删改查可能会失败,导致数据与预期的不一致,为了保证在执行前后的数据一致性,引入了事务。事务具有ACID这4个特性:原子性、一致性、隔离性和持久性。

在事务中不要使用数据库,而只需要使用事务对象访问数据库。

dart 复制代码
await database.transaction((txn) async {
  // 正确
  await txn.execute('CREATE TABLE Test1 (id INTEGER PRIMARY KEY)');
  
  // 不要在事务中使用数据库
  // 下面会导致死锁
  await database.execute('CREATE TABLE Test2 (id INTEGER PRIMARY KEY)');
});
dart 复制代码
try {
   await database.transaction((txn) async {
      await txn.update('TABLE', {'foo': 'bar'});
   });
   
   // No error, the transaction is committed
   // 1. 未报错,则事务被提交
   
   // cancel the transaction (any error will do)
   // 2. 取消或执行时报错,则抛出异常在,catch中被捕获
   // throw StateError('cancel transaction');
} catch (e, st) {
   // this reliably catch if there is a key conflict
   // We know that the transaction is rolled back.
   // 3. 事务被回滚,执行业务相关的操作,比如提示报错
}
批处理

使用 Batch,即批处理,来避免在 Dart 和原生代码之间的反复切换。

dart 复制代码
batch = db.batch();
batch.insert('Test', {'name': 'item'});
batch.update('Test', {'name': 'new_item'}, where: 'name = ?', whereArgs: ['item']);
batch.delete('Test', where: 'name = ?', whereArgs: ['item']);
/// 批处理统一提交
results = await batch.commit();

在事务中,批处理的commit会等到事务提交后

dart 复制代码
await database.transaction((txn) async {
  var batch = txn.batch();
  
  // ...
  
  // commit but the actual commit will happen when the transaction is committed
  // however the data is available in this transaction
  /// 当事务被提交时才会真正的提交
  await batch.commit();
  
  //  ...
});
dart 复制代码
/// 设置批处理出现错误依然提交
await batch.commit(continueOnError: true);
表名和列名

SQLite的关键词,要避免使用作为实体(Entity)名。

sh 复制代码
"add","all","alter","and","as","autoincrement","between","case","check","collate","commit","constraint","create","default","deferrable","delete","distinct","drop","else","escape","except","exists","foreign","from","group","having","if","in","index","insert","intersect","into","is","isnull","join","limit","not","notnull","null","on","or","order","primary","references","select","set","table","then","to","transaction","union","unique","update","using","values","when","where"

sqflite的工具方法会进行处理,避免与关键字的冲突

dart 复制代码
db.query('table')
/// 等价于
db.rawQuery('SELECT * FROM "table"');

其它问题

VSCode 无法调试

Error connecting to the service protocol: failed to connect to http://127.0.0.1:51020/Kra7fZnYjeI=/ Error: Failed to register service methods on attached VM Service: registerService: (-32000) Service connection disposed

原来有成功过,后面发现一直都会有问题,前段时间突然不行,在长时间运行后就会报这个错误,但是单独在VSCode外部用flutter run命令能正常运行。

发现终端可以是把本地的端口转发的代理给去掉了。然后发现VSCode的代理有这样的说明,若未设置则会继承环境变量中的http_proxyhttps_proxy,我把代理加到.zshrc中,所以VSCode的默认会用代理,但是运行在真机上,手机没有代理,应该是这样影响了网络环境。

  1. .zshrc去掉代理的配置
  2. 重新打开VSCode && 运行 => 能正常调试

参考

  1. SQLite CRUD operations in Flutter
  2. sqflite-doc
  3. sqflite Migration example
相关推荐
2401_84100398几秒前
postgresql初体验
数据库·postgresql
再拼一次吧1 分钟前
MySql进阶学习
数据库·学习·mysql
在未来等你2 分钟前
高级SQL技巧:窗口函数与复杂查询优化实战
数据库·sql·性能优化·窗口函数·递归查询
TNTLWT39 分钟前
Qt文件:XML文件
xml·数据库·qt
zhou1851 小时前
【最新】MySQL 5.6 保姆级安装详细教程
java·数据库·python·mysql·php
心仪悦悦1 小时前
sparkSQL读入csv文件写入mysql
数据库·mysql
wxl7812271 小时前
基于自然语言转SQL的BI准确率如何?
数据库·bi·nl2sql
Elastic 中国社区官方博客1 小时前
将嵌入映射到 Elasticsearch 字段类型:semantic_text、dense_vector、sparse_vector
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
大力水手偷吃菠菜变成米老鼠2 小时前
数据库 1.0.1
数据库
Lao A(zhou liang)的菜园3 小时前
Oracle中如何解决BUFFER BUSY WAITS
数据库·oracle