news 2026/9/9 15:28:12

零到全栈项目重构:用SQLite替代内存存储,实现数据持久化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
零到全栈项目重构:用SQLite替代内存存储,实现数据持久化

做零到全栈项目,最容易在哪个阶段卡住?很多人会遇到一个相似的路口:功能已经能跑起来了,数据却留不住。早期为了快速验证想法,大家通常把用户、文章、配置全部塞在内存列表、全局字典甚至 JSON 文件里。程序一重启,数据清零,所有操作只能从头再来。这个问题在演示阶段不致命,但它是一个非常典型的信号:项目到了需要做一次结构性重构的时候了。

这节「6.5 重构,在项目中使用 SQLite」要解决的就是这个转折点。我们会做一次范围可控的重构:把原来用内存数组或 JSON 文件维护的数据,迁移到 SQLite 数据库里。SQLite 是嵌入式单文件关系型数据库,不需要安装服务端,没有账号密码,一个文件就是完整数据库,非常适合零到全栈项目在本地持久化阶段使用。

这篇文章会先讲清楚重构的边界,再解释 SQLite 为什么值得选,然后从建库、建表、CRUD 写到数据访问层,最后给出验证方法和常见坑。读完你至少能完成一次“换存储不动接口”的重构,并且理解为什么这种重构比继续堆功能更值得先做。

1. 这篇文章真正要解决的问题

1.1 “能跑”和“能用”之间,隔着一个持久化

在零到全栈的学习过程中,前几节通常重点解决“功能能不能跑通”:接口能不能返回数据、页面能不能展示列表、按钮能不能触发逻辑。这时候用内存存储是完全合理的,因为它最快、最直接,没有任何额外依赖。

但项目一旦从“演示”走向“自用”,问题就暴露了。最常见的现象就是:页面刷新后数据还在,因为前端状态还在;但后端进程一重启,所有用户、记录、配置全部丢失。再严重一点,如果用了 JSON 文件存储,还要处理多人同时写入、文件损坏、格式校验等问题。

这些问题的本质是:业务逻辑已经在内存里运行,但数据没有找到合适的落点。解决它不是一个功能需求,而是一个架构需求。这也是为什么这节不叫“SQLite 教程”,而叫“重构,在项目中使用 SQLite”——重点不是学会 SQLite 的语法,而是知道在什么时机、用什么方式把存储层换掉。

1.2 为什么选择 SQLite 而不是 MySQL

很多新手的第一反应是:既然要用数据库,为什么不直接上 MySQL?这个问题问得很有价值,但答案是场景决定的。MySQL 适合服务端有独立数据库进程、需要多机部署、多人并发访问的场景。而零到全栈项目在这个阶段通常是单机运行、单用户或少量用户使用,SQLite 反而更合适。

SQLite 不需要单独安装数据库服务,不需要配置端口、账号、权限,不需要维护数据目录。应用代码直接链接到数据库文件,读写都在本地完成。对学习项目来说,这意味着可以把注意力集中在“数据模型怎么设计”和“SQL 怎么写”上,而不是被数据库运维分散精力。

从后续发展角度看,先学会 SQLite 也不亏。SQL 语法在地层上是通用的,今天用 SQLite 写的建表语句、查询语句,将来切换到 MySQL 或 PostgreSQL 时,核心概念和大部分语法都能平移。选 SQLite 不是因为它是“玩具”,而是因为它的学习成本与现实需求在本阶段最匹配。

1.3 什么样的读者应该读这篇文章

如果你正在做自己的全栈练习项目,数据还是存在内存或 JSON 文件里,且已经感受到“重启就丢数据”的麻烦,这篇文章就是为你准备的。如果你是刚开始接触数据库,想找一个门槛最低的方式理解建表、外键、事务、参数化查询,SQLite 也是最好的起点。

