ARTICLE DETAIL

资讯详情

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

健康档案管理系统数据库课程设计:ER模型、MySQL建表与JDBC连接池实战

健康档案管理系统数据库课程设计:ER模型、MySQL建表与JDBC连接池实战 简介《数据库课程设计——健康档案管理系统.doc》是一份完整展示数据库课程设计全过程的文档资料面向计算机相关专业学生、数据库初学者及需要完成类似课题的开发者。文档以健康档案管理系统为案例系统讲解从需求分析、数据流图与数据字典构建到概念结构设计、逻辑结构设计、物理实现与安全备份的完整流程并结合用户管理、档案录入、查询检索、统计分析等功能模块说明具体实现思路。资源为单份doc文档压缩包共1个文件大小863KB便于直接下载查看。目前已有128人学习浏览适合用于课程设计参考、毕业设计入门或数据库实践教学素材。通过该文档读者可掌握ER图绘制、关系模式规范化、SQL建表及索引优化等关键技能同时了解报销处理、工资管理等关联流程在系统设计中的衔接方式对提升数据库综合设计能力有明显帮助。1. 数据库课程设计选健康档案管理系统题目越小越要做全数据闭环数据库课程设计最容易被低估的题目就是健康档案管理系统。没有商城那样的订单链路也没有论坛那样的权限矩阵看起来只有一张居民信息表和几张体检记录表似乎随便写几个增删改查就能交差。但真正动手你会发现最简单的一个居民对应多条体检记录就能让ER图画成蜘蛛网更别说身份证号唯一、档案修改留痕这些细节。下面这份落地路径就是把这类课程设计的完整交付方式拆给你从业务边界、ER模型、建表SQL到JDBC连接池和答辩避坑照着走你能交出一份能演示、敢被老师追问的作业。2. 把业务拆成五张表健康档案系统的ER模型与字段边界数据库课程设计的评审逻辑很简单老师看得不是功能多不多而是你懂不懂表关系、有没有踩范式的雷。健康档案管理系统最合适的粒度不是一张大表而是居民基本信息、体检记录、疾病诊断、用药记录、系统用户五张表。下面先从业务边界说起。2.1 业务边界哪些功能必须做哪些做了纯粹给自己挖坑健康档案的核心不是录入健康数据而是把一个人从建档到历次体检、就诊的变化串起来。所以必须做的功能可以压缩成三条居民档案的增删改查按身份证号/姓名/年龄段检索每名居民可以新增多条体检记录按日期倒序查看系统用户登录区分管理员和普通录入员。能做到这三条数据库课程设计已经及格能解释清楚为什么要拆表就能拿良。容易翻车的是想多做一点。我见过不少学生忙着加健康评估、BMI自动计算、异常指标预警甚至报表导出。这些功能不是不行而是没想好数据从哪来。健康评估需要一套评分规则异常预警需要设定阈值报表导出需要POI库每多一个功能你的数据库就要多几张配置表、多几条边界条件。课程设计的时间有限把这些时间拿去打磨ER图和索引说明性价比高得多。2.2 从ER图到五张表字段类型、主外键和索引怎么定ER图里的关系用一句话说清楚居民与体检记录是1对多体检记录与疾病诊断是1对多居民与用药记录是1对多系统用户和业务数据没有直接关系。画图时注意不要把居民和疾病画成多对多因为一次体检可以查出多种疾病一种疾病也可能在多次体检中出现处理多对多你要额外建一张关系表对课程设计来说是多余的复杂度。更合理的做法是把疾病诊断挂在某一次体检记录下让每次诊断都有明确的日期和来源。字段设计是踩坑重灾区。身份证号不要用int要用varchar(18)加唯一索引出生日期用date不要直接存年龄性别用char(1)存1/0身高体重要用decimal(5,1)不要用float避免浮点误差。体检记录的主键建议直接用自增id不要用居民id体检日期做联合主键因为同一个人同一天完全可能体检两次。把这些规则列成一张表写SQL时照着抄就行。表名主键关键索引/约束核心字段节选resident 居民信息表resident_id int自增id_card varchar(18) uniquename, gender, birth_date, phone, address, created_atphysical_exam 体检记录表exam_id int自增resident_id外键exam_date普通索引height_cm, weight_kg, systolic_pressure, diastolic_pressure, blood_sugardiagnosis 疾病诊断记录表diagnosis_id int自增exam_id外键disease_name, diagnosed_at, remarkmedication 用药记录表medication_id int自增resident_id外键medicine_name, dosage, start_date, end_datesys_user 系统用户表user_id int自增username uniquepassword_hash, role, created_at外键策略上体检记录和诊断记录用ON DELETE CASCADE原因是居民注销档案时他名下的医疗明细一起删掉才合理。但是用药记录我建议用ON DELETE SET NULL或者干脆不用外键只保留resident_id普通索引因为用药记录如果被误删数据是不可恢复的。课程设计报告里能写出根据业务需要选择不同级联策略比千篇一律的CASCADE更能体现思考。2.3 一个反例把档案和体检记录放在同一张表这是数据库课程设计里最经典的翻车现场。有同学图省事把身高、体重、血压直接塞进居民信息表每次体检就在原记录上UPDATE。看上去少写一张表还不用JOIN但体检是多次的覆盖更新会把历史轨迹抹掉。健康档案系统的价值恰恰是保留历史不是只留最新值。老师如果问为什么拆成两张表标准回答是居民信息表保存的是不变的属性体检记录表保存每次测量的时点数据这样一次体检就是一条新记录历史可追溯。这个反例也牵出一个数据库基础知识第三范式。居民信息表里存年龄就是违反第三范式因为年龄可以由出生日期推导今天填29明年变30不改就错。所以建表时不要存age存birth_date查询时用TIMESTAMPDIFF(YEAR, birth_date, CURDATE())计算。写课程设计文档时把范式分析写进数据库设计说明这一节基本上这段就是加分项。画ER图的工具不用太纠结。常见做法是用draw.io画实体框和连线导出图片插进Word也可以先用Navicat把表建好再用它的逆向模型直接生成ER图。注意课程设计交的是docER图不是装饰品图中漏一个外键老师对比实体里的连线就能看出来。图上的每一条1对多连线都要能指到对应的外键字段最好在文档里配一个表关系说明把主外键关系再写一遍。3. 用SQL把表建出来从CREATE TABLE到增删改查的关键代码光有ER图不够数据库课程设计必须交出一份能在MySQL里跑通的建表脚本。这一章把五张表的完整SQL和常用增删改查语句都列出来你可以直接复制去改库名和字段。3.1 建库建表五张表一次写完的最小SQL这里以MySQL为例MySQL 8.0和5.7都能跑。注意建库时指定utf8mb4。-- 建库统一字符集 CREATE DATABASE health_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE health_db; -- 居民信息表 CREATE TABLE resident ( resident_id INT AUTO_INCREMENT PRIMARY KEY, id_card VARCHAR(18) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(1) NOT NULL COMMENT 1男 0女, birth_date DATE NOT NULL, phone VARCHAR(20), address VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 体检记录表 CREATE TABLE physical_exam ( exam_id INT AUTO_INCREMENT PRIMARY KEY, resident_id INT NOT NULL, exam_date DATE NOT NULL, height_cm DECIMAL(5,1), weight_kg DECIMAL(5,1), systolic_pressure INT, diastolic_pressure INT, blood_sugar DECIMAL(4,1), FOREIGN KEY (resident_id) REFERENCES resident(resident_id) ON DELETE CASCADE, INDEX idx_exam_resident (resident_id), INDEX idx_exam_date (exam_date) ) ENGINEInnoDB; -- 疾病诊断记录表 CREATE TABLE diagnosis ( diagnosis_id INT AUTO_INCREMENT PRIMARY KEY, exam_id INT NOT NULL, disease_name VARCHAR(100) NOT NULL, diagnosed_at DATE NOT NULL, remark VARCHAR(255), FOREIGN KEY (exam_id) REFERENCES physical_exam(exam_id) ON DELETE CASCADE ) ENGINEInnoDB; -- 用药记录表按业务选择不建外键 CREATE TABLE medication ( medication_id INT AUTO_INCREMENT PRIMARY KEY, resident_id INT NOT NULL, medicine_name VARCHAR(100) NOT NULL, dosage VARCHAR(50), start_date DATE, end_date DATE, INDEX idx_med_resident (resident_id) ) ENGINEInnoDB; -- 系统用户表 CREATE TABLE sys_user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role TINYINT NOT NULL COMMENT 1管理员 2录入员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB;这段SQL最值得注意的参数是三个CHARACTER SET utf8mb4、ENGINEInnoDB、ON DELETE CASCADE。utf8mb4不是utf8utf8在MySQL里最多3字节存不了生僻字和Emoji课程设计录姓名时遇到䶮这种字就会报错换成utf8mb4直接解决。InnoDB是必须的MyISAM不支持外键也不支持事务你如果建表时忘了指定MySQL 8.0默认是InnoDB但5.7默认是MyISAM所以写清楚最稳妥。ON DELETE CASCADE表示删除居民时自动删掉名下体检记录这个行为要在文档里写明白否则老师会在演示时故意删一个居民然后问你为什么体检表也没了。3.2 增删改查居民档案和体检记录的最少SQL模板课程设计演示环节老师通常不会看你的界面点来点去而是直接让你当场执行几条SQL验证功能。所以把增删改查写成模板放在doc里演示时复制粘贴比现场盲打强得多。-- 新增居民建档 INSERT INTO resident (id_card, name, gender, birth_date, phone, address) VALUES (110101199003071234, 张三, 1, 1990-03-07, 13800138000, 北京市朝阳区); -- 按身份证号查档案唯一索引直接命中 SELECT resident_id, name, gender, birth_date, phone, address FROM resident WHERE id_card 110101199003071234; -- 按姓名模糊查询适合列表页搜索 SELECT * FROM resident WHERE name LIKE CONCAT(%, ?, %); -- 修改联系电话更新时记得加WHERE不然全表更新 UPDATE resident SET phone 13900139000 WHERE resident_id 1; -- 新增一次体检记录 INSERT INTO physical_exam (resident_id, exam_date, height_cm, weight_kg, systolic_pressure, diastolic_pressure, blood_sugar) VALUES (1, 2025-01-10, 175.2, 72.5, 128, 82, 5.6); -- 查询某人全部体检记录按日期倒序 SELECT exam_id, exam_date, height_cm, weight_kg, blood_sugar FROM physical_exam WHERE resident_id 1 ORDER BY exam_date DESC; -- 删除居民外键级联会把体检记录删干净 DELETE FROM resident WHERE resident_id 1;注意UPDATE和DELETE不加WHERE会把整个表清空这是课程设计答辩事故里排名第一的原因。第二模糊查询里的?占位符对应JDBC的PreparedStatement参数不要用字符串拼接否则会产生SQL注入。如果你用的是MyBatis把?换成#{keyword}就行效果一样。3.3 索引与查询优化身份证号为什么能直接命中、日期索引怎么设很多课程设计只写到能跑就停了但答辩时老师会追问你这张表越来越大查询怎么会变慢这时候你得答出索引。居民表的id_card已经建了唯一索引所以WHERE id_card ...是直接走索引的不用全表扫描。体检记录表的exam_date建了普通索引支持按日期范围筛选。但注意不要给低区分度的字段建索引比如gender只有两个值建了索引数据库优化器大概率还是选全表扫描因为回表成本更高。用一个关联查询示例演示索引生效的写法SELECT r.name, e.exam_date, e.height_cm, e.weight_kg, e.blood_sugar FROM resident r JOIN physical_exam e ON r.resident_id e.resident_id WHERE e.exam_date BETWEEN 2024-01-01 AND 2024-12-31 ORDER BY e.exam_date DESC;执行前可以在Navicat里按CtrlL看执行计划确认possible_keys和key不是空。这条SQL把居民表和体检表做关联体检表走idx_exam_date范围扫描再回表取resident_id最后join居民表的主键数据量不大的情况下毫秒级返回。把这个执行计划截图放进课程设计报告的查询优化一节是很实在的加分动作。4. 用JDBC和连接池把程序跑起来从配置文件到分页查询的完整路径建好表只是第一步数据库课程设计最终要交一个能运行的程序。Java Web是最常见的实现方式这一章用一套最小可行的JDBC Druid连接池方案把增删改查和分页串起来。4.1 为什么不要裸写DriverManager连接池的选型与配置如果你用DriverManager.getConnection()实现单次查询确实简单但课程设计里这个写法会带来两个问题每次请求都创建和销毁物理连接性能差并发稍高就报Too many connections因为MySQL默认最大连接数只有151你一个页面刷新10次就占掉10个连接还不释放。所以正确做法是引入连接池课程设计里用Druid比较多因为配置简单还有监控页。在src/main/resources下放一个druid.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/health_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456 jdbc.initialSize2 jdbc.maxActive10 jdbc.minIdle2 jdbc.maxWait60000连接串里的参数是一线排错时最容易忽略的地方。characterEncodingutf8控制写入数据库的中文编码漏了就是乱码serverTimezoneAsia/Shanghai必须设置不然MySQL 8.0驱动会拿UTC和本地时区比直接报错useSSLfalse是为了避免SSL握手开销课程设计环境不需要加密传输。池参数里maxActive10足够10个用户同时操作initialSize2是启动预建连接maxWait60000是拿不到连接时最多等60秒超时抛异常避免线程无限等下去。4.2 用一个JDBC工具类封装增删改查工具类的核心是从Druid连接池借连接使用完还连接。连接池里的close不是真正断开而是归还复用这一点必须理解不然你会在文档里写错。import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Properties; public class JdbcUtils { private static DataSource dataSource; static { try (InputStream in JdbcUtils.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这个工具类最容易被问倒的地方是为什么close里要try catch还忽略异常因为归还连接时如果失败不能影响业务主流程。为什么不用try-with-resources因为你要把连接池里的连接当普通连接一样用完即还而Druid已经重写了close方法try-with-resources也能用但很多老项目的课设代码习惯手动close两种都行。你只要保证每借一个连接在finally里调用close就不会把池子借空。4.3 分页查询让体检记录列表不卡成PPT如果一次查询把某居民全部体检记录都加载到页面数据少还行数据一多页面会越滚越卡。课程设计里至少列表页要做分页。分页SQL用LIMIT offset, size把当前页和每页条数作为参数传进去。import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; public class PhysicalExamDao { public ListPhysicalExam getExamPage(int residentId, int pageNum, int pageSize) { String sql SELECT exam_id, exam_date, height_cm, weight_kg, blood_sugar FROM physical_exam WHERE resident_id ? ORDER BY exam_date DESC LIMIT ?, ?; ListPhysicalExam list new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn JdbcUtils.getConnection(); ps conn.prepareStatement(sql); ps.setInt(1, residentId); ps.setInt(2, (pageNum - 1) * pageSize); ps.setInt(3, pageSize); rs ps.executeQuery(); while (rs.next()) { PhysicalExam exam new PhysicalExam(); exam.setExamId(rs.getInt(exam_id)); exam.setExamDate(rs.getDate(exam_date)); exam.setHeightCm(rs.getBigDecimal(height_cm)); exam.setWeightKg(rs.getBigDecimal(weight_kg)); exam.setBloodSugar(rs.getBigDecimal(blood_sugar)); list.add(exam); } } catch (SQLException e) { e.printStackTrace(); } finally { JdbcUtils.close(rs, ps, conn); } return list; } }注意LIMIT ?, ?的两个参数含义第一个是偏移量第二个是每页条数。pageNum2, pageSize10时offset是10跳过第一页的10条取第11到20条。另外ORDER BY exam_date DESC必须有否则分页结果顺序不稳定翻页时可能看到重复记录。这个分页方法放进文档时最好再贴一个计算总页数的SQLSELECT COUNT(*) FROM physical_exam WHERE resident_id ?让前端能渲染页码。5. 课程设计答辩避坑指南连接池、死锁和字符编码的连环坑下面的坑都是从真实课设现场提炼出来的每一条都按现象、原因、解决来梳理。你可以直接把这一页打印成答辩提纲解决问题比背一堆SQL有用。5.1 程序跑一会儿就报Too many connections连接池没关还是MySQL连接数不够现象程序运行几分钟后控制台抛Too many connections数据库直接拒绝新连接。原因最常见的是有人用DriverManager.getConnection写操作后没有close物理连接只增不减其次是Druid的maxActive设得太大比如设成100或200把MySQL默认的151个连接全占满。课程设计里并发用户本来就没几个连接数开这么大就是给自己找麻烦。解决统一改走连接池并把maxActive设成10initialSize设成2。同时检查工具类里的close逻辑所有分支都要在finally里归还连接。更严格一点可以在druid.properties里把maxWait调小到1000让连接不足时快速失败方便从堆栈定位到具体是哪个请求在泄漏连接。记住改完配置必须重启应用已经占满的连接不会自动释放。5.2 中文乱码建库、连接串、响应头三处都要统一编码现象在Navicat里看数据表中文显示正常但通过网页查询中文变成问号或者控制台输出是一串???。原因乱码从来不是单点问题而是链路里有一处编码不对。建库用了utf8mb4但连接串里忘了characterEncodingutf8或者Servlet/JSP页面没有设置响应编码默认用ISO-8859-1还有一种情况是字段本身排序规则是latin1_swedish_ci导致写入时就已经出错。解决把三处都钉死。建库建表用utf8mb4和utf8mb4_unicode_ci连接串写成useUnicodetruecharacterEncodingutf8两个参数同时存在页面和接口统一在响应头设置Content-Type: text/html; charsetutf-8。如果是Spring Boot项目可以加一个字符编码过滤器但课程设计通常不用Spring手动设置足够。5.3 并发添加体检记录时死锁为什么数据量小也会报锁等待现象两个用户几乎同时提交体检数据其中一笔报Deadlock found when trying to get lock; try restarting transaction。原因InnoDB的行锁是事务级别的死锁往往来自两条更新语句访问同一批行但顺序相反。举例事务A先更新居民表的phone再插入体检记录事务B先插入体检记录再更新居民表的phone。两个事务互相持有对方要的锁数据库只能牺牲其中一个事务。解决给业务方法加事务并统一SQL的执行顺序。比如约定任何操作都先动居民表再动体检记录表让锁请求顺序一致。在Spring里用Transactional(rollbackFor Exception.class)在JDBC里则要自己setAutoCommit(false)执行成功后commit异常时rollback。数据库会自动回滚死锁事务程序要捕获死锁异常并提示用户重新提交不要直接让页面报500。5.4 老师打开你的工程连不上数据库时区、绝对路径和密码明文现象老师把你的工程拷到自己电脑启动后报Public Key Retrieval is not allowed或Communications link failure完全跑不起来。原因常见的有三种——连接串里没有serverTimezoneMySQL 8.0驱动会因为时区未知而拒绝连接druid.properties里写了绝对路径比如D:/project/config/druid.properties换一台机器路径不存在数据库密码写死在代码里老师本地没有这个密码当然连不上。解决配置文件统一放在classpath根目录用getResourceAsStream读取不依赖绝对路径。文档里专门写一节运行环境准备告诉老师先执行CREATE DATABASE health_db再把root密码改成自己本地的。密码不要写在Java类里放properties文件并注明提交前清空为自己的密码。这条是最容易被忽略的很多同学在教室电脑上跑得好好的老师一拷走就凉。如果条件允许录一段3分钟的演示视频放在答辩PPT最后环境出问题时不至于冷场。5.5 删除居民时外键报错删除顺序和级联策略没对齐现象删除一条居民档案时数据库报Cannot delete or update a parent row: a foreign key constraint fails。原因居民表被体检记录表引用体检记录又被诊断记录引用同时用药记录表也保留了resident_id的外键。如果部分表用了默认的RESTRICT策略MySQL会在删除居民时检查到有关联记录直接拒绝。解决最省事的办法是所有关联表都统一用ON DELETE CASCADE删除居民时自动清理名下所有医疗数据。另一种做法是保持限制删除但在业务层的删除方法里手动按顺序先删medication、再删physical_examdiagnosis通过exam_id级联最后删resident。这两种策略在课程设计文档里都要写明并解释你为什么选其中一种这是数据库设计完整性的加分点。6. 进阶用事务把添加体检更新档案做成要么都成功、要么都回滚数据库课程设计最后能拉开差距的地方不只是CRUD而是会不会用事务处理一个业务动作涉及多张表的场景。这里以一个刚毕业的工程师常做的事为例给居民添加一条体检记录时同时修改居民信息表的联系电话。如果只用两条SQL第二条失败时第一条已经入库数据就不一致了。public void addExamWithUpdateResident(PhysicalExam exam, String phone) { Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); // 1. 插入体检记录 String sqlExam INSERT INTO physical_exam (resident_id, exam_date, height_cm, weight_kg, systolic_pressure, diastolic_pressure, blood_sugar) VALUES (?, ?, ?, ?, ?, ?, ?); try (PreparedStatement ps conn.prepareStatement(sqlExam)) { ps.setInt(1, exam.getResidentId()); ps.setDate(2, new java.sql.Date(exam.getExamDate().getTime())); ps.setBigDecimal(3, exam.getHeightCm()); ps.setBigDecimal(4, exam.getWeightKg()); ps.setInt(5, exam.getSystolicPressure()); ps.setInt(6, exam.getDiastolicPressure()); ps.setBigDecimal(7, exam.getBloodSugar()); ps.executeUpdate(); } // 2. 更新居民联系电话演示多条更新 String sqlUpdate UPDATE resident SET phone ? WHERE resident_id ?; try (PreparedStatement ps conn.prepareStatement(sqlUpdate)) { ps.setString(1, phone); ps.setInt(2, exam.getResidentId()); ps.executeUpdate(); } conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(添加体检记录失败已回滚, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这个写法的关键点有三个先setAutoCommit(false)关闭自动提交让两条SQL进入同一个事务成功后手动commit()任何异常先rollback()再把异常抛出去由上层统一提示用户。finally里别忘了把连接的状态恢复并归还连接池否则连接池里的连接可能保持事务状态下一次借出时出问题。这段事务代码放进课程设计报告的核心功能实现里本身就是很好的答辩材料。我当年做课设时因为没加事务体检记录插进去了更新却失败了数据对不上被老师当场指出。后来我就记住了凡是涉及两张表以上的写操作一律先想事务。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取
返回列表