☰
从零搭建灌装监控系统(九):SQLite与FreeSql ORM,让监控数据真正落盘
2026/9/30 2:41:03 网站建设 项目流程

SQLite 与 FreeSql ORM

这是「从零搭建灌装监控系统」系列第9篇。监控数据不应该只停留在内存里,这篇把 SQLite、FreeSql 和应用启动流程接起来,让采集到的生产记录、报警记录可以查询、追溯,也让后面的导出和日志查看有真正的数据来源。


软件重启后,昨天的记录去哪了

现场第一次试跑的时候,操作员问了我一个很朴素的问题:“刚才那批数据能不能查?”

当时界面上的曲线和状态都还在,程序也没有报错。可是软件一重启,内存集合清空,上一批记录就像没有发生过一样。监控程序如果只负责“现在显示什么”,它更像一个仪表盘,不像生产系统。

我不想一上来就引入服务器数据库。灌装线上的单机部署、离线运行和维护成本都要求存储方案简单一点:数据放在本地文件里,程序自己创建表,现场不用额外安装数据库服务。于是这一篇选择 SQLite 作为存储引擎,FreeSql 作为访问层。

这里的重点不是把 ORM 当成魔法,而是把三个边界说清楚:数据库什么时候初始化,实体如何变成表,业务服务如何使用同一个连接对象。


为什么选 SQLite + FreeSql

SQLite 是一个文件型数据库,适合单机监控软件:

  • 不需要独立的数据库服务;
  • 一个.db文件就能随程序和配置一起管理;
  • 支持事务、索引和常见查询;
  • 备份时复制数据库文件即可,部署简单。

但直接拼 SQL 也有问题。生产记录、报警记录、系统日志都会有查询条件,字符串拼接很快就会变成维护负担。FreeSql 提供了实体映射、分页、插入和查询能力,代码表达的还是 C# 对象和查询条件。

示例实体只保留教学需要的字段:

publicclassProductionRecord{[Column(IsIdentity=true,IsPrimary=true)]publiclongId{get;set;}publicDateTimeTime{get;set;}publicstringBatchNo{get;set;}="";publicdecimalVolume{get;set;}publicdecimalTemperature{get;set;}publicdoubleCycleTime{get;set;}publicstringOperator{get;set;}="";}

真实项目里还有更多字段,但文章里的实体要服务于解释,不需要把每个设备寄存器都搬进来。字段越多,读者越容易把注意力放到业务细节上,而不是数据模型设计。


DbProvider:数据库入口只创建一次

项目采用一个静态DbProvider作为数据库入口。它不负责业务查询,只负责创建并暴露 FreeSql 实例。

publicstaticclassDbProvider{privatestaticreadonlyobjectSyncRoot=new();privatestaticIFreeSql?_fsql;publicstaticIFreeSqlFsql=>_fsql??thrownewInvalidOperationException("数据库尚未初始化");publicstaticvoidInitialize(stringconnectionString){if(_fsql!=null)return;lock(SyncRoot){if(_fsql!=null)return;_fsql=newFreeSqlBuilder().UseConnectionString(DataType.Sqlite,connectionString).UseAutoSyncStructure(true).UseMonitorCommand((cmd,trace)=>{LogService.Verbose($"[SQL]{cmd.CommandText}\r\n ->{trace}");}).UseLazyLoading(false).Build();}}}

这里有两个容易被忽略的细节。

第一,Fsql不应该在未初始化时返回 null。让调用点直接得到清晰异常,比后面某个页面突然出现空引用更容易排查。

第二,Initialize使用双重检查。外层检查避免大多数调用进入锁,锁内检查保证多线程启动时仍然只创建一个实例。数据库初始化往往发生在应用启动阶段,但登录窗口、后台服务和主窗口初始化可能同时开始,不能假设它一定只有一个线程。


是

否

是

否

应用启动

Fsql 已创建?

直接返回

进入 lock

Fsql 已创建?

FreeSqlBuilder 构建实例

AutoSyncStructure 同步表结构

暴露 DbProvider.Fsql

AutoSyncStructure 适合什么场景

UseAutoSyncStructure(true)会根据实体结构同步数据表。开发阶段非常省事,新增一个实体属性后重新启动程序,表结构可以自动更新。

但这不是完整的数据库迁移系统。生产环境的表结构变更如果涉及删除字段、数据转换或大表迁移,不能只依赖自动同步。我的建议是:

  • 学习项目和单机小工具,可以打开自动同步;
  • 发布版本要记录实体变更,重要改动通过迁移脚本处理;
  • 不要把测试数据库直接复制成正式数据库;
  • 数据库路径放在工作目录或用户数据目录,不要写死开发机路径。