这篇文章的示例以 Python 为主,因为 Python 标准库自带sqlite3模块,不需要额外安装任何依赖。同时文中也会给出 Node.js 的对应示例,如果你用的是 JavaScript 技术栈,可以对照迁移。无论哪种语言,重构的思路和 SQL 设计都是通用的。

2. SQLite 的核心特性与适用场景

2.1 什么是 SQLite

SQLite 是一个嵌入式关系型数据库管理系统,所谓“嵌入式”,就是它不是一个独立的服务进程,而是以库的形式链接到应用程序中。你的程序通过调用 SQLite 的接口读写数据库,数据库内容保存在一个普通文件里,例如app.db

和 MySQL、PostgreSQL 这种 C/S 架构的数据库相比,SQLite 有几个非常直观的特点:

  • 零配置:不需要初始化数据目录、创建用户、设置密码。
  • 单文件:整个数据库就是一个文件,备份就是复制文件。
  • 无网络端口:不监听任何端口,也就没有远程连接的安全风险。
  • 标准 SQL:支持大部分 SQL-92 标准,支持事务、外键、索引。
  • 跨平台:同一个数据库文件可以在不同操作系统之间复制使用。

这些特点让 SQLite 成为移动端应用、桌面软件、嵌入式设备、本地工具链中最常用的数据库。

2.2 SQLite 与 MySQL、PostgreSQL 的对比

很多人在选择数据库时会纠结,这里用一张表把关键差异列出来:

维度SQLiteMySQL / PostgreSQL
架构嵌入式库,无独立服务独立服务进程
部署成本极低,随应用分发即可需要安装、配置、运维
数据存储单个文件数据目录,多个文件
并发能力写操作全局串行,适合低并发行级锁,适合高并发
网络访问不支持,只能本机访问支持远程连接
使用场景本地存储、移动端、桌面软件、原型服务端业务系统、Web 应用
学习曲线中到高

这里需要特别说明一个常见误区:很多人觉得 SQLite 只能做练习用,不能上生产。实际上 iOS、Android 系统内置的数据库就是 SQLite,大量桌面软件如浏览器、聊天工具也在用 SQLite 做本地存储。它适合的场景是“单机、低并发、需要持久化”,不适合的场景是“多实例部署、高并发写入、复杂权限控制”。

2.3 在零到全栈项目中,SQLite 能解决什么问题

零到全栈项目的特点是:功能越来越多,但基础设施相对简单。这时候用 SQLite,可以解决三个具体问题。

第一,重启不丢数据。数据落盘后,程序进程退出再启动,之前写入的内容依然存在。

第二,让数据查询变得规范。内存列表只能通过遍历查找,而 SQLite 可以用WHEREJOINORDER BY完成复杂查询,代码量更少,表达力更强。

第三,为后续升级打基础。当你在项目里建立了数据访问层,将来切换 MySQL 时,只需要替换数据访问层的实现,业务层基本不用动。

还有一个隐性问题值得注意:如果一直不引入数据库,项目的数据操作会越来越散,每个功能各自操作自己的内存变量,时间长了很难维护。SQLite 的引入,本质上是在项目里建立了一个统一的“数据出口”。

3. 重构前的项目现状与重构目标

3.1 一个典型的“内存存储”实现

假设我们的项目已经实现了一个简单的用户管理功能。在重构之前,数据可能是这样存储的:

# 文件路径:store/memory_store.py(重构前的旧实现) class MemoryUserStore: def __init__(self): self._users = [] self._next_id = 1 def create(self, name, email): user = { "id": self._next_id, "name": name, "email": email, } self._next_id += 1 self._users.append(user) return user["id"] def find_by_id(self, user_id): for user in self._users: if user["id"] == user_id: return user return None def list_all(self): return self._users

这段代码的问题不是说它写得不对,而是它把全部数据保存在进程内存中。进程一结束,列表清空,所有用户数据归零。如果项目还需要保存文章、评论、任务等更多实体,这种内存数组会越来越多,数据之间的关系也会越来越难处理。

