news 2026/10/11 12:37:40

医院系统Oracle课设实战:表结构、PL/SQL与JDBC全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
医院系统Oracle课设实战:表结构、PL/SQL与JDBC全解析

简介:面向Oracle数据库课程设计的医院系统数据库项目,基于Java与Oracle实现,适合需要完成课程设计、毕业设计或工程实训的初学者与进阶学习者。压缩包内共45个文件,以36个Java源码文件为主体,辅以SQL建表脚本、properties数据库连接配置、关系模型PNG、README说明、LICENSE等,整体大小仅323KB,目录紧凑便于按需查阅。项目提供完整的Java代码与SQL语句,默认连接远程Oracle数据库地址test.linyer.cn:9999,如需切换本地环境,直接在database.properties中修改连接信息即可。关系模型图清晰展示医院科室、医生、病人等核心实体的表间关系,SQL脚本可快速创建库表并初始化数据,Java源码则包含JDBC访问与基础业务处理,覆盖从数据建模到代码落地的完整过程。目前已有380人学习下载,适合快速搭建医院系统原型、加深对Oracle数据库设计及Java数据库编程的理解,也可作为课程设计报告的配套参考。

1. 医院系统 Oracle 课设:跑通 Java 代码只是第一步

这套医院系统数据库课设资源里,最容易把新手卡住的其实不是 Java 代码,而是 Oracle 本体的各种小脾气。Java 和 Oracle 的组合,在数据库课程设计中属于最经典也最考验功底的一组:病人、医生、科室、挂号、处方、药品、病历,七类实体环环相扣,任何一张表的外键设错,后面全链路的增删改查就跟着错。下面直接拆这份课设的表结构、PL/SQL 存储过程和 JDBC 连接办法,把最容易翻车的 Oracle 环境配置、监听、字符集、分页问题按“现象—原因—解决”列清楚。你如果正准备用这份资源交课设,先花半小时把表结构过一遍,再动手配数据库,能少熬一个通宵。

2. 表结构设计:把 ER 图落成能撑起全系统的 DDL

2.1 实体关系:七张核心表怎么连

这个医院系统的业务主线是“病人就诊”,流程大致是:病人到科室挂号,挂到某位医生名下;医生接诊后开处方,处方引用药品;同时医生写病历记录诊断结果。顺着这条线,业务实体收敛为七类:科室、医生、病人、挂号记录、药品、处方、病历。对应八张表:DEPT、DOCTOR、PATIENT、REGISTRATION、DRUG、PRESCRIPTION、PRESCRIPTION_ITEM、MEDICAL_RECORD,其中处方明细是处方的从表,和处方一起构成主从结构。

关系梳理起来其实很直观。科室对医生是 1 对 N,一张科室表对应多个医生;病人对挂号是 1 对 N,一个病人可以反复挂号;医生对挂号是 1 对 N,一个医生一天接几十个号;病人对处方是 1 对 N;处方对处方明细是 1 对 N;药品对处方明细是 1 对 N。病历表与病人是 1 对 N,每次就诊插一条记录,这样能保留病人完整的就诊历史,而不是把多次诊断结果覆盖在同一个字段里。

这里有个常见的扣分点:把科室 ID、医生 ID、病人 ID、药品 ID 全塞进一张“大宽表”。比如有的同学把挂号记录写成 REG_ID、PAT_ID、DOC_ID、DEPT_ID、DRUG_ID、PRICE 全挤在一行,看起来省事,但药品是医生开处方时关联的,和挂号记录没有直接关系,强行塞在一起,后面查“某科室某个月的药品消耗”就得写一串子查询,数据冗余还特别严重。正确做法是保持职责单一:挂号表只记录“谁在什么时间挂了哪个医生的号”,药品通过处方明细表间接关联,不直接出现在挂号表里。

外键策略上,我的习惯是:业务主键用 NUMBER 类型,外键一律加 NOT NULL 约束(除非语义上允许为空),并在外键列上建索引。Oracle 不会自动为外键建索引,这一点和 MySQL 不一样,不建索引会导致子表删除、级联操作时出现锁等待。课设数据量小感觉不出来,但答辩时老师追问“为什么这个外键要加索引”,你答出“减少锁竞争、加速连表查询”这两点,是能加分的回答。

2.2 核心表 DDL:约束写在建表语句里

建表语句建议全部写在 init.sql 里,按“先主表、后子表”的顺序执行,避免外键引用不存在的表时报 ORA-00942。下面这套 DDL 是这份资源里最核心的部分,我按依赖顺序拆开讲。

-- 科室表 CREATE TABLE DEPT ( DEPT_ID NUMBER(4) CONSTRAINT PK_DEPT PRIMARY KEY, DEPT_NAME VARCHAR2(50) NOT NULL, DEPT_LOC VARCHAR2(100) ); -- 医生表 CREATE TABLE DOCTOR ( DOC_ID NUMBER(6) CONSTRAINT PK_DOCTOR PRIMARY KEY, DOC_NAME VARCHAR2(30) NOT NULL, DOC_TITLE VARCHAR2(20) DEFAULT '主治医师', DEPT_ID NUMBER(4) NOT NULL, CONSTRAINT FK_DOCTOR_DEPT FOREIGN KEY (DEPT_ID) REFERENCES DEPT(DEPT_ID) );

