到了数据库这一阶段的最后一章,最重要的已经不是再讲一个零散知识点,而是把前面学过的建表、增删改查、参数化查询、事务、数据库设计这些内容,真正落成一个能跑的小项目。
这一章我不准备只给你一堆零散代码。 我们直接按真实项目思路,做一个完整但不复杂的命令行版用户信息管理系统。
这个系统要解决的事很明确:
能新增用户 能查看所有用户 能按条件查询用户 能修改用户信息 能删除用户 能避免明显的数据混乱 代码结构尽量像正式项目,而不是把所有逻辑塞进一个大循环里
你会发现,这一章非常像前面很多内容的一次汇总。
数据库连接要用到。 建表要用到。 增删改查要用到。 参数化查询要用到。 事务意识要用到。 数据库设计也要用到。
所以这章特别重要。 它是数据库阶段第一次真正把知识点串成一个完整小系统。
一、先别急着写代码,先把项目需求想清楚
很多人做小项目,最容易犯的错就是一上来就写:
import sqlite3
然后写着写着,逻辑越来越乱,后面自己也说不清到底程序要干嘛。
更稳的做法是先想清楚,这个系统到底要管理哪些信息。
既然是用户信息管理系统,最基本的用户数据通常会包括:
用户编号 用户名 年龄 手机号 邮箱 城市 注册时间
你会发现,这些字段都在描述同一个对象:用户。
所以从数据库设计角度看,这里很自然就是一张用户表。
这一步非常重要。
因为前一章刚讲过数据库设计。 现在你要真正开始把“按对象设计表”的思路用起来。
这里没有必要一上来拆很多表。 因为这个项目当前阶段的核心对象就是用户本身。 所以一张 users 表就足够清楚。
二、先把表结构设计好,别边写边想
我们先设计用户表。
比较实用的字段可以这样定:
id:主键,自增username:用户名,不能为空age:年龄phone:手机号email:邮箱city:城市created_at:注册时间
为什么这样设计
id 是最稳定的唯一标识,后面修改和删除时特别好用。username 作为最基本的信息,通常不能为空。phone 和 email 在真实项目里也很常见。created_at 能帮助你看这条数据是什么时候进入系统的。
这里你会看到一个很真实的设计感:
数据库表不是为了“能装下数据”就行。 而是要让后续的查询、修改、排序、管理都顺手。
所以表设计一定要先认真一点。
三、先写数据库初始化函数,让程序一启动就有表可用
真实项目里,一个特别好的习惯是:
程序启动时,先检查数据库和表是否准备好了。
如果没有,就自动创建。
这样你后面每次运行程序时,不用反复手工建表。
代码可以这样写:
import sqlite3DB_NAME = "user_system.db"definit_db(): conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, age INTEGER, phone TEXT, email TEXT, city TEXT, created_at TEXT NOT NULL ) """) conn.commit() conn.close()
这段代码很朴素,但特别有项目味道。
它把“建表”这件事独立成了一个初始化动作。 这样主程序里就不用反复关心表存不存在。
这和你前面学模块化、函数拆分的思想是完全一致的。
四、为什么这里先选 SQLite,而不是一上来用 MySQL
这一章既然是数据库实战,很多人可能会问:
前面刚讲了 MySQL,为什么这里还用 SQLite
答案很简单:
因为这一章的重点是把数据库项目流程走通。 而不是把环境配置难度拉高。
SQLite 特别适合这种阶段。
它足够真实。 又足够轻。 不用先搭独立服务。 代码更容易聚焦在“数据库操作逻辑”本身。
等你把这一章的小系统真正做顺了,再迁移到 MySQL,其实会非常自然。 因为项目思路不变,变化的只是连接方式和运行环境。
所以这不是倒退。 而是为了把项目能力练扎实。
五、真实项目里,数据库连接最好别到处乱写
很多初学者做项目时,最容易出现的一种代码味道是:
每个函数里都随手写一遍连接逻辑,而且风格还不统一。
比如有的函数连了不关。 有的函数忘了提交。 有的函数连接名都不一样。
所以我们先把获取连接这件事单独封装一下:
defget_connection():return sqlite3.connect(DB_NAME)
虽然这个函数看起来简单得像没必要, 但它的价值在于:
数据库连接入口统一了。
以后如果你想切换路径、加配置、甚至迁移到别的数据库,修改点会更集中。
这就是为什么正式项目代码通常会很重视这种小封装。 不是为了显得高级。 而是为了后期更好维护。
六、第一个功能,先做新增用户
新增是最自然的第一步。
因为一个用户管理系统,如果连“加人”都做不了,那后面很多功能都没得玩。
不过这里一定不要只图快。 我们要顺手把前面学过的两个重要习惯带进来:
第一,参数化查询。 第二,注册时间自动生成。
代码可以这样写:
from datetime import datetimedefadd_user(username, age, phone, email, city): conn = get_connection() cursor = conn.cursor() created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cursor.execute(""" INSERT INTO users (username, age, phone, email, city, created_at) VALUES (?, ?, ?, ?, ?, ?) """, (username, age, phone, email, city, created_at)) conn.commit() conn.close()
这里有几个点你要特别看清楚。
我们没有自己拼 SQL 字符串。 所有输入都通过参数化查询传进去。
这一步特别重要。 因为从小项目开始就把习惯养对,后面你就不会总想回去用 f-string 拼 SQL。
同时,created_at 是由程序生成的。 这会让每条用户记录都带着明确的时间信息,后面无论排序还是排查,都会方便很多。
七、新增用户时,最好顺手做一点基础校验
如果你现在只想让功能“能跑”,上面的代码已经够了。 但如果你想让项目更像样一点,最好别让程序什么垃圾输入都接。
例如:
用户名为空 年龄不是数字 手机号太离谱 邮箱明显不对
这些都值得做最基础的拦截。
当前阶段我们不追求特别复杂,只做一层很实用的基本校验:
defvalidate_user_data(username, age):ifnot username or username.strip() == "":returnFalse, "用户名不能为空"if age isnotNone:try: age = int(age)if age < 0:returnFalse, "年龄不能是负数"except ValueError:returnFalse, "年龄必须是整数"returnTrue, "校验通过"
然后在新增前调用:
defadd_user(username, age, phone, email, city): valid, message = validate_user_data(username, age)ifnot valid: print("新增失败:", message)return conn = get_connection() cursor = conn.cursor() created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cursor.execute(""" INSERT INTO users (username, age, phone, email, city, created_at) VALUES (?, ?, ?, ?, ?, ?) """, (username, age, phone, email, city, created_at)) conn.commit() conn.close() print("用户新增成功")
这一步特别能体现项目思维。
不是所有逻辑都塞进 SQL 里。 而是程序先做基础校验,再去写数据库。
这会让错误更早暴露,也更容易和用户交互。
八、第二个功能,查看所有用户
新增完了,最自然的下一步就是查看数据。
最基础的读取代码可以写成:
deflist_users(): conn = get_connection() cursor = conn.cursor() cursor.execute(""" SELECT id, username, age, phone, email, city, created_at FROM users ORDER BY id ASC """) rows = cursor.fetchall() conn.close()return rows
为什么这里我没有用 SELECT *。
不是说 SELECT * 完全不行。 而是前面已经讲过,正式一点的代码里,最好尽量明确你到底要哪些字段。
这有两个好处:
可读性更强。 后面表结构变化时,影响更可控。
再往前一步,如果你想让打印更友好一点,还可以单独写一个展示函数:
defprint_users(rows):ifnot rows: print("当前没有用户数据")returnfor row in rows: print(row)
虽然简单,但它让“查数据”和“怎么显示”分开了。 这就是越来越像正式代码的感觉。
九、为什么展示逻辑最好和数据库逻辑分开
这个点非常实用。
很多初学者会把函数写成这样:
deflist_users(): conn = get_connection() cursor = conn.cursor() cursor.execute("SELECT * FROM users") rows = cursor.fetchall() print(rows) conn.close()
这当然也能跑。
但问题是,这个函数既负责查数据库,又负责打印结果。 以后如果你想把结果拿去做别的用途,比如:
导出 CSV 写入日志 提供给接口 继续筛选
它就不够灵活了。
所以更像样的写法通常是:
数据库函数返回数据。 展示函数负责显示数据。
这种拆分你前面在很多章节已经见过了。 它在数据库项目里同样非常重要。
十、第三个功能,按条件查询用户
到了这一步,数据库的真正价值才会开始体现出来。
因为用户管理系统最常见的需求之一不是“把全部用户都列出来”,而是:
按 id 找人 按用户名找人 按城市筛选 按年龄范围查
我们先做一个最常用的小功能:按用户名模糊搜索。
defsearch_users_by_name(keyword): conn = get_connection() cursor = conn.cursor() cursor.execute(""" SELECT id, username, age, phone, email, city, created_at FROM users WHERE username LIKE ? ORDER BY id ASC """, (f"%{keyword}%",)) rows = cursor.fetchall() conn.close()return rows
这里你第一次会比较直观地感受到:
数据库查找,比文件遍历更像一套专门的能力。
我们用了 LIKE 来做模糊匹配。 而且依然保留了参数化查询。
这点很重要。 即便是 LIKE,也不应该为了图省事回去拼 SQL。
十一、按 id 精确查询,也特别常见
尤其是后面的修改和删除,几乎都应该优先按 id 来做。
因为 id 最稳,最唯一,不容易歧义。
defget_user_by_id(user_id): conn = get_connection() cursor = conn.cursor() cursor.execute(""" SELECT id, username, age, phone, email, city, created_at FROM users WHERE id = ? """, (user_id,)) row = cursor.fetchone() conn.close()return row
这类函数非常值得单独保留。
为什么
因为后面很多功能会复用它。
修改前先查一下用户是否存在。 删除前先查一下要删的是谁。 展示单个用户详情时也可以直接用。
这就是为什么我一直说,小系统也要有一点结构感。 因为只要你稍微设计一下,后面的逻辑会顺很多。
十二、第四个功能,修改用户信息
修改是用户管理系统里特别核心的功能。 而且从数据库角度看,它特别能体现“按记录精确操作”的价值。
先看一个最基础版本,比如按 id 修改一个用户的信息:
defupdate_user(user_id, username, age, phone, email, city): conn = get_connection() cursor = conn.cursor() cursor.execute(""" UPDATE users SET username = ?, age = ?, phone = ?, email = ?, city = ? WHERE id = ? """, (username, age, phone, email, city, user_id)) conn.commit() conn.close()
这段代码虽然不长,但特别值得你体会。
修改动作不是“整份数据重写”。 而是精确锁定一条记录,再修改这条记录的字段。
这正是数据库和文件思维最大的差别之一。
不过这里还有个重要问题没解决:
如果 user_id 根本不存在呢 如果用户名为空呢
所以更稳一点的写法,还得继续往前补逻辑。
十三、修改前,最好先确认用户存在
这是非常像正式项目的一步。
很多新手写修改功能时,直接上来就 UPDATE。 虽然 SQL 执行不一定会报错,但如果这条记录根本不存在,程序逻辑上其实已经有问题了。
所以更推荐这样写:
defupdate_user(user_id, username, age, phone, email, city): old_user = get_user_by_id(user_id)if old_user isNone: print("修改失败:用户不存在")return valid, message = validate_user_data(username, age)ifnot valid: print("修改失败:", message)return conn = get_connection() cursor = conn.cursor() cursor.execute(""" UPDATE users SET username = ?, age = ?, phone = ?, email = ?, city = ? WHERE id = ? """, (username, age, phone, email, city, user_id)) conn.commit() conn.close() print("用户信息修改成功")
你看,这样逻辑就完整多了。
先确认目标存在。 再做输入校验。 最后再更新数据库。
这已经非常像真实后台管理系统的基本思路了。
十四、第五个功能,删除用户
删除功能表面看最简单,但它特别适合练你的数据库习惯。
因为删数据有两个特别重要的原则:
先确认删的是谁。 尽量按主键删。
最基础版本可以这样写:
defdelete_user(user_id): user = get_user_by_id(user_id)if user isNone: print("删除失败:用户不存在")return conn = get_connection() cursor = conn.cursor() cursor.execute("DELETE FROM users WHERE id = ?", (user_id,)) conn.commit() conn.close() print("用户删除成功")
这段代码很朴素,但它有几个非常好的习惯:
删除前先查目标是否存在。 删除时按 id,而不是按名字。 SQL 依然参数化。 删除完成后明确给出反馈。
这就比“上来一条 DELETE 然后不管不顾”要稳得多。
十五、一个更像项目的小升级:加菜单,把功能真正串起来
前面我们写的都还是一个个零散函数。 要让它更像“系统”,最自然的方式就是加一个菜单入口。
例如:
defshow_menu(): print("\n==== 用户信息管理系统 ====") print("1. 新增用户") print("2. 查看全部用户") print("3. 按姓名搜索用户") print("4. 修改用户") print("5. 删除用户") print("0. 退出系统")
然后写主循环:
defmain(): init_db()whileTrue: show_menu() choice = input("请输入功能编号:").strip()if choice == "1": username = input("用户名:").strip() age = input("年龄:").strip() phone = input("手机号:").strip() email = input("邮箱:").strip() city = input("城市:").strip() age = int(age) if age elseNone add_user(username, age, phone, email, city)elif choice == "2": rows = list_users() print_users(rows)elif choice == "3": keyword = input("请输入用户名关键词:").strip() rows = search_users_by_name(keyword) print_users(rows)elif choice == "4": user_id = int(input("要修改的用户ID:").strip()) username = input("新用户名:").strip() age = input("新年龄:").strip() phone = input("新手机号:").strip() email = input("新邮箱:").strip() city = input("新城市:").strip() age = int(age) if age elseNone update_user(user_id, username, age, phone, email, city)elif choice == "5": user_id = int(input("要删除的用户ID:").strip()) delete_user(user_id)elif choice == "0": print("系统已退出")breakelse: print("输入无效,请重新选择")
最后加入口:
if __name__ == "__main__": main()
到这里,这个小项目就真的“活起来”了。
它不再只是几条零散 SQL。 而是一个用户可以操作的完整流程。
十六、为什么这个小系统已经很有项目味道了
因为它已经具备了一个数据库小项目最基础的闭环:
先初始化数据库。 再通过菜单接收用户输入。 根据输入调用不同业务函数。 业务函数内部操作数据库。 最后给出结果反馈。
这个闭环特别重要。
很多初学者学数据库时,容易一直停留在:
写几条 SQL 打印几条结果
这当然是必要阶段。 但真正的项目能力,来自于你能不能把这些操作组织起来,形成一个可连续使用的小系统。
这就是这一章的价值所在。
十七、再给项目加一点真实感:避免手机号和邮箱明显重复
到了这里,我们已经不只是追求“能跑”,而是开始让它更像一个实际系统。
一个非常实用的小优化是:
不要让手机号或邮箱随便重复。
因为真实用户系统里,手机号和邮箱通常很重要。 至少它们常常需要保持一定唯一性。
有两种做法。
第一种,在程序层面先查一下是否存在。 第二种,在表结构层面直接加唯一约束。
这里我更推荐第二种,因为它更靠近数据库思维。
例如建表时改成:
definit_db(): conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, age INTEGER, phone TEXT UNIQUE, email TEXT UNIQUE, city TEXT, created_at TEXT NOT NULL ) """) conn.commit() conn.close()
这样数据库自己就会帮你拦住明显的重复手机号和重复邮箱。
这特别有数据库味道。
不是所有规则都在 Python if 里判断。 有些规则,应该直接成为数据库结构的一部分。
这就是为什么我前面一直说,数据库设计和程序逻辑是一起工作的。
十八、加上异常处理,项目会稳很多
有了唯一约束以后,如果插入重复手机号或邮箱,数据库就可能报异常。 所以这时就应该把异常处理接上。
例如:
defadd_user(username, age, phone, email, city): valid, message = validate_user_data(username, age)ifnot valid: print("新增失败:", message)return conn = get_connection() cursor = conn.cursor() created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")try: cursor.execute(""" INSERT INTO users (username, age, phone, email, city, created_at) VALUES (?, ?, ?, ?, ?, ?) """, (username, age, phone, email, city, created_at)) conn.commit() print("用户新增成功")except sqlite3.IntegrityError as e: conn.rollback() print("新增失败:手机号或邮箱可能已存在", e)finally: conn.close()
这段代码很有代表性。
参数化查询继续保留。 事务意识也开始体现出来。 出错时回滚。 最后一定关闭连接。
这已经开始有一点正式项目代码的味道了。
十九、如果想让查询结果更好读,可以把元组转成字典
默认情况下,SQLite 查询出来的是元组。 比如:
(1, '张三', 18, '13800000000', 'test@test.com', '北京', '2026-03-31 10:00:00')
这当然能看,但体验一般。
更适合后续处理的一种方式,是把结果整理成字典。
例如:
deflist_users(): conn = get_connection() cursor = conn.cursor() cursor.execute(""" SELECT id, username, age, phone, email, city, created_at FROM users ORDER BY id ASC """) rows = cursor.fetchall() conn.close() result = []for row in rows: result.append({"id": row[0],"username": row[1],"age": row[2],"phone": row[3],"email": row[4],"city": row[5],"created_at": row[6] })return result
再改一下打印函数:
defprint_users(rows):ifnot rows: print("当前没有用户数据")returnfor user in rows: print(f"ID: {user['id']} | "f"用户名: {user['username']} | "f"年龄: {user['age']} | "f"手机号: {user['phone']} | "f"邮箱: {user['email']} | "f"城市: {user['city']} | "f"注册时间: {user['created_at']}" )
这会让整个系统的输出体验好很多。 而且后面如果你想导出 JSON 或对接接口,也会更自然。
二十、这个项目真正让你练到的,不只是 CRUD
如果你回头看,会发现这章做的不是单纯的数据库练习题。
它把很多前面学过的东西都真正拉进来了。
数据库初始化 连接封装 参数化查询 唯一约束 异常处理 事务提交与回滚 按主键做精确修改和删除 菜单式程序组织 函数拆分与职责分离
这些东西一叠加,这就不再只是“会几句 SQL”。 而是真正开始像一个小型管理系统。
这就是为什么我说数据库阶段最重要的,不是背更多语法。 而是把数据库能力变成项目能力。
二十一、如果你想继续升级,这个系统还能怎么扩展
这个系统现在已经能用了。 但如果你继续往上走,还可以自然加很多功能。
比如:
按城市筛选用户 按年龄范围查询 统计不同城市用户数量 增加修改时只改某一个字段 支持分页显示 支持导出为 CSV 或 JSON 支持逻辑删除而不是物理删除 增加登录账号表 把 SQLite 迁移到 MySQL
你会发现,一个小小的用户管理系统,其实特别适合拿来练数据库项目能力。
因为它既不复杂到让你无法入手, 又足够真实,能把数据库最核心的东西都练到。
二十二、这一章最该真正带走的,是“数据库项目闭环”
如果要用一句话总结这章最重要的东西,那不是某条 SQL。 而是你已经开始真正具备这样的能力:
把数据库设计、Python 操作、业务逻辑和用户输入,组织成一个完整小系统。
这一步非常关键。
因为从这里开始,数据库对你来说就不再只是:
表 字段 SQL
而开始变成:
项目里的数据层 业务里的信息管理 程序里的长期状态存储
这就是为什么数据库实战阶段特别重要。 它是你从“会操作表”走向“会做系统”的第一步。
本章小结
这一章我们不是再单独讲某一条 SQL,而是直接做了一个完整的数据库小项目:用户信息管理系统。
这个系统真正串起了数据库阶段最核心的能力:
先设计用户表 再初始化数据库 然后实现新增、查询、搜索、修改、删除 接着用菜单把功能组织成一个可持续使用的小系统 同时把参数化查询、唯一约束、异常处理、事务意识这些真正落到代码里
你要真正看到的,不只是这个系统“能跑”。 更是它已经开始具备一个小型数据库项目该有的样子:
结构清楚 职责分离 数据可长期保存 功能可持续扩展
走到这里,数据库这条线你其实已经不再只是“会写几句 SQL”了。 你开始真正有能力把数据库放进一个完整程序里,让它服务于实际问题。