3.2 当前实现存在的主要问题

用内存存储或者 JSON 文件存储,在数据量小的时候确实够用,但项目继续推进会遇到四个问题。

第一是数据生命周期太短。进程重启、电脑关机,数据就没了。如果项目是用来记录真实信息,比如个人笔记、学习进度、任务清单,这种丢失是无法接受的。

第二是查询逻辑越来越复杂。当数据实体变多,比如用户下面有任务,任务下面有标签,用纯 Python 列表去维护关系,代码里会出现大量嵌套循环和条件判断,阅读成本急剧上升。

第三是写入并发问题。如果使用 JSON 文件存储,多个写入请求同时操作文件,很容易出现数据覆盖或者文件损坏。JavaScript 的事件循环虽然是单线程,但异步写入依然可能造成竞争条件。

第四是数据完整性不受保障。没有唯一约束,邮箱可能重复;没有外键约束,任务可能指向不存在的用户;没有事务,一次批量操作只写完一半程序就崩了,数据就停留在半成品状态。

这些问题的共同根源,是缺少一个真正能持久化、能约束数据关系的存储层。

3.3 重构的目标边界

说到重构,首先要明确边界。这节的重构不是推翻重来,而是保持对外行为基本不变的前提下,把内部存储机制换掉。具体目标有三个:

  • 持久化:数据写入后,程序重启依然存在。
  • 接口复用:之前调用createfind_by_id的代码尽量少改动。
  • 数据关系清晰:用外键和唯一约束把数据规则固化到数据库层。

重构最怕的是边重构边加新功能。那样出了问题,你很难判断是重构引入的 bug,还是新功能的 bug。所以本文的实践原则是:先只换存储,不增加业务功能,验证跑通后再继续演进。

4. 环境准备与 SQLite 工具选择

4.1 运行环境

本文示例使用 Python 3 编写,充分利用标准库自带的sqlite3模块。你不需要手动安装任何数据库程序,只要本机有 Python 3 即可。如果你想用 Node.js 复现代码,需要准备 Node.js 环境,并通过 npm 安装better-sqlite3依赖。

需要特别说明的是,SQLite 的版本由运行环境的动态库决定。Python 内置的sqlite3模块会附带一个较新版本的 SQLite,常用功能完全够用。如果你需要查看当前 SQLite 版本,可以执行:

python -c "import sqlite3; print(sqlite3.sqlite_version)"

4.2 官方命令行工具

虽然 Python 自带驱动,但验证数据库内容时,命令行工具会更直观。在 macOS 和大部分 Linux 发行版中,系统自带sqlite3命令。Windows 用户需要从 SQLite 官网下载命令行工具,或者通过包管理器安装。

进入数据库文件后,可以用.tables查看表列表,.schema查看建表语句,SELECT查询数据:

sqlite3 app.db
.tables .schema users SELECT * FROM users;

这个工具很适合在开发阶段快速确认数据是否正确写入。

4.3 图形化工具:DB Browser for SQLite

如果你不习惯命令行,推荐使用开源免费的 DB Browser for SQLite。它支持 Windows、macOS、Linux,可视化查看表结构、执行 SQL、导入导出数据。对于零到全栈项目来说,它比商业数据库客户端更轻量,也没有授权问题。

实际开发中,我喜欢用这样的分工:代码里写好建表和 CRUD,命令行用来快速验证,遇到复杂查询先用 DB Browser 试 SQL,确认无误后复制回代码。这个流程可以减少很多试错成本。

5. 在项目中引入 SQLite:建库、建表与基础 CRUD

5.1 项目目录结构设计

在动手写代码之前,先规划一下目录结构。这里采用了数据访问层的分离思路,把数据库连接、建表、数据操作分别放在不同模块中:

project/ ├── app.py # 业务入口 ├── db/ │ ├── __init__.py │ ├── database.py # 连接管理与建表 │ └── user_repository.py # 用户数据访问 └── scripts/ └── demo.py # 手动验证脚本