DEPT_ID 用 NUMBER(4),因为科室数量撑死几十个;DOCTOR 用 NUMBER(6),给后续扩容留空间。DOC_TITLE 给了默认值“主治医师”,插入时少写一个字段。外键 FK_DOCTOR_DEPT 建在 DEPT_ID 上,保证医生必须属于某个真实存在的科室。注意 Oracle 的 VARCHAR2 长度单位是字节,VARCHAR2(50) 在 ZHS16GBK 字符集下能存 25 个中文汉字,在 AL32UTF8 下可能只存 16 个左右,所以涉及姓名字段最好用 VARCHAR2(30) 以上,别卡着 10 个字符设计。

继续建病人、药品、挂号表:

-- 病人表 CREATE TABLE PATIENT ( PAT_ID NUMBER(10) CONSTRAINT PK_PATIENT PRIMARY KEY, PAT_NAME VARCHAR2(30) NOT NULL, GENDER CHAR(1) CHECK (GENDER IN ('M','F')), BIRTH_DATE DATE, PHONE VARCHAR2(15) ); -- 药品表 CREATE TABLE DRUG ( DRUG_ID NUMBER(6) CONSTRAINT PK_DRUG PRIMARY KEY, DRUG_NAME VARCHAR2(50) NOT NULL, SPEC VARCHAR2(50), PRICE NUMBER(8,2) NOT NULL CHECK (PRICE >= 0), STOCK NUMBER(8) DEFAULT 0 CHECK (STOCK >= 0) ); -- 挂号表 CREATE TABLE REGISTRATION ( REG_ID NUMBER(10) CONSTRAINT PK_REG PRIMARY KEY, PAT_ID NUMBER(10) NOT NULL, DOC_ID NUMBER(6) NOT NULL, REG_TIME DATE DEFAULT SYSDATE, REG_STATUS CHAR(1) DEFAULT '0' CHECK (REG_STATUS IN ('0','1','2')), REG_FEE NUMBER(8,2) NOT NULL, CONSTRAINT FK_REG_PAT FOREIGN KEY (PAT_ID) REFERENCES PATIENT(PAT_ID), CONSTRAINT FK_REG_DOC FOREIGN KEY (DOC_ID) REFERENCES DOCTOR(DOC_ID) );

REG_STATUS 我用字符型,0 表示已挂号未就诊、1 表示已就诊、2 表示已退号。CHECK 约束在课设里是加分项,能证明你考虑过数据完整性。REG_FEE 挂号费不允许为空,金额字段统一 NUMBER(8,2),不要用 FLOAT,浮点金额在账目统计时会出现 0.30000000000000004 这种玄学问题,答辩现场查出来很尴尬。

处方和明细是典型的主从表结构,这也是整个库设计里最值得讲清楚的部分:

-- 处方表 CREATE TABLE PRESCRIPTION ( PRES_ID NUMBER(10) CONSTRAINT PK_PRES PRIMARY KEY, PAT_ID NUMBER(10) NOT NULL, DOC_ID NUMBER(6) NOT NULL, CREATE_TIME DATE DEFAULT SYSDATE, TOTAL_AMT NUMBER(10,2) DEFAULT 0, CONSTRAINT FK_PRES_PAT FOREIGN KEY (PAT_ID) REFERENCES PATIENT(PAT_ID), CONSTRAINT FK_PRES_DOC FOREIGN KEY (DOC_ID) REFERENCES DOCTOR(DOC_ID) ); -- 处方明细表 CREATE TABLE PRESCRIPTION_ITEM ( ITEM_ID NUMBER(12) CONSTRAINT PK_ITEM PRIMARY KEY, PRES_ID NUMBER(10) NOT NULL, DRUG_ID NUMBER(6) NOT NULL, QUANTITY NUMBER(4) NOT NULL CHECK (QUANTITY > 0), UNIT_PRICE NUMBER(8,2) NOT NULL, CONSTRAINT FK_ITEM_PRES FOREIGN KEY (PRES_ID) REFERENCES PRESCRIPTION(PRES_ID) ON DELETE CASCADE, CONSTRAINT FK_ITEM_DRUG FOREIGN KEY (DRUG_ID) REFERENCES DRUG(DRUG_ID) );

PRESCRIPTION_ITEM 里的 UNIT_PRICE 从 DRUG 表冗余过来,这是一个有意的冗余。药品价格以后可能调整,但处方明细作为历史单据必须保留开单时的价格,所以冗余单价是有业务价值的。TOTAL_AMT 同样做成冗余字段,后续由存储过程维护,统计处方金额时不用每次都做关联聚合,查询会轻很多。

病历表相对独立,单独维护:

-- 病历表 CREATE TABLE MEDICAL_RECORD ( REC_ID NUMBER(10) CONSTRAINT PK_REC PRIMARY KEY, PAT_ID NUMBER(10) NOT NULL, DOC_ID NUMBER(6) NOT NULL, DIAGNOSIS VARCHAR2(200), REC_TIME DATE DEFAULT SYSDATE, CONSTRAINT FK_REC_PAT FOREIGN KEY (PAT_ID) REFERENCES PATIENT(PAT_ID), CONSTRAINT FK_REC_DOC FOREIGN KEY (DOC_ID) REFERENCES DOCTOR(DOC_ID) );

2.3 索引与外键:别把索引当装饰

上面建的所有外键列建议都补上索引。Oracle 外键列不加索引,在主表删除或更新被引用行时会触发表级锁。课设数据量小感觉不出来,但老师追问起来,你要能答出“防止锁竞争、加速连表查询”这两点。

CREATE INDEX IDX_DOCTOR_DEPT ON DOCTOR(DEPT_ID); CREATE INDEX IDX_REG_PAT ON REGISTRATION(PAT_ID); CREATE INDEX IDX_REG_DOC ON REGISTRATION(DOC_ID); CREATE INDEX IDX_PRES_PAT ON PRESCRIPTION(PAT_ID); CREATE INDEX IDX_PRES_DOC ON PRESCRIPTION(DOC_ID); CREATE INDEX IDX_ITEM_PRES ON PRESCRIPTION_ITEM(PRES_ID); CREATE INDEX IDX_ITEM_DRUG ON PRESCRIPTION_ITEM(DRUG_ID);

需要注意的是,不要给状态类字段(REG_STATUS)单独建索引。状态字段区分度很低,只有 0/1/2 三种值,全表扫描比走索引更快,Oracle 优化器大概率也会忽略这种索引。课设里另一个常见问题是把主键写成 VARCHAR2 还带业务前缀,比如 REG001、REG002,排序和关联都别扭,能用 NUMBER 序列就不要用字符串主键。

到这里整个库的骨架就出来了。我一般会在建完表之后立刻插入一小批测试数据,再跑几条连表查询,确认外键没有建错方向。下一步就是往库里写业务逻辑,也就是第 3 章的存储过程。

3. PL/SQL 实战:存储过程与触发器把业务逻辑收进数据库

3.1 挂号存储过程:事务与异常处理

课设里最容易得分的是存储过程。理由很简单:老师要求“体现 Oracle 特性”,增删改查 JDBC 也能干,PL/SQL 才是 Oracle 区别于 MySQL 的硬功夫。这份资源里我建议重点看三个存储过程:挂号、退号退款、药品出库扣减库存。

挂号这个操作在业务上不是“INSERT 一条记录”这么简单,它至少要做两件事:写入挂号记录,同时判断该医生当天是否还有号源。更麻烦的是并发场景——两个人同时挂最后一个号,不能两个人都成功。为了演示事务和行锁,常见做法是给 DOCTOR 表加一个当日剩余号数字段 DOC_LEFT,然后在存储过程里用 SELECT ... FOR UPDATE 先锁住医生行。

CREATE OR REPLACE PROCEDURE PROC_REGISTER( P_PAT_ID IN NUMBER, P_DOC_ID IN NUMBER, P_FEE IN NUMBER, P_REG_ID OUT NUMBER ) AS V_LEFT NUMBER; BEGIN SAVEPOINT SP_REG; -- 锁定医生行,防止并发下超挂 SELECT DOC_LEFT INTO V_LEFT FROM DOCTOR WHERE DOC_ID = P_DOC_ID FOR UPDATE; IF V_LEFT <= 0 THEN RAISE_APPLICATION_ERROR(-20001, '该医生号源已用完'); END IF; SELECT SEQ_REG.NEXTVAL INTO P_REG_ID FROM DUAL; INSERT INTO REGISTRATION(REG_ID, PAT_ID, DOC_ID, REG_TIME, REG_STATUS, REG_FEE) VALUES (P_REG_ID, P_PAT_ID, P_DOC_ID, SYSDATE, '0', P_FEE); UPDATE DOCTOR SET DOC_LEFT = DOC_LEFT - 1 WHERE DOC_ID = P_DOC_ID; COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK TO SP_REG; RAISE; END PROC_REGISTER;

逻辑说明:先 SAVEPOINT 建保存点,SELECT 医生行并加行级锁,号源不足时抛业务异常;号源充足就取序列下一个值做挂号 ID,插入挂号表,再把医生剩余号数减一。异常处理里回滚到保存点后重新 RAISE,把错误抛给 Java 层去提示。

参数说明:P_PAT_ID、P_DOC_ID、P_FEE 是输入参数,P_REG_ID 是输出参数,Java 端通过 CallableStatement 注册 OUT 参数拿到新生成的挂号 ID。DOC_LEFT 需要在 init.sql 里补上:ALTER TABLE DOCTOR ADD (DOC_LEFT NUMBER(3) DEFAULT 20); 初始值按一个医生一天 20 个号算。

