ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+SQLite+tkinter构建图书馆管理系统:数据库设计大作业全解析

Python+SQLite+tkinter构建图书馆管理系统:数据库设计大作业全解析 简介一份面向高校数据库系统大作业的图书馆管理系统完整方案采用Python与PyQt5开发GUI图形界面后端使用MySQL 8.0资源内包含完整SQL脚本和程序源码适合正在完成课程设计或需要快速搭建可运行项目、同时想学习数据库表设计与界面编程的学生参考尤其是对课程设计无从下手时可作直接范本。压缩包大小约118.23MB内容覆盖建库建表SQL、Python业务逻辑与界面代码以及PyQt5、pyqt5-tools、pymysql等依赖库打包文件可直接放入Python的Lib目录完成环境配置也可使用pip一键安装省去手动查找依赖的麻烦。需要注意的是Mysql版本为8.0导入到低版本数据库可能报错建议使用MySQL Workbench或Navicat导入。作者还提供了B站使用教程视频能有效降低初学者上手门槛。资源已吸引19523人浏览学习对于需要完成图书管理类课设、希望节省开发时间并获取完整源码与部署指导的同学来说是一份实用且完整的参考资料。1. 为什么图书馆管理系统是数据库系统设计大作业里最能覆盖考点的选题数据库系统设计大作业题目很多选课系统、学生信息管理、图书馆管理系统是三个最常出现的其中图书馆管理系统最值得选实体少但关系典型读者和图书之间的多对多关系必须靠中间表拆解借书还书又天然要求事务保证这些都是课程大纲里的核心考点。图形界面要求则把题目从“写几条 SQL”拉高到“把 Python 工程完整跑起来”。我在几次课程答辩中看到的情况是能拿到高分的版本未必界面多漂亮但一定在数据模型、事务边界和异常处理上都有交代。下面按数据库设计、Python 数据访问层、tkinter 图形界面、答辩演示四个层次展开覆盖到一个可验收的项目版本。2. 图书馆管理系统建模实体划分、多对多拆解与建表 DDL在实际敲代码之前先定数据模型这一步直接决定大作业文档中 E-R 图和关系模式能拿多少分。图书馆管理系统的实体并不复杂也正因为实体少老师才有精力逐字段审查属性划分、主外键设置和范式设计。下面按实体、关系、规范化三个步骤把模型落定。2.1 先用属性表厘清实体边界图书、读者、借阅记录各存什么实体设计最容易犯的错误是把“书目”和“馆藏册”混为一谈。同一本《数据库系统概论》在馆里有 10 册每一册都能被不同读者单独借走如果只用一个书名加一个总数记录就无法回答“现在有几册在架、哪一册被谁借走”这类核心业务问题。常见做法是把每册书分配一个独立的图书 ID 作主键ISBN 只作为出版信息字段保留。借阅记录关联的是图书 ID定位的就是“某一册书”而非书名语义下的“某一种书”。表 2-1 是三个核心实体的基本属性划分可以直接作为 E-R 图的属性清单。表 2-1 图书馆管理系统核心实体属性划分实体主要属性主键图书图书ID、ISBN、书名、作者、出版社、分类、入库日期、状态(在馆/借出)图书ID读者读者ID、姓名、性别、学号、联系电话、读者类型ID、办证日期读者ID借阅记录记录ID、图书ID、读者ID、借出日期、应还日期、实际归还日期、续借次数记录ID为什么要把读者类型单独拆出来如果把“普通读者可借 5 册、借期 30 天”直接写死在每个读者记录里后续调整借阅规则时就要 UPDATE 全表。拆出读者类型表后读者通过类型 ID 引用它就形成了一张典型的字典表加业务表的层次结构这也为后面讲到第二范式时提供了现成论据。2.2 多对多关系必须借助中间表借阅记录的设计细节从 E-R 图视角看读者与图书是多对多关系一个读者可以先后借阅多册书一册书在不同时间也会被多个读者借走。关系数据库中多对多关系不能直接表达必须拆成两个一对多读者到借阅记录一对多图书到借阅记录一对多。借阅记录表就是这个中间表。借阅记录常被漏掉的字段是实际归还日期 return_date。没有它系统就无法区分“这本书记录在案但已归还”与“这本书确实在被借走”图书的在馆状态只能靠猜。还书时应当同时回填 return_date 并把图书状态改回“在馆”两个动作放在同一个事务里执行保证借阅流水和状态字段始终一致。还需要防一个业务漏洞同一时刻同一册书不能被两个读者借走。外键约束只能保证记录引用的图书存在管不了“是否已被扣留”靠 Python 条件判断在并发场景会失效。一个更能体现数据库素养的做法是给借阅记录表加上一个条件唯一索引CREATE UNIQUE INDEX idx_active_borrow ON borrow_record (book_id) WHERE return_date IS NULL;这条语句只在 return_date 为空的行上约束 book_id 唯一即“同一时刻一本书只能有一条未完结的借阅记录”。SQLite 支持这种部分索引MySQL 不支持部分索引语法可以在借阅记录上增加一个“是否在借”标记字段再对 (book_id, is_active) 建联合唯一索引。把业务约束下沉到数据库层比只在界面层判断更有说服力。2.3 规范化到第三范式值得写进设计文档的完整建表脚本第一范式的检查最简单每个字段只能保存一个不可再分的语义值。把多个作者用顿号拼进 author 字段或在 category 中塞入“文学/小说/2015年新编”这种混合信息都会在这一项被扣分。多个作者的规范处理是另建作者表关联如果大作业不想引入过多表至少应保证单作者场景下字段语义清晰。第二和第三范式在大作业里表现为“有没有故意拆出字典表、有没有消除重复存储”。读者类型表反映了非主属性对码的部分依赖处理图书表不保存出版社地址、只保存出版社名称则是消除传递依赖的自然结果。答辩时如果能讲出“读者类型表是规范化到第二范式时产生的拆分”已经比大多数版本强了。下面这组 DDL 是完整可用的建表脚本SQLite 与 MySQL 都能跑差异点只在自增主键写法CREATE TABLE reader_type ( type_id INTEGER PRIMARY KEY AUTOINCREMENT, type_name TEXT NOT NULL UNIQUE, max_borrow INTEGER NOT NULL DEFAULT 5, borrow_days INTEGER NOT NULL DEFAULT 30 ); CREATE TABLE reader ( reader_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, gender TEXT, student_no TEXT UNIQUE, phone TEXT, type_id INTEGER NOT NULL REFERENCES reader_type(type_id), reg_date TEXT NOT NULL DEFAULT (date(now)) ); CREATE TABLE book ( book_id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT NOT NULL, title TEXT NOT NULL, author TEXT NOT NULL, publisher TEXT, category TEXT, in_date TEXT NOT NULL DEFAULT (date(now)), status TEXT NOT NULL DEFAULT 在馆 CHECK (status IN (在馆, 借出)) ); CREATE TABLE borrow_record ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL REFERENCES book(book_id), reader_id INTEGER NOT NULL REFERENCES reader(reader_id), borrow_date TEXT NOT NULL DEFAULT (date(now)), due_date TEXT NOT NULL, return_date TEXT, renew_count INTEGER NOT NULL DEFAULT 0 );建表脚本里值得在文档中说明的参数有几处DEFAULT (date(now))利用 SQLite 内建函数生成当天日期换到 MySQL 需要改为CURDATE()CHECK (status IN (在馆,借出))把图书状态限制在合法取值内从数据库层挡住了非法状态UNIQUE约束加在学号上防止同一个学生重复办证。SQLite 的AUTOINCREMENT会创建一个额外的序列如果只是普通自增也可以直接用INTEGER PRIMARY KEY。提示SQLite 默认不强制执行外键约束每次建立连接后都要执行PRAGMA foreign_keys ON否则即便插入不存在的 reader_id 也不会报错。这个开关写在第 3 章的数据访问层里。3. Python 连接 SQLite数据访问层封装与核心事务写法数据模型定好后下一步是让 Python 程序真正操作这些表。这一层的目标很明确把数据库连接、提交、回滚这些样板代码集中管理业务代码只写 SQL 和参数。这样图形界面层可以专心渲染界面不被 cursor、commit 这些细节反复打断。3.1 为什么首选 sqlite3以及 MySQL 场景怎么换Python 连数据库有几种常见路线标准库 sqlite3、PyMySQL 连 MySQL、SQLAlchemy ORM。对大作业来说sqlite3 最稳妥数据库就是一个文件Python 首次连接时若文件不存在会自动创建无需安装数据库服务也无需配置账号密码。课堂展示时把项目目录拷到演示机就能直接运行这是 MySQL 方案很难做到的优势。如果课程明确要求必须用 MySQL则用 PyMySQL 驱动连接参数、SQL 写法大体一致差别只体现在连接配置和部分 SQL 语法上。这里给一张选型对照表答辩被问到“为什么用 SQLite”时可以直接引用。表 3-1 Python 数据库方案选型对比方案适用场景优缺点sqlite3 标准库单机大作业、答辩演示零配置、跨平台但并发写能力弱PyMySQL MySQL课程要求、多用户并发功能全、接近生产环境但演示环境配置繁琐SQLAlchemy ORM大型项目、对象映射开发效率高但对课程项目偏重、查错链路长无论你平时用 vscode 配置 python 环境还是习惯在 pycharm 里直接建工程只要机器上装了 Python 3.10 以上解释器sqlite3 就是标准库的一部分不需要额外 pip install这也是大作业环境准备成本最低的一条路。3.2 用上下文管理器封装连接一个函数走完连接、提交、关闭每个查询都重复写 connect、cursor、execute、close代码会迅速膨胀且容易漏掉 commit。我一般用一个上下文管理器封住连接生命周期后续所有数据函数只需要暴露 SQL 和参数。下面是完整的封装代码import sqlite3 from contextlib import contextmanager DB_PATH library.db contextmanager def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row conn.execute(PRAGMA foreign_keys ON) try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def query_all(sql, params()): with get_conn() as conn: rows conn.execute(sql, params).fetchall() return [dict(row) for row in rows] def execute(sql, params()): with get_conn() as conn: cur conn.execute(sql, params) return cur.lastrowid三个设置是这段代码的关键。row_factory sqlite3.Row使查询结果按列名取值后续写成 row[title] 而不是 row[2]可读性和可维护性都更好。PRAGMA foreign_keys ON必须在每次连接时执行因为 SQLite 默认关闭外键检查漏掉这行删除仍有借阅记录的读者就不会报错。contextmanager 使函数体正常结束时自动 commit异常时自动 rollback事务边界被收敛到一个地方。3.3 图书检索、借书、还书占位符与事务的典型写法图书检索是大作业里演示频率最高的功能一般按书名、作者、ISBN 中任一关键词做模糊匹配。最忌讳的写法是 f-string 拼接 SQL不仅存在注入风险中文参数在部分编码环境还会产生乱码。正确的做法是用?占位符def search_books(keyword): sql SELECT book_id, isbn, title, author, publisher, status FROM book WHERE title LIKE ? OR author LIKE ? OR isbn LIKE ? like % keyword % return query_all(sql, (like, like, like))?占位符由 sqlite3 库负责转义参数中的引号、百分号都会被当作普通字符处理。三个占位符对应三个参数一个都不能少返回结果是字典列表可以直接交给 Treeview 逐行展示。借书操作设计的核心是事务边界。借阅记录表和图书状态表必须同时变更任何一个先成功而另一个失败都会留下脏数据要么书目记录显示正常但状态没变要么状态改了却查不到借阅记录。下面是放在同一个事务里的借书函数from datetime import date, timedelta def borrow_book(book_id, reader_id): with get_conn() as conn: row conn.execute( SELECT status FROM book WHERE book_id ?, (book_id,), ).fetchone() if row is None: raise ValueError(图书不存在) if row[status] ! 在馆: raise ValueError(该书已被借出) due_date (date.today() timedelta(days30)).isoformat() conn.execute( INSERT INTO borrow_record (book_id, reader_id, borrow_date, due_date) VALUES (?, ?, ?, ?) , (book_id, reader_id, date.today().isoformat(), due_date), ) conn.execute( UPDATE book SET status 借出 WHERE book_id ?, (book_id,), )函数内部先查图书状态再插入借阅流水最后更新图书状态。三步共用同一个连接查询得到的 row 是 sqlite3.Row 对象可以直接用 row[status] 判断。借期先固定写成 30 天如果需要按读者类型区分可在这一步 join reader_type 表取出 borrow_days 替换掉 30。抛出的 ValueError 会向上传递到图形界面层由界面弹出错误框。还书是逆过程回填 return_date再把状态改回“在馆”同样放在一个事务里def return_book(record_id, book_id): with get_conn() as conn: conn.execute( UPDATE borrow_record SET return_date ? WHERE record_id ? AND return_date IS NULL , (date.today().isoformat(), record_id), ) conn.execute( UPDATE book SET status 在馆 WHERE book_id ?, (book_id,), )WHERE return_date IS NULL是防止重复还书的关键条件二次点击还书时这条 UPDATE 影响 0 行配合界面层的提示就能把重复还书挡在外面。到这里数据访问层已经具备查询、借书、还书三个实时操作若需要新增图书、删除读者按同一个“封装好 SQL 加参数”的模式扩展即可。4. tkinter 图形界面Treeview 里的图书检索与借还交互数据库层的函数全部就绪后图形界面的任务就是把结果呈现出来并把用户动作传回数据层。tkinter 是 Python 标准库自带的 GUI 工具包不引入额外依赖控件风格朴素但足够完成演示如果老师对界面观感要求高可以后续换 ttkbootstrap 或 PyQt核心接口保持原样即可。4.1 图形界面分区工具栏、Treeview 与状态提示图形界面通常拆成三个区域顶部工具栏放搜索输入框和功能按钮中部用 Treeview 展示图书列表底部留一条状态栏显示操作提示。Treeview 是表格型控件天然适合展示“图书ID、书名、作者、出版社、状态”这类关系型数据比 Canvas 手绘表格式界面维护成本低得多。表 4-1 图形界面主要控件及职责控件类型用途关键词输入框ttk.Entry接收书名/作者/ISBN 检索条件查询按钮ttk.Button触发检索并刷新表格借书按钮ttk.Button对选中行执行借书事务还书按钮ttk.Button对选中行执行还书事务图书表格ttk.Treeview展示图书列表与在馆状态状态栏ttk.Label提示最近一次操作结果Treeview 每次刷新需要先清空旧行再重新插入这个动作必须独立成一个方法。把“取数据”和“刷新界面”分开查询、借书、还书三个回调都只需要改变数据源然后调用同一个刷新入口。4.2 主窗口骨架与刷新逻辑把字典列表变成表格行主窗口类初始化时创建控件并同步一次全量数据。下面代码包含构造函数、表格刷新和关键词查询三个方法import tkinter as tk from tkinter import ttk, messagebox, simpledialog class LibraryApp: def __init__(self, root): self.root root self.root.title(图书馆管理系统) self.root.geometry(900x600) toolbar ttk.Frame(root, padding8) toolbar.pack(sidetk.TOP, filltk.X) ttk.Label(toolbar, text关键词).pack(sidetk.LEFT) self.keyword_var tk.StringVar() ttk.Entry(toolbar, textvariableself.keyword_var, width30).pack(sidetk.LEFT, padx4) ttk.Button(toolbar, text查询, commandself.do_search).pack(sidetk.LEFT, padx4) ttk.Button(toolbar, text借书, commandself.do_borrow).pack(sidetk.LEFT, padx4) ttk.Button(toolbar, text还书, commandself.do_return).pack(sidetk.LEFT, padx4) columns (book_id, isbn, title, author, publisher, status) heading_map { book_id: 图书ID, isbn: ISBN, title: 书名, author: 作者, publisher: 出版社, status: 状态, } self.tree ttk.Treeview(root, columnscolumns, showheadings) for col in columns: self.tree.heading(col, textheading_map[col]) self.tree.column(col, width120) self.tree.pack(filltk.BOTH, expandTrue, padx8, pady8) self.refresh_table() def refresh_table(self, rowsNone): for item in self.tree.get_children(): self.tree.delete(item) if rows is None: rows query_all(SELECT * FROM book) for row in rows: self.tree.insert( , tk.END, values( row[book_id], row[isbn], row[title], row[author], row[publisher], row[status], ), ) def do_search(self): keyword self.keyword_var.get().strip() if not keyword: self.refresh_table() return self.refresh_table(search_books(keyword))columns 元组定义了表格的列顺序Treeview 插入数据时 values 必须与列顺序一一对应。refresh_table 默认从数据库读取全量数据也可以传入外部检索结果这样查询组件只要改变数据来源而无需改动渲染逻辑。self.tree.get_children() 返回当前全部行ID挨个 delete 就是“清空表格”的常规做法如果界面数据量大可以改用 self.tree.delete(*self.tree.get_children()) 一次删完。Treeview 的 showheadings 表示只显示表头不显示树列若去掉这个参数表格左侧会出现一列默认的 #0 空列。column(col, width120) 可以逐列设置宽度长书名的列可以放宽到 200。刷新带检索结果时要注意查询方法返回的是完整字典Treeview 的 values 只挑需要的字段多余的键即使存在也不会被渲染。4.3 借书与还书的回调选中行、读输入、再调用事务函数借书按钮的动作分成三步从 Treeview 取选中行的 book_id弹出输入框让用户填读者ID调用数据层的 borrow_book。界面层只负责交互不自己写 SQL数据层函数抛出的异常统一在界面层转为弹窗。def do_borrow(self): selection self.tree.selection() if not selection: messagebox.showwarning(提示, 请先在表格中选择一本书) return book_id self.tree.item(selection[0], values)[0] reader_id simpledialog.askinteger( 借书, 请输入读者ID, parentself.root ) if reader_id is None: return try: borrow_book(book_id, reader_id) self.refresh_table() messagebox.showinfo(成功, 借书成功) except ValueError as e: messagebox.showerror(错误, str(e))self.tree.selection() 返回被选中行的 ID 数组没有选中时为空数组用 self.tree.item(...)[values] 得到的元组与插入时的 values 顺序一致book_id 总是索引 0。simpledialog.askinteger 自带输入校验返回值就是整数用户点“取消”时返回 None必须提前 return 退出处理否则会触发数据层类型错误。借书成功后的刷新能让表格中的状态立即变为“借出”。还书回调也遵循同样的套路先定位借阅记录再调用事务函数def do_return(self): selection self.tree.selection() if not selection: messagebox.showwarning(提示, 请选择要归还的图书) return book_id self.tree.item(selection[0], values)[0] with get_conn() as conn: row conn.execute( SELECT record_id FROM borrow_record WHERE book_id ? AND return_date IS NULL , (book_id,), ).fetchone() if row is None: messagebox.showinfo(提示, 这本书不在借出状态无需归还) return return_book(row[record_id], book_id) self.refresh_table() messagebox.showinfo(成功, 还书成功)这段代码里界面层完成“判断是否需要还书”的工作预先查询只是为了拿到 record_id真正的两个 UPDATE 操作仍然由 return_book 在独立事务内完成。关注点分离是图形界面项目最容易忽略的规范界面层里不出现 INSERT 或 UPDATE 字符串后续想切换数据库或加单元测试时会轻松很多。5. 答辩演示重复借阅、SQL 注入与推荐演示顺序图形界面能跑通只是及格线答辩时老师不会只点两个按钮就走必然会尝试边界操作。下面三个检查项和一套演示顺序是我在验收前一定会过一遍的。5.1 重复借书时会发生什么三层校验缺一不可把同一本书连续借出两次第一次应该成功第二次必须弹“该书已被借出”的错误提示。这背后其实有三层校验第一层是借书函数里的 if row[status] ! 在馆 条件判断第二层是数据库里的 CHECK 约束把 status 限制在合法值第三层是借阅记录表的条件唯一索引保证并发情况下也不会产生两条未归还的借阅记录。演示时从界面上对同一本书连续点两次借书观察提示是否出现就能验证这三层是否都到位。5.2 搜索框输入特殊字符参数化查询的现场证明在搜索框输入1 OR 11再点查询如果程序仍然只展示匹配结果而不报错、不展示全量数据说明参数化查询已经在起作用。这里容易踩的坑是把模糊匹配的拼接位置搞错正确的参数传递是把 %keyword% 当作一个完整的字符串传给占位符而不是在 SQL 内部自己拼接 %。检索代码里 like % keyword % 就是标准写法答辩时可以直接解释这一步。5.3 演示顺序查询、借出、异常、归还四步走演示顺序建议固定为先输入书名关键词搜索展示结果集的即时刷新再选中一本状态为“在馆”的书点击借书演示正常借出路径随后马上对同一本书再次点击借书让系统弹出错误提示这是最能体现边界处理能力的一个动作最后点击还书观察状态列恢复为“在馆”。整个过程覆盖了查询、事务、唯一性约束、状态回滚四个知识点每个操作在界面上都有明确反馈。演示前建议在数据库层面做一次对照验证确认界面状态和真实表数据一致可以用一条 SQL 查看指定书籍的完整在借信息SELECT b.book_id, b.title, b.status, br.borrow_date, br.due_date, r.name AS reader_name FROM book b LEFT JOIN borrow_record br ON br.book_id b.book_id AND br.return_date IS NULL LEFT JOIN reader r ON r.reader_id br.reader_id WHERE b.book_id 1;这条查询把“未归还的借阅记录”和读者姓名一起带出来如果书籍在馆br 系列字段全部为 NULL如果已借出则能看到借出日期和应还日期。演示结束后把这条 SQL 和界面截图一起放进设计文档数据层到界面层的对应关系会显得非常扎实。本文还有配套的精品资源点击获取
返回列表