这样的好处是:业务层不需要知道数据库是怎么连接的,只需要调用UserRepository的方法。未来如果切换数据库,只需要重写db包内部实现。

5.2 初始化数据库与建表

第一步是建立连接管理模块。这里设置row_factory = sqlite3.Row,让查询结果可以通过字段名访问;然后开启外键约束,确保任务表引用用户表时不会出现“悬空引用”。

# 文件路径:db/database.py import sqlite3 from pathlib import Path DB_PATH = Path(__file__).resolve().parent.parent / "app.db" def get_connection(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") return conn def init_db(): conn = get_connection() conn.executescript(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); """) conn.commit() conn.close()

这段 SQL 做了几件重要的事情。users表的id使用INTEGER PRIMARY KEY AUTOINCREMENT,SQLite 会自动生成递增的主键;email加了UNIQUE约束,从数据库层面保证邮箱不重复;tasks表通过FOREIGN KEY (user_id) REFERENCES users(id)建立外键关系,ON DELETE CASCADE表示删除用户时自动删除其任务。

5.3 数据访问层:基础 CRUD

建表完成后,开始写最核心的UserRepository类。这里的关键是参数化查询,用?占位符替代字符串拼接,既能防止 SQL 注入,也让 SQL 语句更清晰。

# 文件路径:db/user_repository.py from db.database import get_connection class UserRepository: def create(self, name, email): with get_connection() as conn: cursor = conn.execute( "INSERT INTO users (name, email) VALUES (?, ?)", (name, email) ) return cursor.lastrowid def find_by_id(self, user_id): with get_connection() as conn: row = conn.execute( "SELECT * FROM users WHERE id = ?", (user_id,) ).fetchone() return dict(row) if row else None def list_all(self): with get_connection() as conn: rows = conn.execute( "SELECT id, name, email FROM users ORDER BY id DESC" ).fetchall() return [dict(row) for row in rows] def update_email(self, user_id, new_email): with get_connection() as conn: cursor = conn.execute( "UPDATE users SET email = ? WHERE id = ?", (new_email, user_id) ) return cursor.rowcount def delete(self, user_id): with get_connection() as conn: cursor = conn.execute( "DELETE FROM users WHERE id = ?", (user_id,) ) return cursor.rowcount

with get_connection() as conn这段代码利用sqlite3连接对象的上下文管理器,在操作成功后自动提交事务,在发生异常时自动回滚。lastrowid可以拿到刚插入记录的自增主键,rowcount则是受影响的行数。

5.4 多表查询:JOIN 示例

当数据进入关系型数据库后,最典型的受益场景就是 JOIN 多表查询。比如查出某个用户的任务列表,不用写循环去内存里匹配,一句 SQL 就能完成:

SELECT users.name, tasks.title, tasks.done FROM users JOIN tasks ON tasks.user_id = users.id WHERE users.id = ?;

在 Python 中调用这段 SQL 时,同样使用参数化查询。先通过UserRepository.find_by_id拿到用户,再执行 JOIN 查询,返回的每一行都包含用户姓名和任务标题,数据关系一目了然。

5.5 Node.js 技术栈的对应实现

如果你的项目基于 Node.js,推荐使用better-sqlite3,它是同步 API,性能好且代码直观。安装命令如下:

npm install better-sqlite3

对应的建表和插入示例:

// 文件路径:db/index.js const Database = require('better-sqlite3'); const db = new Database('app.db'); db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE ); `); const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)'); const info = insert.run('李四', 'lisi@example.com'); console.log('insert id:', info.lastInsertRowid); const row = db.prepare('SELECT * FROM users WHERE id = ?').get(info.lastInsertRowid); console.log('row:', row);

Node.js 版的思路和 Python 版完全一致:先建立连接,再执行 SQL,最后获取结果。掌握了一边的思路,另一边的代码很容易迁移。