这里有一个对新手来说特别容易忽略的细节:SELECT ... FOR UPDATE 必须在事务里才有意义,所以这个存储过程里没有在 SELECT 之前提交任何东西,最后用 COMMIT 收尾。如果 Java 端在调用前把连接设成了自动提交 setAutoCommit(true),FOR UPDATE 的锁会在语句结束时释放,并发保护就失效了。这一点第 4 章会再强调。

另外提醒一个坑:RAISE_APPLICATION_ERROR 的编号范围是 -20001 到 -20999,超出会报 ORA-21000。我第一次写存储过程时用的 -30001,编译直接失败,折腾了十分钟才发现是编号越界。

3.2 退费与库存扣减:事务边界怎么划

退号是挂号的逆操作,但事务边界要画在“退号”和“释放号源”两件事上。退号时必须先确认挂号状态还是 0(未就诊),已就诊的号不能退;退号成功后把医生号源加回来。药品出库则是发药时的动作,涉及更新药品库存和扣减逻辑,两个操作要么一起成功要么一起失败。

CREATE OR REPLACE PROCEDURE PROC_REFUND( P_REG_ID IN NUMBER ) AS V_STATUS CHAR(1); BEGIN SELECT REG_STATUS INTO V_STATUS FROM REGISTRATION WHERE REG_ID = P_REG_ID FOR UPDATE; IF V_STATUS <> '0' THEN RAISE_APPLICATION_ERROR(-20002, '该挂号记录不可退号'); END IF; UPDATE REGISTRATION SET REG_STATUS = '2' WHERE REG_ID = P_REG_ID; UPDATE DOCTOR D SET D.DOC_LEFT = D.DOC_LEFT + 1 WHERE D.DOC_ID = (SELECT DOC_ID FROM REGISTRATION WHERE REG_ID = P_REG_ID); COMMIT; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20003, '挂号记录不存在'); WHEN OTHERS THEN ROLLBACK; RAISE; END PROC_REFUND;

逻辑说明:先对挂号记录加行锁并读状态,防止并发退同一笔号;状态校验通过后更新状态、归还号源,两个 UPDATE 在一个事务里。NO_DATA_FOUND 单独捕获转成业务异常,否则 Java 端拿到的会是 ORA-01403,而不是一句能看懂的业务提示。

库存扣减存储过程的结构类似,但多一个检查点:扣减前必须判断 STOCK 是否充足,扣减语句要写成 STOCK = STOCK - 数量,而不是“先查再算”。先查再算在并发下会被覆盖,直接 UPDATE 配合 WHERE STOCK >= 数量才是一步原子操作:

UPDATE DRUG SET STOCK = STOCK - P_QTY WHERE DRUG_ID = P_DRUG_ID AND STOCK >= P_QTY; IF SQL%ROWCOUNT = 0 THEN RAISE_APPLICATION_ERROR(-20004, '库存不足'); END IF;

SQL%ROWCOUNT 是 PL/SQL 里的隐式游标属性,UPDATE 影响行数为 0 说明库存不够或者药品不存在。这种写法比 SELECT STOCK INTO ... 再判断更优雅,也是答辩时可以主动讲给老师听的一个点。

3.3 触发器:记录操作日志的常见做法

触发器在课设里的定位比较微妙:用得好是亮点,用滥了会给自己埋坑。这份资源里触发器只做一件简单的事——记录挂号操作的日志:

CREATE OR REPLACE TRIGGER TRG_REG_LOG AFTER INSERT ON REGISTRATION FOR EACH ROW BEGIN INSERT INTO REG_LOG(LOG_ID, REG_ID, PAT_ID, DOC_ID, OP_TIME, OP_TYPE) VALUES (SEQ_LOG.NEXTVAL, :NEW.REG_ID, :NEW.PAT_ID, :NEW.DOC_ID, SYSDATE, 'INSERT'); END;

逻辑说明:AFTER INSERT 行级触发器,在 REGISTRATION 插入成功后,把关键字段写到日志表 REG_LOG。:NEW 代表新插入行的列值,这是 Oracle 触发器的标准写法。REG_LOG 表结构很简单:LOG_ID 主键、REG_ID、PAT_ID、DOC_ID、OP_TIME、OP_TYPE。

为什么说触发器别用多?课设里常见的问题是:有人在 PATIENT 表上建触发器做“主键自增”,又在 REGISTRATION 表上建触发器,导致 Java 里每插一条记录就要多执行一段隐式逻辑,一旦触发器中报错,主操作也跟着回滚,排查成本很高。我的建议是:课设里触发器只做审计日志类辅助操作,主键生成放到存储过程或序列里显式控制,别写在触发器里。这样老师问“为什么不用触发器做自增”,你可以回答“自增逻辑显式化,避免隐式执行难以调试”,这是一个能站得住脚的设计取舍。往下就是 Java 端怎么把这些存储过程接起来,第 4 章专门讲 JDBC 连接。

4. Java 连接 Oracle:JDBC、DAO 与参数配置

