资讯详情

JDBC直连MySQL实现仓储系统全链路事务管理

📅 2026/9/10 8:25:59 | 华诺云谱 👁 阅读
JDBC直连MySQL实现仓储系统全链路事务管理
简介这是一套面向Java初学者与毕业设计学生的物资管理系统完整源码项目聚焦仓储管理核心场景解决企业库存登记、采购出入库、供应商协同等实际业务痛点。资源包含212个文件以36个Java源文件、51个JSP页面、30个XML配置文件及24个JAR依赖库为主体覆盖MVC分层结构与数据库交互逻辑JSP实现用户友好的操作界面Java类封装DAO与业务逻辑XML配置数据源与Spring框架整体压缩包仅11.7MB轻量易部署。已有319人学习下载适合课程设计、毕设开发或Java Web技术栈综合实践。读者可直接导入Eclipse运行获得含商品/供应商/采购/出入库全模块的可执行系统配套文档清晰代码结构规范DAO层命名如InstorageDAO、OutStorageDAO与实体类Goods、User、Provider对应严谨是理解JDBC连接MySQL、前后端交互与权限验证机制的优质教学案例。1. 这不是又一个CRUD练习——它用JDBC直连MySQL跑通了真实仓储业务闭环你可能已经见过几十个“Java图书管理系统”“学生成绩系统”但这个物资管理系统源码是少数几个把「入库单→库存扣减→供应商对账→出库审批流」全链路用原生JDBC落地的毕业设计级项目。它不依赖Spring Boot自动装配不包装MyBatis动态SQL而是用InstorageDAO.class、OutStorageDAO.class、ProviderDAO.class等12个DAO类手写PreparedStatement参数绑定、事务控制和结果集映射。这意味着你能清晰看到每一条SQL如何影响goods表的stock_quantity字段也能在LoanForm.class里读到出库申请被驳回时的status REJECTED状态机逻辑。它适合两类人一是正在准备Java后端面试、需要讲清“JDBC事务怎么保证入库不出错”的应届生二是想快速搭建轻量级内部仓储工具、又不想引入Spring生态复杂度的中小团队运维人员。所有DAO操作都基于MySQL 5.7建表语句藏在src/main/resources/sql/下连外键约束和索引命名都按生产环境习惯如idx_goods_provider_id不是教学玩具。2. 从DAO层切入为什么用原生JDBC而不是ORM框架2.1 选型依据轻量、可控、易调试的仓储场景适配物资管理系统的典型操作是高频小事务单次入库新增3条记录instorage主表、instorage_detail明细表、goods库存更新要求原子性且响应快。Spring Data JPA的二级缓存在此类场景反而增加一致性风险而MyBatis的XML映射层在简单CRUD中显得冗余。本项目选择JDBC直连核心优势有三点第一Connection.setAutoCommit(false)配合commit()/rollback()可精确控制跨表事务边界比如InstorageDAO.addInstorage()方法内先插入主单再循环插入明细任一环节失败即回滚第二ResultSetMetaData能动态获取列名使QueryDAO.queryByCondition()支持任意字段组合查询如SELECT * FROM goods WHERE category ? AND stock_quantity ?第三无反射代理开销UserDAO.login()验证密码时直接调用MessageDigest.getInstance(SHA-256)哈希比对避免Hibernate的Column(transform ...)等扩展语法。提示不要误以为“不用ORM不专业”。在库存扣减这类强一致性场景手写UPDATE goods SET stock_quantity stock_quantity - ? WHERE id ? AND stock_quantity ?并检查executeUpdate()返回值是否为1比任何ORM的乐观锁更直观可靠。2.2 DAO类结构解析以InstorageDAO为例看事务封装InstorageDAO.class是入库模块的核心其addInstorage(Instorage instorage, ListInstorageDetail details)方法实现典型的“主-明细”事务。代码逻辑如下public boolean addInstorage(Instorage instorage, ListInstorageDetail details) { Connection conn null; PreparedStatement psMain null; PreparedStatement psDetail null; try { conn JDBCUtil.getConnection(); // 从Druid连接池获取 conn.setAutoCommit(false); // 关闭自动提交 // 1. 插入入库主单 psMain conn.prepareStatement( INSERT INTO instorage (id, provider_id, instorage_date, operator_id, remark) VALUES (?, ?, ?, ?, ?), Statement.RETURN_GENERATED_KEYS ); psMain.setString(1, instorage.getId()); psMain.setString(2, instorage.getProviderId()); psMain.setDate(3, new java.sql.Date(instorage.getInstorageDate().getTime())); psMain.setString(4, instorage.getOperatorId()); psMain.setString(5, instorage.getRemark()); psMain.executeUpdate(); // 2. 获取自增主键用于关联明细 ResultSet rs psMain.getGeneratedKeys(); String instorageId null; if (rs.next()) { instorageId rs.getString(1); } // 3. 批量插入明细并更新商品库存 psDetail conn.prepareStatement( INSERT INTO instorage_detail (instorage_id, goods_id, quantity, unit_price) VALUES (?, ?, ?, ?) ); for (InstorageDetail detail : details) { psDetail.setString(1, instorageId); psDetail.setString(2, detail.getGoodsId()); psDetail.setInt(3, detail.getQuantity()); psDetail.setDouble(4, detail.getUnitPrice()); psDetail.addBatch(); // 同步更新goods表库存 PreparedStatement psUpdate conn.prepareStatement( UPDATE goods SET stock_quantity stock_quantity ? WHERE id ? ); psUpdate.setInt(1, detail.getQuantity()); psUpdate.setString(2, detail.getGoodsId()); int rows psUpdate.executeUpdate(); if (rows ! 1) { throw new SQLException(商品库存更新失败goods_id detail.getGoodsId()); } } psDetail.executeBatch(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); // 显式回滚 } catch (SQLException rollbackEx) { e.addSuppressed(rollbackEx); } } e.printStackTrace(); return false; } finally { JDBCUtil.close(psDetail, psMain, conn); } }2.2.1 关键参数与异常处理逻辑说明Statement.RETURN_GENERATED_KEYS必须显式声明否则getGeneratedKeys()返回空结果集导致明细表无法关联主单IDpsDetail.addBatch()批量插入提升性能但需注意MySQL默认max_allowed_packet限制建议设为32M库存更新校验if (rows ! 1)防止商品ID不存在或并发修改这是比乐观锁更早的防御层e.addSuppressed(rollbackEx)将回滚异常作为抑制异常附加到主异常上便于排查事务中断原因。2.3 数据库表关系与JDBC映射策略系统共8张核心表通过外键形成强约束关系。关键设计点如下表所示表名主要字段外键约束JDBC映射要点goodsid,name,category,stock_quantity,unit无stock_quantity用getInt()读取避免getDouble()精度丢失providerid,name,contact,phone无contact字段含中文PreparedStatement.setString()自动处理编码instorageid,provider_id,instorage_dateprovider_id → provider.id日期字段用java.sql.Date转换非LocalDateTimeJDBC 4.2前不支持instorage_detailinstorage_id,goods_id,quantityinstorage_id → instorage.id,goods_id → goods.id主键为联合主键executeBatch()前需确保instorage_id已生成注意OutStorageDAO.class中出库逻辑与入库镜像但库存更新SQL为stock_quantity stock_quantity - ?且必须前置校验SELECT stock_quantity FROM goods WHERE id ?是否足够否则UPDATE返回0行却无异常——这是新手最常踩的坑。3. 用户认证与权限控制基于Session的轻量级安全实现3.1 登录流程中的密码安全实践UserDAO.class的login(String username, String password)方法不存储明文密码而是采用SHA-256哈希加盐。关键代码如下public User login(String username, String password) { String sql SELECT id, username, password_hash, salt, role FROM user WHERE username ?; try (Connection conn JDBCUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { String storedSalt rs.getString(salt); String storedHash rs.getString(password_hash); String inputHash hashPassword(password, storedSalt); // 盐值拼接后哈希 if (inputHash.equals(storedHash)) { User user new User(); user.setId(rs.getString(id)); user.setUsername(rs.getString(username)); user.setRole(rs.getString(role)); // role字段值为ADMIN或OPERATOR return user; } } } } catch (SQLException e) { e.printStackTrace(); } return null; }3.1.1 密码哈希实现细节盐值salt为16字节随机字符串存于数据库user.salt字段每次注册独立生成hashPassword()方法使用MessageDigest.getInstance(SHA-256)输入为password salt的UTF-8字节数组对比时严格使用String.equals()而非避免字符串驻留导致的时序攻击风险。3.2 基于角色的菜单与操作拦截系统未使用Shiro或Spring Security而是通过IfDeal.class名称暗示“是否可操作”实现运行时权限判断。例如OutStorageDAO.checkPermission(String userId, String action)方法public boolean checkPermission(String userId, String action) { // 查询用户角色 String roleSql SELECT role FROM user WHERE id ?; String role null; try (Connection conn JDBCUtil.getConnection(); PreparedStatement ps conn.prepareStatement(roleSql)) { ps.setString(1, userId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { role rs.getString(role); } } } catch (SQLException e) { e.printStackTrace(); return false; } // 角色-操作矩阵 MapString, SetString permissionMatrix new HashMap(); permissionMatrix.put(ADMIN, Set.of(CREATE_INSTORAGE, CREATE_OUTSTORAGE, MANAGE_USER)); permissionMatrix.put(OPERATOR, Set.of(CREATE_INSTORAGE, CREATE_OUTSTORAGE)); return permissionMatrix.getOrDefault(role, Collections.emptySet()).contains(action); }3.2.1 权限校验的调用时机与位置界面层User.class的getMenuItems()方法根据role返回不同菜单列表如ADMIN显示“用户管理”项OPERATOR不显示DAO层OutStorageDAO.createOutStorage()开头调用checkPermission(userId, CREATE_OUTSTORAGE)失败则抛出自定义InsufficientPermissionExceptionServlet层OutStorageServlet中doPost()先验证Session中用户ID有效性再委托DAO执行。提示这种硬编码权限矩阵适合角色少于5个的系统。若需动态权限应扩展role_permission中间表并在checkPermission()中改为SQL查询SELECT COUNT(*) FROM role_permission WHERE role ? AND action ?。4. 部署与本地运行绕过Maven陷阱的实操指南4.1 MySQL环境准备与初始化脚本执行项目依赖MySQL 5.7需手动创建数据库并执行初始化SQL。步骤如下# 1. 登录MySQL假设root密码为空 mysql -u root -p # 2. 创建数据库注意字符集 CREATE DATABASE instorage_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 3. 切换数据库并执行建表脚本 USE instorage_system; SOURCE /path/to/your/project/src/main/resources/sql/create_tables.sql;create_tables.sql包含完整建表语句关键约束示例如下CREATE TABLE goods ( id VARCHAR(32) PRIMARY KEY, name VARCHAR(100) NOT NULL, category VARCHAR(50), stock_quantity INT NOT NULL DEFAULT 0, unit VARCHAR(20), created_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE instorage ( id VARCHAR(32) PRIMARY KEY, provider_id VARCHAR(32) NOT NULL, instorage_date DATE NOT NULL, operator_id VARCHAR(32) NOT NULL, remark TEXT, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (provider_id) REFERENCES provider(id), FOREIGN KEY (operator_id) REFERENCES user(id) );4.1.1 字符集与排序规则说明utf8mb4支持Emoji及四字节Unicode字符如某些生僻汉字避免INSERT时乱码utf8mb4_unicode_ci比utf8mb4_general_ci更准确的Unicode排序尤其对中文姓名排序更合理DEFAULT CURRENT_TIMESTAMPMySQL 5.6支持无需Java层生成时间戳。4.2 JDBC连接配置与Druid连接池参数调优数据库连接信息配置在src/main/resources/jdbc.properties中jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/instorage_system?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.passwordyour_password注意serverTimezoneAsia/Shanghai必须显式指定否则java.sql.Date转换时区错误导致日期偏移allowPublicKeyRetrievaltrue解决MySQL 8.0默认禁用公钥检索的问题。Druid连接池配置在JDBCUtil.class的静态块中static { DruidDataSource dataSource new DruidDataSource(); dataSource.setDriverClassName(props.getProperty(jdbc.driver)); dataSource.setUrl(props.getProperty(jdbc.url)); dataSource.setUsername(props.getProperty(jdbc.username)); dataSource.setPassword(props.getProperty(jdbc.password)); // 关键调优参数 dataSource.setInitialSize(5); // 初始化5个连接 dataSource.setMinIdle(5); // 最小空闲连接数 dataSource.setMaxActive(20); // 最大活跃连接数根据并发预估 dataSource.setMaxWait(60000); // 获取连接最大等待60秒 dataSource.setTimeBetweenEvictionRunsMillis(60000); // 每分钟检测空闲连接 dataSource.setMinEvictableIdleTimeMillis(300000); // 连接空闲5分钟才回收 dataSource.setValidationQuery(SELECT 1); // 连接有效性检测SQL dataSource.setTestWhileIdle(true); // 空闲时检测 dataSource.setPoolPreparedStatements(true); // 开启PSCache dataSource.setMaxOpenPreparedStatements(20); // PSCache最大数量 instance dataSource; }4.2.1 生产环境参数调整建议参数开发环境值生产环境建议值调整原因maxActive2050~100支持更高并发入库请求maxWait6000030000缩短超时时间快速失败而非阻塞timeBetweenEvictionRunsMillis6000030000更频繁检测失效连接minEvictableIdleTimeMillis300000600000延长空闲连接存活时间减少重建开销4.3 Eclipse运行配置与常见启动错误排错在Eclipse中运行需正确配置构建路径和运行参数构建路径右键项目 →Properties→Java Build Path→Libraries→ 添加mysql-connector-java-8.0.28.jar和druid-1.2.8.jar版本需与pom.xml一致运行配置Run→Run Configurations→Java Application→ 新建 →Main class填com.example.instorage.MainServlet入口类名依实际而定VM参数可选添加-Dfile.encodingUTF-8确保日志中文不乱码。4.3.1 三类高频启动错误及解决方案错误现象根本原因解决方案java.lang.ClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL驱动jar未加入Build Path将mysql-connector-java-*.jar拖入Referenced Libraries文件夹或通过Maven依赖管理java.sql.SQLException: Access denied for user rootlocalhostJDBC配置中用户名/密码错误或MySQL未授权本地访问执行GRANT ALL PRIVILEGES ON instorage_system.* TO rootlocalhost IDENTIFIED BY your_password; FLUSH PRIVILEGES;java.lang.NoClassDefFoundError: javax/servlet/HttpServlet缺少Servlet API依赖在pom.xml中添加dependencygroupIdjavax.servlet/groupIdartifactIdjavax.servlet-api/artifactIdversion4.0.1/versionscopeprovided/scope/dependency5. 实战技巧用QueryDAO快速诊断库存不平问题5.1 库存不平的典型场景与SQL定位法当发现goods.stock_quantity与instorage_detail/outstorage_detail汇总不一致时不能仅靠日志排查。QueryDAO.class提供了针对性查询方法可直接在MySQL命令行执行等效SQL-- 步骤1查某商品所有入库总量 SELECT SUM(quantity) AS total_in FROM instorage_detail d JOIN instorage i ON d.instorage_id i.id WHERE d.goods_id G001 AND i.status APPROVED; -- 步骤2查某商品所有出库总量 SELECT SUM(quantity) AS total_out FROM outstorage_detail d JOIN outstorage o ON d.outstorage_id o.id WHERE d.goods_id G001 AND o.status APPROVED; -- 步骤3对比库存表当前值 SELECT stock_quantity FROM goods WHERE id G001; -- 验证公式stock_quantity (初始值) total_in - total_out5.1.1 自动化校验脚本BashMySQL将上述逻辑封装为可定时执行的校验脚本保存为check_inventory_balance.sh#!/bin/bash MYSQL_CMDmysql -u root -pyour_password instorage_system -N -s # 获取所有商品ID goods_ids$($MYSQL_CMD -e SELECT id FROM goods) for gid in $goods_ids; do # 计算入库总量 total_in$($MYSQL_CMD -e SELECT IFNULL(SUM(d.quantity),0) FROM instorage_detail d JOIN instorage i ON d.instorage_idi.id WHERE d.goods_id$gid AND i.statusAPPROVED) # 计算出库总量 total_out$($MYSQL_CMD -e SELECT IFNULL(SUM(d.quantity),0) FROM outstorage_detail d JOIN outstorage o ON d.outstorage_ido.id WHERE d.goods_id$gid AND o.statusAPPROVED) # 获取当前库存 current_stock$($MYSQL_CMD -e SELECT stock_quantity FROM goods WHERE id$gid) # 计算理论库存假设初始值为0 expected_stock$((total_in - total_out)) if [ $current_stock ! $expected_stock ]; then echo ALERT: 商品 $gid 库存不平当前:$current_stock, 理论:$expected_stock echo 入库:$total_in, 出库:$total_out fi done赋予执行权限并加入crontabchmod x check_inventory_balance.sh # 每日凌晨2点执行 echo 0 2 * * * /path/to/check_inventory_balance.sh /var/log/inventory_check.log 21 | crontab -5.2 使用LoanForm.class复现出库审批流断点LoanForm.class代表出库申请单其status字段流转为PENDING → APPROVED/REJECTED。当业务方反馈“申请提交后卡在PENDING”可模拟审批操作快速验证// 在测试类中手动触发审批 public void testApproveLoanForm() { String formId LF20240501001; OutStorageDAO dao new OutStorageDAO(); // 1. 检查当前状态 String currentStatus dao.getLoanFormStatus(formId); System.out.println(当前状态: currentStatus); // 应输出 PENDING // 2. 手动审批跳过前端按钮 boolean approved dao.approveLoanForm(formId, ADMIN_USER_ID); System.out.println(审批结果: approved); // true表示成功 // 3. 验证库存是否扣减 String goodsId dao.getGoodsIdByLoanForm(formId); int afterStock dao.getGoodsStock(goodsId); System.out.println(商品 goodsId 扣减后库存: afterStock); }此技巧的价值在于绕过前端JavaScript校验和网络延迟直接验证DAO层事务逻辑是否完备。若approveLoanForm()返回true但库存未变则问题必在UPDATE goods SET stock_quantity stock_quantity - ?语句的WHERE条件或参数绑定错误。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。