6. 核心重构:把内存存储替换为数据访问层

6.1 重构第一步:让业务层面向接口编程

在替换存储之前,我们先做一件重要的事情:让业务层只依赖UserRepository,而不是直接依赖内存列表或者数据库连接。也就是说,业务层调用的是“仓库”,而不是“列表”或者“连接”。

这样做的好处是,替换存储方案时,业务层的代码不需要动。之前业务层可能会这样写:

# 重构前业务代码:直接操作内存存储 store = MemoryUserStore() user_id = store.create("张三", "zhangsan@example.com")

重构后只需要换成:

# 重构后业务代码:使用统一的数据访问层 repo = UserRepository() user_id = repo.create("张三", "zhangsan@example.com")

从业务层的视角看,create方法签名没有变化,变化的只是底层实现。这就是重构中常说的“保持接口稳定,替换内部实现”。

6.2 重构第二步:把查询逻辑改造成 SQL

内存存储时代,查找一个用户需要遍历整个列表。数据量小的时候无所谓,但一旦数据量增长,每次遍历都是 O(n) 的时间复杂度,而且代码里会塞进大量循环。

改成 SQL 之后,查找逻辑变成一句话:

def find_by_email(self, email): with get_connection() as conn: row = conn.execute( "SELECT * FROM users WHERE email = ?", (email,) ).fetchone() return dict(row) if row else None

这里体现了关系型数据库的核心价值:数据操作交给 SQL 引擎处理,应用的职责是组织数据和呈现结果。

6.3 重构第三步:引入事务保证数据一致性

如果一个业务操作需要同时写多张表,比如“创建用户并给该用户分配三个默认任务”,一次性写多个 INSERT 就必须考虑事务。事务的四个特性 ACID 在这里最有用:要么全部成功,要么全部回滚。

def create_user_with_tasks(repo, task_repo, name, email, tasks): with get_connection() as conn: try: conn.execute("BEGIN") cursor = conn.execute( "INSERT INTO users (name, email) VALUES (?, ?)", (name, email) ) user_id = cursor.lastrowid for title in tasks: conn.execute( "INSERT INTO tasks (user_id, title) VALUES (?, ?)", (user_id, title) ) conn.commit() return user_id except Exception: conn.rollback() raise

如果中途某个 INSERT 失败,rollback()会把前面的写入全部撤销,数据库不会出现“用户已创建但任务没创建成功”的中间状态。事务是在重构中最容易被忽略、但影响最大的一部分。

6.4 重构的验收标准

重构完成不代表“改完了”,还要对照几个验收标准确认没有破坏原有功能:

  • 原有业务逻辑的入口函数签名是否保持不变。
  • 创建、查询、更新、删除的结果是否与重构前一致。
  • 程序重启后,数据是否仍然存在。
  • 顶层接口能否在测试环境中跑通。

如果以上四项都满足,这次重构就达到了目标。

7. 运行结果与效果验证

7.1 编写验证脚本

为了验证重构效果,写一个简单的手动验证脚本,依次执行初始化、创建、查询、更新、删除等操作:

# 文件路径:scripts/demo.py from db.database import init_db from db.user_repository import UserRepository if __name__ == "__main__": init_db() repo = UserRepository() uid = repo.create("张三", "zhangsan@example.com") print("create user id:", uid) user = repo.find_by_id(uid) print("find by id:", user) repo.update_email(uid, "zhangsan_new@example.com") print("after update:", repo.find_by_id(uid)) users = repo.list_all() print("all users:", users) repo.delete(uid) print("after delete:", repo.find_by_id(uid))

运行脚本:

python scripts/demo.py

7.2 预期输出

如果一切正常,输出大致如下:

create user id: 1 find by id: {'id': 1, 'name': '张三', 'email': 'zhangsan@example.com', 'created_at': '2025-01-01 12:00:00'} after update: {'id': 1, 'name': '张三', 'email': 'zhangsan_new@example.com', 'created_at': '2025-01-01 12:00:00'} all users: [{'id': 1, 'name': '张三', 'email': 'zhangsan_new@example.com', 'created_at': '2025-01-01 12:00:00'}] after delete: None

created_at的具体时间取决于运行时刻。到这里,CRUD 基本验证通过。

7.3 验证持久化效果

CRUD 验证通过后,最关键的验证是持久化:先运行一次脚本创建一个用户,但不删除;然后关闭程序,重新运行一次查询脚本,确认用户仍然存在。

python scripts/create_user.py python scripts/list_users.py

如果第二次运行时用户数据还能查询出来,说明数据已经成功落盘。这时候再打开项目目录,会看到一个app.db文件,可以用命令行工具直接查看:

sqlite3 app.db SELECT * FROM users;

如果看到刚刚创建的用户记录,持久化验证就成功了。如果数据不存在,优先检查DB_PATH指向的文件路径,确认程序读写的是同一个数据库文件。

8. 常见问题与排查思路

从内存存储切到 SQLite,过程中会遇到一些典型问题。下面列出最常出现的几种。

问题现象可能原因排查方式解决方案
程序启动后提示 no such table: users建表语句未执行,或init_db()未调用检查入口是否调用了init_db()在应用启动入口显式调用init_db()
插入数据显示 database is locked其他连接持有写锁未释放检查是否有长事务未提交缩短事务时间,开启 WAL 模式
数据写入成功但查询不到连接未提交事务确认是否使用with语句或显式commit()统一使用上下文管理器管理连接
程序一重启数据就消失DB_PATH指向临时目录或相对路径漂移打印DB_PATH确认路径使用基于项目根目录的绝对路径
插入数据报 UNIQUE constraint failed唯一约束字段重复查询已存在的数据在应用层做前置校验或使用 ON CONFLICT 语句
中文数据乱码终端编码问题,数据库本身没问题用 DB Browser 查看确认设置终端 UTF-8 编码
更新返回值总是 0更新的数据与原数据相同,或主键不存在先查询记录是否存在根据业务需要区分“无记录”和“值相同”

在这些问题中,database is locked是新手最常遇到,也最容易慌的。它通常发生在多线程或多次连接同时写入的场景。解决思路是确保每次写入操作尽快提交,不要在一个事务里做耗时操作。SQLite 也支持 WAL 模式,可以显著提升并发读写体验,这个后面会讲。

9. 最佳实践与工程建议

9.1 数据文件路径要稳定

数据文件路径是新手最容易忽略的坑。如果使用相对路径,比如sqlite3.connect("app.db"),数据库文件会落在“当前工作目录”。从项目根目录运行和从scripts目录运行,生成的文件位置完全不同。

建议统一使用基于项目根目录的绝对路径。Python 中可以通过Path(__file__).resolve().parent.parent / "app.db"计算,这样无论从哪里启动程序,数据库都固定在同一个位置。

9.2 开启外键和 WAL 模式

PRAGMA foreign_keys = ON必须放在每次连接建立后执行,因为 SQLite 的外键约束默认是关闭的。如果忘记开启,外键虽然定义在表结构里,但不会生效。

对于本地单机项目,WAL(Write-Ahead Logging)模式值得开启。它会把写操作先追加到独立的-wal文件中,读操作不会被写操作阻塞,适合读写并存的场景。开启方式是在连接后执行:

PRAGMA journal_mode = WAL;

注意 WAL 模式会产生额外的app.db-walapp.db-shm文件,备份数据库时这三个文件要一起处理,或者先执行一次 checkpoint 合并。

9.3 参数化查询是底线

永远不要用字符串格式化拼 SQL。这不仅是为了防注入,更是为了代码可维护性和执行计划复用。下面这种写法必须避免:

# 错误示例:不要用 f-string 拼接 SQL cursor = conn.execute(f"SELECT * FROM users WHERE email = '{email}'")