4.1 驱动、URL 与连接池:先把通道打通

Java 端连接 Oracle 的经典组合是 ojdbc 驱动加标准 JDBC。驱动 jar 的版本要跟服务端匹配,Oracle 11g 对应 ojdbc6 或 ojdbc7,Oracle 12c 和 19c 用 ojdbc8。拿 ojdbc8 去连 11g 一般也兼容,但个别版本会报 ORA-28040: No matching authentication protocol,第 5 章会展开说。先给一个驱动版本对照表,方便你排查环境时快速定位:

服务端版本推荐驱动 jarJDK 要求
Oracle 11g R2ojdbc6 / ojdbc7JDK 6 及以上
Oracle 12c R2ojdbc8JDK 8 及以上
Oracle 19cojdbc8JDK 8 及以上

最简连接方式:

Class.forName("oracle.jdbc.OracleDriver"); String url = "jdbc:oracle:thin:@localhost:1521:ORCL"; String user = "hospital"; String password = "hospital123"; Connection conn = DriverManager.getConnection(url, user, password);

逻辑说明:Class.forName 加载驱动,Oracle 的驱动类名有 oracle.jdbc.driver.OracleDriver 和 oracle.jdbc.OracleDriver 两种写法,从 ojdbc6 开始后者是标准入口。URL 里 thin 表示纯 Java 驱动,不需要装 Oracle 客户端;localhost 换成真实 IP,1521 是默认监听端口,ORCL 是实例名。如果库配置的是 SID 而不是 Service Name,写法通常也一样,拿不准就找提供环境的人要连接串。

我一般会在课设里用一个 DBUtil 工具类管理连接,避免每个 DAO 里都写一遍 DriverManager 代码:

public class DBUtil { private static final String URL = "jdbc:oracle:thin:@localhost:1521:ORCL"; private static final String USER = "hospital"; private static final String PASSWORD = "hospital123"; static { try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) { throw new RuntimeException("Oracle 驱动加载失败,请检查 ojdbc jar 是否在 classpath", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

参数说明:URL、USER、PASSWORD 硬编码在常量里,课设够用。如果用的工具版本比较新,可以把这三个值放到 db.properties 里由 Properties 加载,避免每次改连接串都重新编译。getConnection 每次返回一个新连接,性能上有损耗,但课设演示完全没问题。如果想体现工程能力,可以引入 HikariCP 连接池,配置要点如下:

HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:oracle:thin:@localhost:1521:ORCL"); config.setUsername("hospital"); config.setPassword("hospital123"); config.setMaximumPoolSize(10); config.setDriverClassName("oracle.jdbc.OracleDriver"); HikariDataSource ds = new HikariDataSource(config);

HikariCP 参数含义:MaximumPoolSize 10 表示最多维持 10 个物理连接,课设并发量低,设 5 也够;MinimumIdle 默认与 MaximumPoolSize 相同。连接池不是必须的,但能体现“连接是稀缺资源”这个意识。

4.2 DAO 层:增删改查怎么封装

DAO 层的职责是隔离 SQL 与业务逻辑。课设里我会按表建 DAO,比如 PatientDAO、DoctorDAO、RegistrationDAO。下面以查询某医生当天挂号记录为例:

public List<Map<String, Object>> findRegsByDoc(int docId) throws SQLException { String sql = "SELECT R.REG_ID, P.PAT_NAME, R.REG_TIME, R.REG_STATUS " + "FROM REGISTRATION R JOIN PATIENT P ON R.PAT_ID = P.PAT_ID " + "WHERE R.DOC_ID = ? ORDER BY R.REG_TIME DESC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, docId); try (ResultSet rs = ps.executeQuery()) { List<Map<String, Object>> list = new ArrayList<>(); while (rs.next()) { Map<String, Object> row = new HashMap<>(); row.put("regId", rs.getInt("REG_ID")); row.put("patName", rs.getString("PAT_NAME")); row.put("regTime", rs.getTimestamp("REG_TIME")); row.put("regStatus", rs.getString("REG_STATUS")); list.add(row); } return list; } } }

逻辑说明:使用 PreparedStatement 的 ? 占位符传参,而不是拼字符串,这是防 SQL 注入的基本功。try-with-resources 保证 Connection、PreparedStatement、ResultSet 自动关闭,避免 Oracle 连接被耗尽。这一点特别重要,Oracle 并发连接数默认只有 150,课设里频繁打开不关闭,跑到一半就报 ORA-12519: TNS:no appropriate service handler found。

参数说明:setInt 对应 Oracle NUMBER 列,setTimestamp 对应 DATE 列,setString 对应 VARCHAR2。返回用 List<Map<String, Object>> 是课设里最省事的做法,胜在通用;如果你想体现面向对象功底,可以建一个 RegistrationVO 类,把列映射到实体字段。答辩时用 VO 类比 Map 多两分工程感,但 Map 也绝不至于扣分。

增删改查里最容易出问题的是删除。外键约束下,直接 DELETE FROM PATIENT WHERE PAT_ID = 1 会报 ORA-02292,因为挂号表、处方表都引用着病人。解决方案有两个:一是删除前先删子表记录;二是在业务里做逻辑删除,加一个 IS_DELETED 字段。课设建议用第二种,既能保住数据完整性,也能在答辩时讲“软删除避免误删就诊历史”,比物理删除更符合医院系统的真实需求。

4.3 调用存储过程:CallableStatement 的三种写法

Java 调用 PL/SQL 存储过程用 CallableStatement。以第 3 章的 PROC_REGISTER 为例,完整调用代码:

public int register(int patId, int docId, double fee) throws SQLException { String sql = "{call PROC_REGISTER(?, ?, ?, ?)}"; try (Connection conn = DBUtil.getConnection(); CallableStatement cs = conn.prepareCall(sql)) { cs.setInt(1, patId); cs.setInt(2, docId); cs.setDouble(3, fee); cs.registerOutParameter(4, Types.INTEGER); cs.execute(); return cs.getInt(4); } }

逻辑说明:{call PROC_REGISTER(?, ?, ?, ?)} 是标准调用语法。前三个参数按存储过程的输入参数顺序绑定,第四个注册为 OUT 参数,类型用 Types.INTEGER 对应 Oracle NUMBER。execute 执行后 getInt(4) 取出存储过程生成的挂号 ID。整个过程中事务边界由存储过程内部的 COMMIT 控制,Java 端不需要也不能再调用 commit,否则可能造成事务提前提交。这一点很关键,如果你在 Java 端把自动提交设成 false 又调了 commit,存储过程里已经 commit,连接层的事务状态会对不上,后续错误回滚完全无效。

第二种写法是使用 setAutoCommit 配合存储过程传参,适合“Java 侧先插一条主记录、再调用存储过程更新子表”的场景:

conn.setAutoCommit(false); try { // 先插入主记录 insertRegMaster(conn, regId, patId, docId); // 调用存储过程 callSomeProcedure(conn, regId); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); }

第三种写法是通过 PreparedStatement 调用存储函数,取 RETURN 值,比如返回一个统计值的函数。课设里用得少,不展开。你需要记住的原则是:事务边界要么全在 Java 端,要么全在存储过程端,不要两边交叉控制。我见过一个课设,Java 里 setAutoCommit(false),存储过程里又写了 COMMIT,结果演示时第一次点击正常,第二次点击数据重复,就是因为两边都控制了提交边界。

5. 避坑与排查:Oracle 课设最容易翻车的五个位置

这一章用几条踩坑记录换你少熬几个通宵。每个坑都按“现象、原因、解决”说清楚。

5.1 监听服务起不来,报 ORA-12541 / ORA-12514

现象:安装 Oracle 11g 后,SQL Developer 或 Java 代码连不上,报 ORA-12541: TNS:no listener 或 ORA-12514: TNS:listener does not currently know of service requested in connect descriptor。Windows 服务列表里 OracleOraDb11g_home1TNSListener 处于停止状态,手动启动瞬间又停。

原因:最常见两个。第一是安装时端口 1521 被占用,监听配置的端口和实际不符;第二是 Oracle 11g 的监听服务依赖注册表里的 ORACLE_HOME 路径,如果本机装过多个 Oracle 版本或环境变量被改过,服务启动就会中途失败。还有一类是 listener.ora 文件被误改。

解决:先执行 netstat -ano | findstr 1521 看端口被谁占用,如果被占,改 listener.ora 里的 PORT 换一个端口,同时把 Java URL 里的 1521 一起改。如果端口没被占用但服务起不来,打开 listener.ora,检查 HOST 是否写成了主机名,有些机器的 hosts 解析有问题,把 HOST 改成 127.0.0.1 或具体 IP 最省事。改完用 lsnrctl start 手动启动,命令行输出的错误信息比 Windows 服务日志直观得多。

补充一个小细节:Oracle 11g 的监听日志文件 listener.log 会无限增长,有的环境跑几个月日志几个 GB,导致监听变慢、连接超时。课设阶段不会遇到,但如果老师让你部署到服务器演示,记得定期清空:停监听、删日志文件、再启动,Oracle 会自动重建。

5.2 查询出来全是问号或乱码

现象:Java 控制台和 SQL Developer 里查中文显示成??,或者插入的中文在界面上显示错乱。

原因:客户端字符集与数据库字符集不一致。数据库建库时字符集如果是 AL32UTF8,而 Windows 命令行默认 GBK,代码页 936;Java 程序里 JDBC 连接的 NLS_LANG 没配或者配错。

解决:最省事的办法是统一到 AL32UTF8。JDBC URL 上加 ?useUnicode=true&characterEncoding=UTF-8 对 Oracle 无效,Oracle 的 JDBC 连接字符集受 NLS_LANG 环境变量控制。正确做法是在 Java 启动参数里加 -Dfile.encoding=UTF-8,并保证源文件也是 UTF-8 保存。操作层面,插入前先执行 ALTER SESSION SET NLS_LANGUAGE = 'SIMPLIFIED CHINESE'; 治本还是检查环境变量里的 NLS_LANG。课设机器上常见配置是 NLS_LANG=SIMPLIFIED CHINESE_CHINA.ZHS16GBK,如果数据库是 AL32UTF8,把 NLS_LANG 改成 SIMPLIFIED CHINESE_CHINA.AL32UTF8 通常能解决。如果改完还乱码,检查是否建库时字符集选错了,ZHS16GBK 的库想改成 AL32UTF8 只能重建库,没有后悔药。

5.3 ORA-00933:SQL 命令未正确结束

现象:一条在 MySQL 里跑得很顺的 SQL,搬到 Oracle 报 ORA-00933: SQL command not properly ended。最常见的是分页语句 SELECT * FROM REGISTRATION LIMIT 0, 20,在 Oracle 里直接报这个错。

原因:Oracle 没有 MySQL 的 LIMIT 语法,也没有 SQL Server 的 TOP,分页要用 ROWNUM 伪列。

解决:Oracle 12c 之前用三层子查询分页,12c 开始支持 OFFSET ... FETCH。课设普遍用 11g,优先写 ROWNUM 版本:

SELECT * FROM ( SELECT R.*, ROWNUM RN FROM (SELECT * FROM REGISTRATION ORDER BY REG_TIME DESC) R WHERE ROWNUM <= 20 ) WHERE RN > 0;

逻辑说明:最内层子查询先排序,第二层给每行加 ROWNUM 并截断到最大行号,最外层再过滤起始行。注意 ROWNUM 要在 ORDER BY 之后的子查询里生成,否则先取 20 行再排序,分页就乱了。12c 以上可以直接写 SELECT * FROM REGISTRATION ORDER BY REG_TIME DESC OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY,但答辩老师如果规定用 11g,还是老老实实写 ROWNUM。

顺便提一句,上面 SQL 里的 DUAL 空表也常被问。SELECT 1 FROM DUAL 是 Oracle 特有的写法,MySQL 用 SELECT 1 就行。有同学问 DUAL 能存多大,其实 DUAL 永远是单行单列表,用来取序列值或系统常量,没有存储量概念。

5.4 ORA-02292:外键约束导致删不掉数据

现象:往 DOCTOR 表 DELETE 一行时报 ORA-02292: integrity constraint (FK_REG_DOC) violated - child record found。

原因:REGISTRATION 表有外键引用 DOCTOR,子表存在记录时主表不能直接删。这是设计层面的约束生效,不是语法错。

解决:删除顺序按“先子后主”走:先 DELETE FROM REGISTRATION WHERE DOC_ID = xxx; 再删医生。如果确实想保留子表数据,就别物理删,用前面提到的软删除字段。还有一点要注意,TRUNCATE TABLE 在 Oracle 里默认不能对有外键引用的表执行,必须 DISABLE 约束再 TRUNCATE:

ALTER TABLE REGISTRATION DISABLE CONSTRAINT FK_REG_DOC; TRUNCATE TABLE REGISTRATION; ALTER TABLE REGISTRATION ENABLE CONSTRAINT FK_REG_DOC;

课设演示中基本碰不到 TRUNCATE,知道这个限制就行,不用主动在答辩里展示。

5.5 ORA-00001:违反唯一约束,主键重复

现象:程序第一次跑插入正常,第二次跑同样的数据报 ORA-00001: unique constraint (PK_PATIENT) violated。

原因:主键没有用序列,而是 Java 端用 System.currentTimeMillis() 或随机数生成 ID,重启后时间戳重复;或者用了 SELECT MAX(REG_ID)+1 的方式,并发时取到同一个值。

解决:Oracle 不像 MySQL 有 AUTO_INCREMENT,主键自增的标准做法是序列加触发器,或者在存储过程里显式取序列值。推荐把主键生成收敛到存储过程,就像第 3 章 PROC_REGISTER 里 SELECT SEQ_REG.NEXTVAL INTO P_REG_ID FROM DUAL 那样。如果你用 Hibernate 或 JPA,把 @GeneratedValue 策略配成 SEQUENCE,并设置 hibernate.id.new_generator_mappings=false,否则会报 ORA-02289: sequence does not exist。这个点我在好几个课设 demo 里见过,代码里配了 GenerationType.AUTO,MySQL 上跑没事,换到 Oracle 就报序列不存在。

序列和触发器组合时,注意触发器里用 :NEW.REG_ID 赋值,但必须在 INSERT 之前执行,也就是 BEFORE INSERT 触发器,否则主键 NULL 直接报 ORA-01400。这些问题处理完,剩下的功夫就都在演示细节上,下面三个技巧能把课设从“能跑”拉到“好看”。

6. 进阶:把课设演示变成答辩加分项的三个技巧

到这一步,表结构、存储过程、Java 连接都已经打通,接下来最实在的提升是让这套资源在演示时更像一个系统,而不是一堆代码。三个技巧,我自己每次带课设都会强制过一遍。

第一个技巧是准备一个一键初始化脚本。把所有建表、建序列、建索引、创建用户授权全部写进 init.sql,再用 sqlplus hospital/hospital123@localhost:1521/ORCL @init.sql 批量执行。这样换一台机器演示,一个命令就能重建环境,比在图形工具里逐个执行 SQL 高效得多。顺序必须注意:先建用户、再建表、再建序列、再建触发器和存储过程、最后插测试数据,否则先插数据后建外键会报约束错误。

第二个技巧是用视图封装高频查询。比如统计各科室每天的挂号量,写成视图后 Java 端只查一张虚拟表,代码复杂度直线下降:

CREATE OR REPLACE VIEW V_DEPT_REG_COUNT AS SELECT D.DEPT_NAME, TRUNC(R.REG_TIME) AS REG_DATE, COUNT(*) AS REG_CNT FROM REGISTRATION R JOIN DOCTOR D2 ON R.DOC_ID = D2.DOC_ID JOIN DEPT D ON D2.DEPT_ID = D.DEPT_ID GROUP BY D.DEPT_NAME, TRUNC(R.REG_TIME);

Java 端查视图和查普通表没有区别,但 SQL 更短,答辩时能顺带讲清楚“视图是逻辑表,不占额外存储,每次查询实时计算”这个高频考点。

第三个技巧是把异常场景预演一遍。演示时老师很可能问:病人挂了号没缴费怎么办?退号时号源已经放完了怎么办?我习惯在测试数据里故意留一条挂账单不缴费,演示时对同一张表连续跑两次“确认缴费”,第二次数据库用约束和存储过程把边界挡住,这个瞬间比讲十页 PPT 都有说服力。

从那以后,我每次拿到新的课设资源,都强制自己先跑通 init.sql 再动 Java 代码;遇到环境问题先查监听和字符集,不折腾代码。这份基于 Java + Oracle 的医院系统数据库资源适合当起点,但真正拉开分数差距的,永远是边界检查、事务划分和“能不能扛住答辩追问”这三个习惯。希望帮到你。

本文还有配套的精品资源,点击获取

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

人物玩手机图片数据集构建与YOLOv8检测实战:从标注到避坑

简介&#xff1a;面向目标检测与行为识别任务的深度学习/机器学习图像数据集&#xff0c;适合训练手机使用行为检测模型的算法工程师与科研人员。数据源自现实场景拍摄与网络收集&#xff0c;由团队自行标注&#xff0c;标注质量高。图片围绕人物持手机状态&#xff0c;设置tel…

作者头像 李华
网站建设 2026/10/11 12:35:54

从测试案例1到测试脚手架:用pytest搭建可复用回归防线

如果你在各种代码仓库、学习笔记、甚至面试项目里见过一种命名——“测试案例1”、“测试案例2”、“测试案例&#xff08;最终版&#xff09;”——那你一定知道我说的是什么。这类项目往往不起眼&#xff0c;目录里就几个文件&#xff0c;却能真实反映一个人对工程质量的理解…

作者头像 李华
网站建设 2026/10/11 12:35:54

win64-11gR2-client.zip 部署指南:环境变量配置与连接问题排查

简介&#xff1a;这份资源为Oracle 11g Release 2&#xff08;11.2&#xff09;64位Windows客户端安装包&#xff0c;面向需要在Windows平台上连接Oracle数据库服务器的开发者、DBA及数据库学习者&#xff0c;可用于搭建本地客户端环境、执行SQL Plus连接测试以及配置ODBC、JD…

作者头像 李华
网站建设 2026/10/11 12:35:52

暗黑4 d3d12.dll缺失怎么办?官方工具安全修复DX12组件完整指南

“无法启动&#xff1a;d3d12.dll缺失”“找不到 d3d12.dll”……最近这款游戏社区里关于暗黑4启动报错的求助帖突然多了起来。第一次遇到这个问题的人&#xff0c;第一反应往往是上网搜dll修复工具&#xff0c;甚至从某个网站下载一个裸文件就往System32里丢。我劝你千万别这么…

作者头像 李华
网站建设 2026/10/11 12:35:51

全1输入引发的线上事故:从边界值测试到参数校验的排查实践

我最近在排查一个线上问题的时候&#xff0c;被一个输入值折腾得够呛&#xff0c;就是这串看起来完全没智力的数字&#xff1a;1111111111。用户在一个资料表单里随手填了这串数字&#xff0c;前端提示“保存成功”&#xff0c;后端却报参数不合法&#xff0c;日志里还留了一段…

作者头像 李华
网站建设 2026/10/11 12:34:35

从UEFI到systemd:电脑启动全过程与开机慢排查指南

先问大家一个场景&#xff1a;同办公室两台电脑&#xff0c;A同事按下电源键后泡了杯咖啡回来&#xff0c;系统还没进桌面&#xff1b;B同事开完机连微信都登录完了。差距到底在哪&#xff1f;答案基本都藏在"Boot"这个词背后。很多人把开机理解为"电脑亮起来然…

作者头像 李华