ARTICLE DETAIL

资讯详情

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

接手SSH车管项目先拆数据:Hibernate映射、Spring事务与Oracle分页实战

接手SSH车管项目先拆数据:Hibernate映射、Spring事务与Oracle分页实战 简介车辆管理系统StrutsHibernateSpringOracle是一套基于SSH整合架构的Java Web项目面向正在学习SSH框架整合、MVC分层开发及Oracle数据库应用的开发者也适合毕业设计或课程设计参考。压缩包内共594个文件约35.89MB包含122个Java与122个class源码文件、112个jar依赖库、68个JSP页面、54个XML配置以及JS/CSS/GIF等前端静态资源基本覆盖了从后端持久层、业务层、控制层到前端展示的完整工程结构。系统围绕车辆状态、维修记录、保养周期、燃油消耗等典型业务展开可帮助读者理解Struts如何分发请求、Hibernate如何映射数据表、Spring如何管理Bean与事务。压缩包中还包括Oracle数据库表设计相关文件便于学习车辆信息、车主、维修记录等表结构的规划与建表思路。资源目前已有504人学习下载内容预览中出现的DAO、Service、Action等类文件也说明代码组织规范适合对照源码梳理SSH项目的实际写法并快速应用到自己的项目中。1. 为什么接手一套 (strutshibernatespringoracle) 车管项目先拆数据拿到车辆管理系统(strutshibernatespringoracle).rar这类包第一件事不是解压跑起来而是先把这套技术栈的边界想清楚。Struts 2 负责页面跳转与请求参数Hibernate 做对象关系映射Spring 在中间管 Bean、管事务Oracle 承载车辆、司机、调度、维修的全部业务数据。这是 2008 到 2015 年间企业应用里最常见的 SSH 组合到现在仍有大量车管后台跑在 Oracle 10g/11g 上不是因为它新是因为业务数据和流程换不动。下面按一线接手视角把这套组合拆开先立数据模型再串 Spring 事务接着接 Struts 2 页面和 Oracle 分页最后落到运维里最容易被卡住的几个现场问题。2. 车辆管理的数据底盘Oracle 表结构与 Hibernate one-to-many 映射2.1 核心表怎么建车辆、司机、调度、维修一条派车记录不能只靠一张表。车辆管理系统至少要拆出四类核心实体车辆主档、司机主档、调度派车单、维修保养记录它们之间是典型的父子表外键关系。老项目里表名和字段命名风格差异很大但结构基本跑不出下面这个模型。表名用途关键字段示例T_VEHICLE车辆主档VEHICLE_ID NUMBER(12) 主键LICENSE_PLATE VARCHAR2(20)VEHICLE_TYPE VARCHAR2(2)ENGINE_NO VARCHAR2(30)BUY_DATE DATESTATUS VARCHAR2(2)T_DRIVER司机主档DRIVER_ID NUMBER(10)NAME VARCHAR2(50)ID_CARD_NO VARCHAR2(18)LICENSE_TYPE VARCHAR2(20)PHONE VARCHAR2(20)T_DISPATCH调度派车单DISPATCH_ID NUMBER(12)VEHICLE_ID 外键DRIVER_ID 外键START_TIME DATEEND_TIME DATESTART_ODO NUMBER(10)END_ODO NUMBER(10)T_MAINTENANCE维修保养记录MAINT_ID NUMBER(12)VEHICLE_ID 外键MAINT_DATE DATEODO_METER NUMBER(10)ITEM_DESC VARCHAR2(200)COST NUMBER(12,2)这套表有三个容易在 Oracle 上翻车的细节。第一字符列尽量用 VARCHAR2历史库很少用 NVARCHAR2。车牌、驾驶证号里有汉字的时候要确认数据库字符集是 ZHS16GBK 还是 AL32UTF8否则 Java 端长度校验和 Oracle 端完全对不上。第二金额与里程不要定义成不带精度的 NUMBERCOST 要写成 NUMBER(12,2)不然 Hibernate 映射 BigDecimal 时可能出现精度转换异常。第三Oracle 的 DATE 类型本身就带时分秒Java 端如果对应java.util.Date查询边界很容易出现“差一秒”问题见 2.3 的处理方式。2.2 Hibernate 映射文件车辆一对多维修记录、一对多调度历史 SSH 项目里 hbm.xml 存量极大翻包时经常看到的是Vehicle.hbm.xml而不是注解。车辆对维修记录、车辆对调度单都是一对多最典型的配置是这样set namemaintenanceList tableT_MAINTENANCE inversetrue lazytrue cascadeall-delete-orphan key columnVEHICLE_ID not-nulltrue/ one-to-many classcom.vehicle.entity.MaintenanceRecord/ /set这里三个属性必须逐个理解。inversetrue表示关系由子表维护保存维修记录时由 MaintenanceRecord 里的 vehicle 属性决定外键如果漏配 inverseHibernate 会在保存时对主表外键做两次 updateSQL 多一倍但不报错排查起来很隐蔽。cascadeall-delete-orphan的含义是在车辆对象上移除维修记录时Hibernate 会把对应数据库记录删除这个级联适合维修、派车这类完全依附于车辆的子表。lazytrue是必须保留的列表页加载车辆时不会把全部维修记录一起查出来否则一个页面能拖垮 Oracle。反过来子表里配多对一many-to-one namevehicle columnVEHICLE_ID classcom.vehicle.entity.Vehicle/并不需要把两个方向都写上inverse通常只在集合那一侧配置子表保持普通多对一引用即可。2.3 Oracle 日期边界和序列生成trunc(SYSDATE)、无自增主键车管系统里“今日派车”“当月维修”这类统计很频繁。Oracle 的标准写法是SELECT * FROM T_DISPATCH d WHERE d.START_TIME TRUNC(SYSDATE) AND d.START_TIME TRUNC(SYSDATE) 1;TRUNC(SYSDATE)把当前时间截到当天零点 1把右边界推到次日零点。不要写成START_TIME TRUNC(SYSDATE)那样会把当天 00:00:00 的数据漏掉也容易把次日 0 点整的数据多算进来。Hibernate 里对应做法是先在 Java 侧用 Calendar 算出当天起止时间再作为参数传入而不是在 HQL 里依赖数据库函数。主键方面Oracle 没有自增列历史表很少用IDENTITY最通用的是序列加触发器也可以在 Hibernate 里直接配置id namevehicleId typejava.lang.Long column nameVEHICLE_ID/ generator classsequence param namesequenceSEQ_VEHICLE_ID/param /generator /id这样每次 insert 前 Hibernate 会执行select SEQ_VEHICLE_ID.nextval from dual拿到主键。这里要注意Oracle 序列默认CACHE是 20如果系统有批量导入场景主键跳号是正常现象不要当成 bug 处理。3. Spring 串出 SessionFactory 与事务SSH 体系才站得住3.1 最少够用的 Spring 装配数据源、SessionFactory、DialectStruts 和 Hibernate 之间如果没有 SpringSession 和事务会散落在每个 DAO 里。SSH 项目的核心是把数据源、SessionFactory、事务管理器都交给 Spring常用配置如下bean iddataSource classorg.apache.commons.dbcp.BasicDataSource destroy-methodclose property namedriverClassName valueoracle.jdbc.OracleDriver/ property nameurl valuejdbc:oracle:thin:192.168.10.20:1521:ORCL/ property nameusername valuevehicle/ property namepassword valuevehicle_pwd/ property nameinitialSize value2/ property namemaxActive value20/ property namemaxWait value3000/ /bean bean idsessionFactory classorg.springframework.orm.hibernate3.LocalSessionFactoryBean property namedataSource refdataSource/ property namemappingResources list valuecom/vehicle/entity/Vehicle.hbm.xml/value valuecom/vehicle/entity/Dispatch.hbm.xml/value /list /property property namehibernateProperties props prop keyhibernate.dialectorg.hibernate.dialect.Oracle10gDialect/prop prop keyhibernate.show_sqltrue/prop prop keyhibernate.jdbc.batch_size20/prop /props /property /beanurl 这里的192.168.10.20:1521:ORCL是三段式最后一段是 SID。如果是服务名方式要用//192.168.10.20:1521/orcl这种斜杠写法两者混用会出现 ORA-12505 或 ORA-12514。driverClassName 写成oracle.jdbc.OracleDriver兼容性最好很多老包里的oracle.jdbc.driver.OracleDriver也能用但个别新驱动已经对旧包做了调整。Hibernate 版本不同Spring 的集成类也不同。老工程常是 Hibernate 3 配LocalSessionFactoryBean升级到 Hibernate 4/5 之后要改成对应版本的类否则会出现 NoSuchMethodError。集成场景常见配置类Hibernate 3 Spring 3.xorg.springframework.orm.hibernate3.LocalSessionFactoryBeanHibernate 4 Spring 4.xorg.springframework.orm.hibernate4.LocalSessionFactoryBeanHibernate 5 Spring 5.xLocalSessionFactoryBuilder或 JPA 的LocalContainerEntityManagerFactoryBeandialect 选择也要注意Oracle 10g 项目不要随手换成 Oracle12cDialect分页 SQL 会从 rownum 改成FETCH FIRST旧库执行计划可能完全失效。3.2 声明式事务只读查询与写操作分开车辆管理的事务边界应该放在 Service 层而不是 Action 或 DAO。常见的 txAdvice 配置bean idtxManager classorg.springframework.orm.hibernate3.HibernateTransactionManager property namesessionFactory refsessionFactory/ /bean tx:advice idtxAdvice transaction-managertxManager tx:attributes tx:method namefind* read-onlytrue propagationREQUIRED/ tx:method nameget* read-onlytrue propagationREQUIRED/ tx:method namesave* propagationREQUIRED/ tx:method nameupdate* propagationREQUIRED/ tx:method namedelete* propagationREQUIRED/ /tx:attributes /tx:advice aop:config aop:pointcut idserviceTx expressionexecution(public * com.vehicle.service..*.*(..))/ aop:advisor advice-reftxAdvice pointcut-refserviceTx/ /aop:config这里read-onlytrue不只是语义标记它会让 Hibernate 在查询后跳过 dirty check少发一轮无意义的 update。命名规则不能任意改queryList、pageList开头的方法不会命中find*规则新建方法时最好沿用 find/get/save/update/delete 前缀。有一个和 Spring 代理相关的坑同在一个 Service 内部this.saveDispatch()调用同类里另一个带事务注解的方法事务不生效。因为 Spring AOP 生成的是代理对象事务控制在代理上内部this调用绕过了代理。接手旧代码时如果发现方法标了事务却不回滚先查是不是自调用。3.3 Spring 三级缓存、循环依赖与懒加载的三个伴生问题热词里“spring 三级缓存原理”正好对应这类老项目里一个高频话题。三级缓存解决的是单例 Bean 的循环依赖A 依赖 BB 又依赖 A且都用 setter 注入时Spring 在 A 创建后把早期引用暴露到第三级缓存B 创建时能拿到 A 的未完成实例。但构造器注入下的循环依赖无法通过三级缓存解决因为构造阶段根本拿不到可用的早期引用。SSH 老项目里 Service 相互调用很常见接手时先把构造器改成 setter 注入能少碰很多 BeanCurrentlyInCreationException。另一个是老项目几乎都会遇到的 LazyInitializationException列表页 JSP 里访问dispatch.vehicle.plateNo但 Hibernate Session 已经关了。常规做法是加 Spring 的 OpenSessionInViewFilter把 Session 生命周期延长到视图渲染结束filter filter-nameOpenSessionInViewFilter/filter-name filter-classorg.springframework.orm.hibernate3.support.OpenSessionInViewFilter/filter-class /filter filter-mapping filter-nameOpenSessionInViewFilter/filter-name url-pattern/*/url-pattern /filter-mapping这不是万能药它把数据库连接持有到 JSP 渲染完成页面越复杂连接占用越久。车管系统的导出功能如果也走同一线程连接可能在导出期间被占满所以导出接口单独开查询比硬扛 OSIV 更稳。4. Struts 2 的 Action 路由、Oracle 分页与组合条件查询4.1 struts.xml 与 Spring objectFactoryStruts 2 默认自己 new Action接入struts2-spring-plugin后可以把 Action 当作 Spring Bean 管理。典型入口配置struts constant namestruts.objectFactory valuespring/ package namevehicle extendsstruts-default action namevehicleList classvehicleAction result namesuccess/vehicle/list.jsp/result /action action namedispatchList classdispatchAction methodlist result namesuccess/vehicle/dispatch_list.jsp/result /action /package /strutsstruts.objectFactoryspring之后classvehicleAction对应的是 Spring 容器里的 bean id不再是全限定类名。此时 Spring 配置里的 Action 必须是 prototype 作用域bean idvehicleAction classcom.vehicle.web.action.VehicleAction scopeprototype/如果是单例Struts 2 的请求参数会被多线程共享用户 A 查车牌时可能读到用户 B 刚提交的车牌这是固定坑。Service 在 Action 里用 setter 注入就好public class VehicleAction extends ActionSupport { private VehicleService vehicleService; public void setVehicleService(VehicleService vehicleService) { this.vehicleService vehicleService; } public String list() { PageBeanVehicle page vehicleService.pageQuery(getPageNo(), getPageSize()); ActionContext.getContext().put(page, page); return SUCCESS; } }Action 里只做参数接收和结果放入真正的查询、事务留在 Service这个边界不要乱。4.2 Oracle 分页在 Hibernate 落地rownum、setFirstResult/setMaxResultsOracle 原生分页依赖 ROWNUM但没有 LIMIT 那么直接。最基础的分页写法是三层嵌套SELECT * FROM ( SELECT t.*, ROWNUM rn FROM (SELECT * FROM T_DISPATCH ORDER BY START_TIME DESC) t ) WHERE rn BETWEEN 11 AND 20;内层先排序中层生成连续行号外层截取目标页。直接写WHERE ROWNUM 10查不出任何行这是 Oracle 分页最容易踩的点。用 Hibernate 时不需要手写这段 SQLDialect 会帮你生成public PageBeanDispatch pageQuery(int pageNo, int pageSize) { int firstResult (pageNo - 1) * pageSize; Query q sessionFactory.getCurrentSession() .createQuery(from Dispatch d order by d.startTime desc); q.setFirstResult(firstResult); q.setMaxResults(pageSize); ListDispatch list q.list(); Long total (Long) sessionFactory.getCurrentSession() .createQuery(select count(d) from Dispatch d) .uniqueResult(); return new PageBean(list, total, pageNo, pageSize); }setFirstResult从 0 开始setMaxResults是每页条数。翻到第二页时firstResult就是 10对应 SQL 里的BETWEEN 11 AND 20。不同写法的差别如下写法实际结果WHERE ROWNUM 10查不到任何行WHERE ROWNUM 20只返回前 20 行三层嵌套后BETWEEN 11 AND 20正确返回第二页还需要注意排序的稳定性。如果 ORDER BY 字段在数据里有重复值Oracle 不保证每次返回顺序一致分页可能出现重复记录车管调度表里同一个时间点多次派车时尤其明显。处理办法是排序字段加上主键比如order by d.startTime desc, d.id desc。4.3 车牌、时间、司机的组合条件查询检索页面最常见的需求是“车牌号 日期 司机姓名”组合查询。用 DetachedCriteria 可以只拼有值的条件避免手写动态 SQL 的引号地狱DetachedCriteria dc DetachedCriteria.forClass(Dispatch.class, d); if (StringUtils.isNotBlank(plateNo)) { dc.createAlias(vehicle, v); dc.add(Restrictions.like(v.plateNo, plateNo.trim(), MatchMode.ANYWHERE)); } if (startDate ! null) { Date end DateUtils.nextDayStart(startDate); dc.add(Restrictions.ge(startTime, startDate)); dc.add(Restrictions.lt(startTime, end)); } if (StringUtils.isNotBlank(driverName)) { dc.createAlias(driver, dr); dc.add(Restrictions.eq(dr.name, driverName.trim())); } List list getHibernateTemplate().findByCriteria(dc, (pageNo - 1) * pageSize, pageSize);创建多个别名时别名不能重复且如果同一个关联被创建两次查询会做重复连接返回结果可能成倍增长。另一个容易被忽略的是 trim页面上是“ 京A12345 ” 带空格传到数据库会把索引也干扰掉。姓名查询建议用 eq 而不是全模糊因为传一个空字符串进去like 条件会退化成扫全表日期条件用加的半开区间才能包含当天最后一秒。5. 现场运维Oracle 监听、PL/SQL 远程连接与身份证文本导出5.1 监听器无法启动的三步定位车管应用突然连不上数据库时不要急着重启服务。先执行lsnrctl status lsnrctl start监听器无法启动通常不是服务本身坏了而是 listener.ora 里的 HOST 与实际机器名不一致。修改$ORACLE_HOME/network/admin/listener.ora把 HOST 改成机器真实 IP 或主机名然后重新lsnrctl stop、lsnrctl start。如果 Windows 服务启动失败但手工命令能成功多半是服务路径指向了旧 ORACLE_HOME重建监听服务比反复重启有效。还有一类是 1521 端口被占用用netstat -ano | findstr 1521确认有没有别的进程占用这种插了中间件之后最常见。5.2 PL/SQL Developer 连局域网另一台 Oracle接手车管项目时开发库通常不在本机。除了在登录窗口填主机名、端口、服务名之外最稳定的做法是配置 tnsnames.oraVEHICLE_ORCL (DESCRIPTION (ADDRESS (PROTOCOL TCP)(HOST 192.168.10.20)(PORT 1521)) (CONNECT_DATA (SERVER DEDICATED)(SERVICE_NAME orcl)) )报 ORA-12541 是监听器没起来或端口不通先回到 5.1报 ORA-12514 是 SERVICE_NAME 写错老库一般不是服务名而是 SID把SERVICE_NAME换成SID ORCL就能解决。连接能从本机、不能从局域网访问时检查服务器防火墙对 1521 的入站规则不要直接关防火墙。5.3 身份证号在导出里变成科学计数法Oracle 导出 CSV 后身份证号常被 Excel 显示成1.23457E17而且后三位直接丢成 0。根本原因是 Excel 把超过 15 位的数字列按数值处理。PL/SQL Developer 里导出前把查询先转成文本SELECT TO_CHAR(ID_CARD_NO) AS ID_CARD FROM T_DRIVER;如果导出的 CSV 仍然被识别成数值就在 SQL 里给值加双引号让 Excel 按文本解析SELECT || ID_CARD_NO || AS ID_CARD FROM T_DRIVER;更稳的做法是直接导 XLSX再用 Excel 把该列格式设为“文本”这样身份证不会触发科学计数法。运维人员长期维护报表时尽量把身份证、车牌这类标识列和金额、里程数值列分开两个 Sheet 导出。本文还有配套的精品资源点击获取
返回列表