正确的做法是使用?占位符,把参数作为元组传给execute。Python 的sqlite3模块以及 Node.js 的better-sqlite3都支持这种方式。

9.4 建表变更要用迁移方案

随着项目迭代,你一定会遇到需要给表加字段的情况。直接在旧表上执行ALTER TABLE可以解决简单需求,比如加一列:

ALTER TABLE users ADD COLUMN nickname TEXT;

但如果涉及改字段类型、拆分表、合并表,就需要设计迁移流程。零到全栈阶段不需要引入复杂的迁移工具,但可以在项目里预留一个migrations目录,按时间或版本保存 SQL 文件:

db/migrations/ ├── 001_create_users.sql ├── 002_create_tasks.sql └── 003_add_nickname.sql

每次启动时读取已执行过的迁移记录,没有执行过的按顺序执行。这样表结构的演进可以得到记录和回溯。

9.5 备份与恢复

SQLite 的备份非常简单,本质上就是复制文件。但要保证数据一致性,最好在执行备份时避免写入操作。Python 的sqlite3模块提供了备份 API:

import sqlite3 source = sqlite3.connect("app.db") target = sqlite3.connect("backup.db") source.backup(target) target.close() source.close()

这个 API 会生成一个一致性快照,比直接cp文件更安全。对零到全栈项目来说,每天定时备份或者手动备份一次就够了。

9.6 什么时候不该用 SQLite

虽然 SQLite 很好,但也要知道它的边界。如果你预计项目会部署到服务器上多进程访问,且写入并发很高,SQLite 就不太合适了。写操作全局串行意味着同一时间只能有一个连接写入,高并发场景下会成为瓶颈。

另外,如果项目需要跨设备、跨地区共享数据,SQLite 作为单机文件数据库也无法满足需求。这时候需要换成服务端数据库,并把数据访问逻辑抽象成 API。好在本文做重构时已经把数据访问收拢到了Repository层,将来切换不会伤筋动骨。

10. 总结与后续学习方向

这一节的核心不是“SQLite 的语法大全”,而是一次完整的最小重构:理解内存存储的局限,选型 SQLite,建立数据访问层,用事务保证一致性,再通过验证脚本证明重构没有破坏功能。整个过程下来,你的项目第一次拥有了真正可靠的数据落点。

接下来值得深入的方向有三个。第一是学习更多 SQL 查询技巧,比如分组聚合、子查询、窗口函数,这些在后续报表类功能中非常有用。第二是研究索引原理,理解为什么某些字段加索引后查询速度大幅提升,为什么有些场景加索引反而没效果。第三是考虑把数据访问层进一步抽象成通用模块,为将来接入 MySQL 或 PostgreSQL 做准备。

如果在动手实践时遇到问题,建议先把报错信息完整读一遍,再去定位连接管理和查询语句的部分。数据库问题大多不是玄学,多数情况下错误信息已经把原因写得很清楚了。建议把这篇文章收藏备用,搭好骨架后,后续为项目添加实体、字段、查询都会顺很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 15:27:00

opencode不是开源项目,而是AI编程代理工具的本地运行时环境

1. “opencode”不是开源项目,而是AI编程代理工具的误传代称——先破除一个广泛存在的认知偏差“opencode”这个词在最近三个月的开发者社区里高频出现,但它既不是GitHub上某个star过万的开源仓库,也不是Linux基金会或Apache软件基金会旗下的…

作者头像 李华
网站建设 2026/9/9 15:22:38

服装零售数字化转型:业务与IT联合规划的核心框架与实践

1. 为什么服装零售企业的数字化,必须先做业务和IT的联合规划做这版“业务与IT转型规划”PPT之前,我正好在帮一家年营收二十多亿的服装企业做数字化落地方案。这家企业的老板一开始的想法非常直接:“上个中台、上一套新ERP,再搞个数…

作者头像 李华