连接字符串应该由路径服务生成:

vardataDirectory=Path.Combine(workRoot,"Data");Directory.CreateDirectory(dataDirectory);vardatabasePath=Path.Combine(dataDirectory,"filltrack.db");DbProvider.Initialize($"Data Source={databasePath};");

文章中的filltrack.db是示例名,现场项目应当根据部署约定生成,不能把开发机盘符、用户名或内部目录写进代码。


业务服务只表达业务,不重复造连接

有了统一入口后,数据服务可以很薄:

publicstaticclassDataService{publicstaticTaskSaveProductionRecordAsync(ProductionRecordrecord){returnDbProvider.Fsql.Insert(record).ExecuteAffrowsAsync();}publicstaticTask<List<ProductionRecord>>QueryAsync(DateTimestart,DateTimeend){returnDbProvider.Fsql.Select<ProductionRecord>().Where(x=>x.Time>=start&&x.Time<end).OrderByDescending(x=>x.Time).ToListAsync();}}

注意结束时间用了“小于”,而不是把end直接写成“小于等于”。如果界面传入的是某一天的 00:00,半开区间[start, end)更容易组合,也不会因为毫秒精度造成边界重复。

不要在每个服务里重新new FreeSqlBuilder()。那样做会产生多个连接管理器,表结构同步、SQL 监控和事务边界都会变得混乱。服务可以是静态类,也可以通过 DI 注册成单例,关键是数据库访问入口要统一。


SQL 监控不是为了炫技

开发时打开 SQL 监控,可以回答几个很实际的问题:

  • 页面加载一次到底发了几条 SQL?
  • 分页查询有没有真的使用LIMIT/OFFSET?
  • 条件查询是否因为写法问题变成全表扫描?
  • 某个按钮卡顿时,是数据库慢还是设备通信慢?
.UseMonitorCommand((cmd,trace)=>{if(DebugSettings.EnableSqlLog)LogService.Verbose($"[SQL]{cmd.CommandText}\n{trace}");})

生产环境不建议把所有 SQL 都打到 UI 日志里。日志量太大时,真正的报警反而会被淹没。可以让 SQL 监控由调试配置开关控制,并且不要把密码、令牌等敏感值写入日志。


事务边界要跟业务动作走

一次灌装记录可能包含主记录、明细和统计数据。只插入主记录成功,并不代表整批数据成功。简单场景下单表插入够用;跨多表时要显式建立事务:

usingvarunitOfWork=DbProvider.Fsql.CreateUnitOfWork();try{unitOfWork.Orm.Insert(record).ExecuteAffrows();unitOfWork.Orm.Insert(detail).ExecuteAffrows();unitOfWork.Commit();}catch{unitOfWork.Rollback();throw;}

事务不是“所有代码都包起来”。设备通信、等待用户操作、文件导出都不应该放在数据库事务里,否则一个慢设备就能长时间锁住数据库。


踩坑记录

把数据库初始化放在页面构造函数

页面可能被导航多次,构造函数也可能重复执行。数据库初始化应该放在应用启动流程或核心服务初始化阶段,页面只查询数据。

自动同步当成版本迁移

开发时加字段很舒服,生产环境遇到字段删除、历史数据转换就不够了。正式发布前要明确数据库升级策略。

查询时间条件包含边界错误

“今天”的结束时间如果直接传当天 00:00,会查不到当天数据。统一采用[start, end),调用方把结束时间传到下一天 00:00。

把 SQL 全量打印到界面

调试时很有用,上线后会造成日志噪声。SQL 监控应当由调试开关控制,日志级别也要区分。


本篇小结

问题做法
数据保存在哪里SQLite 文件数据库
ORM 如何创建FreeSql + 实体模型
如何保证单例双重检查 + 锁
表结构怎么处理开发阶段 AutoSyncStructure,生产阶段记录迁移
数据访问放哪里DataService,不在页面里直接写 SQL
查询如何稳定统一时间半开区间,必要时增加索引

数据库接通以后,下一步就是把采集到的条码变成真正的生产记录。否则数据库只是一个空壳。


下期预告

第10篇:生产追溯:条码触发自动记录

下一篇从轮询数据里的条码变化开始,讨论如何防止同一个条码被重复入库,并把生产记录和当前设备状态关联起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询