尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
JDBC实训报告怎么写?从工程化思维到数据访问层设计要点
开头每年实训季我都会看到大量JDBC数据库编程的实训报告大部分翻来覆去就是贴代码、放截图本质上是一份代码说明书而不是实训报告。JDBC这个主题实训真正要考察的其实是三件事对JDBC API执行流程的理解、对数据库交互工程结构的拆分能力、对资源管理和异常处理的基本功。我在辅导实训时反复强调一句话JDBC并不难难的是用工程化的思维去写JDBC代码。这篇文章把我带实训时梳理的完整思路放出来讲清楚JDBC从连接建立到结果处理的底层逻辑、一个合格的数据访问层应该怎么拆分、实训报告里哪些内容才是加分项。无论你是正在准备Java实训报告的学生还是刚入行想把数据访问这块基础补扎实的开发者这篇都值得读一读。1. 实训报告不是代码清单而是工程方案说明1.1 一篇合格的实训报告要回答哪四个问题很多同学写JDBC实训报告习惯从DriverManager开始贴代码一直贴到关闭连接再加个运行截图就收工。这种写法在课程设计里能及格但离“合格”还有很大距离。我评审实训报告时会重点看四个问题有没有交代清楚第一这个实训到底要解决什么问题业务背景是什么第二用的什么数据库、什么驱动、什么连接方式为什么这样选第三数据访问层的结构是怎么设计的每个类和接口承担的职责是什么第四异常和资源是怎么处理的出了故障怎么排查。如果你把实训报告当作一份工程方案说明来写而不是代码清单思路就会清晰很多。JDBC实训的本质是用Java语言通过标准API去操作关系型数据库完成增删改查这一整套流程。但标准API只是最底层的地基真正值钱的是你在这上面搭建的工程结构——连接怎么管理、SQL怎么组织、结果怎么转换、异常怎么兜底。这些才是实训报告应该重点展开的内容。1.2 环境选型和版本匹配环境部分每个人都写但很少有人写清楚为什么是这个版本。实训报告里建议把环境信息用表格列出来这样导师一眼就能看清你的运行环境组件推荐版本说明JDK1.8大多数实训环境的老版本语法兼容性好MySQL5.7或8.08.0以上时区处理方式有变化需要注意驱动类型mysql-connector-java 8.0.x8.x的驱动类名和URL参数与5.x不同IDEIDEA或Eclipse关键在配置项目依赖的方式管理工具Navicat / DataGrip / 命令行用于建库建表验证SQL的正确性版本匹配是特别容易踩坑的地方。MySQL 5.x用的驱动类是com.mysql.jdbc.Driver8.x改成了com.mysql.cj.jdbc.Driver。URL参数也不一样8.x必须带serverTimezone否则会出现时区报错。很多实训项目的报错根因就是驱动版本和数据库版本对不上。我建议实训环境统一采用MySQL 8.0 mysql-connector-java 8.0.33这套组合相对来说最稳定。如果你所在实训机房装的是MySQL 5.7那驱动就用5.1.49不要把高版本驱动硬塞到低版本数据库上反之亦然。2. JDBC执行流程拆解从驱动加载到结果集遍历2.1 驱动加载的底层逻辑JDBC的第一步是驱动加载通常写Class.forName(com.mysql.cj.jdbc.Driver)。很多同学只是背了这行代码完全不知道它在干什么也不知道从JDBC 4.0开始这行代码其实可以省略。Class.forName的作用是让类加载器把Driver类加载进JVM触发类中的静态代码块执行。MySQL的Driver类里有一个静态块会主动创建一个Driver实例并且调用DriverManager.registerDriver把这个实例注册到DriverManager的驱动列表中。DriverManager维护一个CopyOnWriteArrayList存放所有注册进来的驱动后面调用getConnection时它就遍历这个列表让每个驱动尝试去解析URL谁能解析谁就负责建立连接。这里有一个非常有用的机制URL解析。DriverManager会根据jdbc:mysql前缀来匹配驱动如果你引入多个数据库的驱动比如MySQL、Oracle、PostgreSQL的jar包都在classpath里它也是靠这个前缀找到正确驱动的。所以URL不是随便写的前缀就是驱动注册时的匹配密钥。从JDBC 4.0开始只要我们通过Maven或手动引入驱动jar包并且保证它在classpath中ServiceLoader机制会自动加载META-INF/services/java.sql.Driver文件里声明的驱动类Class.forName这行代码可以省略。但实训报告里我仍然建议保留原因有两个一是显式加载能让人一眼看懂你用的是哪种数据库二是兼容老版本JDK环境毕竟实训机房的JDK版本不一定由你说了算。// 显式加载驱动让连接过程意图更清晰 Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; String username root; String password 123456; Connection conn DriverManager.getConnection(url, username, password);2.2 Connection、Statement、ResultSet三者如何协同JDBC的核心执行链条就三步Connection负责创建会话Statement负责承载SQLResultSet负责接收查询结果。Connection是重量级资源底层对应一条数据库的物理网络连接创建它需要经过TCP握手、身份认证、权限校验开销很大。这也是为什么高并发场景必须用连接池而不是每操作一次就新建一条连接。Statement有两个子接口需要区分PreparedStatement和CallableStatement。实训中绝大多数场景用PreparedStatement。它有两个关键能力第一是预编译SQL骨架先发给数据库编译后面只传参数重复执行时性能更好第二是参数化查询用问号占位符代替字符串拼接从根上杜绝SQL注入。很多同学疑惑为什么PreparedStatement能防SQL注入我给你拆开看。如果用字符串拼接String sql SELECT * FROM user WHERE name input ;用户输入admin OR 11拼出来后就是SELECT * FROM user WHERE name admin OR 11条件恒成立整张表都被查出来了这就是SQL注入。而PreparedStatement是先用问号占位String sql SELECT * FROM user WHERE name ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, input);SQL骨架发送给数据库后参数值通过二进制协议单独传输数据库把输入当作纯数据而不是SQL片段处理所以不管用户输入什么都不可能改变SQL的执行语义。ResultSet是结果集它本质上是一个指向数据行的游标。刚查询完游标指向第一行的前一行调用next()方法才会把游标移到第一行返回boolean表示是否还有下一行。用getString(columnName)或getInt(1)可以取出当前行某一列的数据既有列名方式也有索引方式。列名方式可读性更好索引方式性能略好一点点实训代码里推荐列名方式别人看代码的时候不用去数第几列是什么字段。// 标准查询流程Connection → PreparedStatement → ResultSet String sql SELECT id, name, age FROM student WHERE age ?; try (Connection conn DriverManager.getConnection(url, username, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, 18); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { int id rs.getInt(id); String name rs.getString(name); int age rs.getInt(age); System.out.println(id id , name name , age age); } } } catch (SQLException e) { e.printStackTrace(); }2.3 executeQuery和executeUpdate的适用边界Statement系列有两个执行方法很容易混淆executeQuery和executeUpdate。executeQuery专门执行SELECT语句返回值是ResultSetexecuteUpdate执行INSERT、UPDATE、DELETE以及DDL语句返回值是int代表受影响的行数。有个边界情况如果SQL是CREATE TABLE这类DDL语句executeUpdate也会返回0是“正常影响0行”和 UPDATE 操作WHERE条件未命中影响0行在返回值上是一样的。所以要判断操作是否成功不能只看返回0就觉得失败更稳妥的方式是结合逻辑判断或检查数据库的实际状态。还有个冷门但实用的方法execute它可以执行任意SQL返回boolean表示是否是ResultSet。如果返回true就用getResultSet拿结果集如果是false就用getUpdateCount拿影响行数。这个方法在写通用SQL执行工具类时会用到实训里不需要深挖但知道它存在就够了。3. 实训项目实战用DAO模式写一个不烧脑的数据访问层3.1 建表、Javabean、DAO三者的对应关系实训项目需要一个具体场景我推荐用“学生信息管理”作为载体它足够简单又覆盖了完整的CRUD操作。第一步是建表CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_db; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT NOT NULL, gender VARCHAR(10) DEFAULT 男, major VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表语言里不建议写任何编码格式为utf8mb4的字段数据库整体字符集在库级别设置就好了。有个细节MySQL的utf8字符集最多3字节存不了emoji和一些生僻字utf8mb4才是真正的4字节UTF-8。实训里虽然存emoji的概率不高但库和表统一用utf8mb4配合连接参数characterEncodingutf8基本能规避所有中文乱码问题。Javabean是与student表字段一一对应的Java类属性名尽量与数据库列名保持一致public class Student { private Integer id; private String name; private Integer age; private String gender; private String major; private Date createTime; // 无参构造、全参构造 // getter、setter方法 // toString方法 }DAO即Data Access Object是隔离业务逻辑与数据库访问的中间层。它的价值在于业务层只需要面对DAO接口不关心SQL怎么写、数据库连接怎么管理。以后换数据库或者修改表结构只改DAO实现类业务层代码不用动。接口定义五个核心方法覆盖增删改查和列表查询public interface StudentDao { int insert(Student student); // 新增 int deleteById(Integer id); // 按ID删除 int update(Student student); // 更新 Student selectById(Integer id); // 按ID查询 ListStudent selectAll(); // 查询所有 }3.2 DAO实现类中的SQL与参数绑定细节DAO实现类一方面要完成SQL执行另一方面还要实现ResultSet到Javabean的映射。这个映射过程在MyBatis里叫ORM在JDBC原生环境里只能你自己写。写多了之后你会发现这是整个JDBC编程中最枯燥但最核心的部分。public class StudentDaoImpl implements StudentDao { private static final String URL jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USERNAME root; private static final String PASSWORD 123456; Override public int insert(Student student) { String sql INSERT INTO student(name, age, gender, major) VALUES(?, ?, ?, ?); try (Connection conn DriverManager.getConnection(URL, USERNAME, PASSWORD); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, student.getName()); ps.setInt(2, student.getAge()); ps.setString(3, student.getGender()); ps.setString(4, student.getMajor()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(新增学生数据失败, e); } } Override public Student selectById(Integer id) { String sql SELECT id, name, age, gender, major, create_time FROM student WHERE id ?; try (Connection conn DriverManager.getConnection(URL, USERNAME, PASSWORD); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Student student new Student(); student.setId(rs.getInt(id)); student.setName(rs.getString(name)); student.setAge(rs.getInt(age)); student.setGender(rs.getString(gender)); student.setMajor(rs.getString(major)); student.setCreateTime(rs.getTimestamp(create_time)); return student; } } return null; } catch (SQLException e) { throw new RuntimeException(按ID查询学生数据失败, e); } } }这里有个参数绑定的细节setString、setInt是根据Java类型选择的方法数据库端的类型转换由驱动完成。比如Java的String映射到MySQL的VARCHAR或TEXT都能处理Java的int映射到INT或BIGINT也可以。但需要注意setTimestamp对应MySQL的DATETIME/TIMESTAMP如果Javabean用java.util.Date在传给setTimestamp时要先转成java.sql.Timestamp这是新手最容易忽略的。createTime字段在数据库里是DATETIME同时有DEFAULT CURRENT_TIMESTAMP。insert时我们没有手动设置它数据库会自动填当前时间。selectById中返回时用rs.getTimestamp(create_time)取出java.sql.Timestamp它是java.util.Date的子类所以直接setCreateTime没问题。3.3 资源释放为什么建议用try-with-resourcesJDBC编程有一条铁律Connection、Statement、ResultSet用完后必须关闭。原因很实在Connection是网络连接Statement和ResultSet在数据库端也有对应的资源占用不释放的话轻则连接数打满重则数据库直接被拖垮。传统写法是在finally块里逐个关闭判断是否为null还要处理close方法自身的异常。代码难看且容易漏。从JDK 7开始try-with-resources语法可以自动关闭实现了AutoCloseable接口的资源代码干净很多。// 传统写法finally块里关闭资源 Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DriverManager.getConnection(URL, USERNAME, PASSWORD); ps conn.prepareStatement(sql); rs ps.executeQuery(); // 处理结果集 } catch (SQLException e) { e.printStackTrace(); } finally { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } }finally块里三个try-catch嵌套占了将近十行。而try-with-resources写法把资源的声明放在try后面的括号里方法结束时自动逆序关闭也就是先关ResultSet再关Statement最后关Connectiontry (Connection conn DriverManager.getConnection(URL, USERNAME, PASSWORD); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { // 处理结果集 } catch (SQLException e) { e.printStackTrace(); }还有一层隐藏的逻辑如果你手动关闭了Connection它底层的Statement和ResultSet也会被级联关闭。但依赖这种级联是很危险的习惯因为一旦使用连接池Connection.close()不真正关闭物理连接而是归还给连接池此时Statement和ResultSet如果不显式关闭就会一直在数据库端残留。连接池环境下“只关Connection就够”的偷懒做法绝对是坑。实训报告里我建议专门用一个小节来写资源释放策略这比多写十个CRUD方法更让老师认可。4. 实训里的“加分项”SQL注入防护、连接池与事务边界4.1 参数化查询如何把SQL注入扼杀在摇篮里SQL注入是老生常谈但每次实训里总有同学用字符串拼接而且觉得自己写得挺好的。这里我强烈建议在实训报告里把防SQL注入这个点明确写出来体现你不仅会写代码还有安全意识。参数化查询的原理前面已经提过SQL骨架与参数分离传输参数永远作为数据处理不会参与SQL解析。只要SQL中使用?占位符并且通过setXxx方法绑定参数就不存在注入的可能。还要注意一个细节Order by子句后面的部分和表名字段名这类结构性的元素不能用问号占位符。因为预编译是把值作为参数绑定而表名、列名、排序方式属于SQL结构的一部分参数化机制不覆盖这些位置。如果需要动态传入排序字段必须用白名单校验// 不安全的写法 String sql SELECT * FROM student ORDER BY orderBy; // 安全的做法对排序字段做白名单校验 String orderBy age; if (!Arrays.asList(id, name, age, create_time).contains(orderBy)) { throw new IllegalArgumentException(非法排序字段); } String sql SELECT * FROM student ORDER BY orderBy;4.2 为什么实训中要主动引入Druid连接池实训报告里如果出现了连接池已经算是一个亮点了。很多同学会问实训项目的并发量又不高学连接池有必要吗我的回答是有必要。因为连接池不仅仅是性能优化手段更是一种资源管理思想。驱动自带的DriverManager每次getConnection都新建物理连接在高并发下性能极差而连接池预先创建一批连接放在池里需要时直接取用完归还省去重复建连和断连的开销。Druid是Java生态中用得非常广泛的连接池功能全面自带监控页面。实训里用Druid意义在于让你提前接触生产级的连接管理方式而且配置非常简单。你需要引入druid的jar包然后写一个工具类public class JdbcUtils { private static DruidDataSource dataSource; static { try { dataSource new DruidDataSource(); dataSource.setUrl(jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMinIdle(5); // 最小空闲 dataSource.setMaxActive(20); // 最大活跃连接数 dataSource.setMaxWait(10000); // 获取连接的最大等待时间毫秒 } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }代码里两个地方最容易出错。一是setUrl时不带参数后续中文乱码或者时区不对又要回头改二是maxWait如果设成0表示无限等待在数据库连接池耗尽时会导致线程一直卡死生产环境一般不推荐。实训项目设成10000毫秒超过等待时间直接抛异常问题暴露得反而快。4.3 事务的ACID在JDBC里怎么落地事务是数据库编程绕不开的话题。JDBC中Connection默认是自动提交模式也就是autocommit为true每执行一条DML自动提交。如果要让多条SQL组成一个原子操作必须手动关闭自动提交执行完成后提交或回滚。经典的实训案例是转账操作扣钱和加钱是两条UPDATE必须保证同时成功或同时失败。如果第一条执行成功、第二条执行失败不回滚的话钱就凭空消失了。Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 扣款 String sql1 UPDATE account SET balance balance - 100 WHERE id 1; try (PreparedStatement ps1 conn.prepareStatement(sql1)) { ps1.executeUpdate(); } // 入账 String sql2 UPDATE account SET balance balance 100 WHERE id 2; try (PreparedStatement ps2 conn.prepareStatement(sql2)) { ps2.executeUpdate(); } conn.commit(); // 全部成功提交事务 } catch (SQLException e) { try { if (conn ! null) conn.rollback(); // 任一失败回滚事务 } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(转账失败, e); } finally { try { if (conn ! null) { conn.setAutoCommit(true); // 恢复自动提交避免影响连接池下一次使用 conn.close(); } } catch (SQLException e) { e.printStackTrace(); } }事务的边界往往是从业务层决定的。什么时候开始事务什么时候提交需要根据业务操作涉及的SQL范围来确定。有一个常见错误把事务控制的代码写在DAO层单个方法内部但一次业务操作横跨多个DAO方法导致事务被切碎无法保证整体原子性。正确的做法是事务放在Service层开启DAO层只负责SQL执行。实训报告里如果能写到这一层抽象说明你真的理解了事务的本质。5. 实训中必然踩到的坑与排查链路5.1 ClassNotFoundException和SQLException两种异常两套排查思路JDBC编程最常见的异常是ClassNotFoundException和SQLException但它们的排查方向完全不同。ClassNotFoundException是Java层面的问题说明JVM在classpath里找不到驱动类。可能的原因jar包没有导入、jar包导入失败、jar包版本不对导致包路径不存在。排查链路是先检查项目依赖中是否存在mysql-connector-java再检查驱动类名是否正确MySQL 5.x是com.mysql.jdbc.Driver8.x是com.mysql.cj.jdbc.Driver八点零以后老类名仍然保留但会打印警告偶尔还会遇到新驱动彻底移除老类名的情况所以最好统一用新类名。SQLException是数据库层面的通讯问题具体原因千奇百怪。最常见的几个是错误提示根因解决方案Access denied for user rootlocalhost用户名或密码错误核对连接参数MySQL 8默认认证插件是caching_sha2_password驱动必须8.xUnknown host主机地址无法解析检查localhost或者IP是否正确Communications link failure数据库服务未启动或IP端口不可达先用命令行或Navicat测试能否正常连接Server returns invalid timezone时区参数缺失URL加serverTimezoneAsia/Shanghaisql query is not allowed在只读连接上执行写入操作检查账号权限或连接参数中的readOnly设置5.2 中文乱码多级都可能是元凶中文乱码问题涉及四级环节数据库表字符集、连接参数字符集、IDE文件编码、控制台输出编码。任何一个不一致都会导致乱码。排查链路要从下往上逐层确认。先检查数据库表字符集执行SHOW CREATE TABLE student确认CHARSET是utf8mb4。再检查连接参数里的characterEncodingutf8。接着检查IDE的项目编码IDEA在Settings Editor File Encodings里全局和项目都设为UTF-8。最后如果控制台打印中文乱码但数据库里正常那是控制台编码问题IDEA的Help Edit Custom VM Options里加-Dfile.encodingUTF-8重启生效。最常见的场景是数据库里中文正常Java代码里中文常量也正常就是查询结果打印出来乱码。这种基本就是连接参数没带characterEncodingutf8或者带了但数据库表本身是latin1字符集。5.3 连接泄漏与连接池耗尽写代码时看不见跑起来要命连接泄漏是JDBC内存管理里最隐蔽的坑。表现是单次执行没问题程序跑一会儿就报“连接数不足”或“连接池超时”。原因是某处的Connection获取了没关闭连接被永久占用。在没有连接池的年代Connection不关就是socket不释放程序跑久了系统文件句柄耗尽。有了连接池之后Connection不close只是归还失败池里的连接被占着不放最终池耗尽。排查方法很朴素如果你的项目里用了Druid开启监控页面就能看到活跃连接数持续不降就是泄漏了。不用连接池的话可以在finally块里打日志观察close方法是否被执行。最直接的自查方式是全项目搜索new Connection或者getConnection然后逐个确认是否都在try-with-resources或finally中关闭了。这里我还会专门提醒一个点如果写了like查询通配符要在参数里拼而不是直接拼进SQL。比如查询所有姓张的同学// 推荐写法 String sql SELECT * FROM student WHERE name LIKE ?; ps.setString(1, 张%); // 而不是 String sql SELECT * FROM student WHERE name LIKE 张%; ps.setString(1, name); // 错误参数被忽略或拼接成二维SQL这个细节在实训答辩中很容易被老师追问提前掌握能让你省去很多尴尬。5.4 流式查询大结果集下的内存保护机制实训里查询所有学生可能只有几十条数据感受不出问题。但如果你以后工作中用JDBC查询几百万行数据默认方式下ResultSet会一次性把所有结果加载到JVM堆内存内存直接被打爆是常有的事。流式查询是一种边读边取的方式设置fetchSize并让连接不自动提交时MySQL的驱动会一条一条或按批次从数据库取数据而不是一次性加载全部结果。用法Connection conn JdbcUtils.getConnection(); conn.setAutoCommit(false); // 流式查询要求关闭自动提交 String sql SELECT * FROM big_table; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setFetchSize(Integer.MIN_VALUE); // MySQL驱动流式查询的触发条件 try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 逐行处理内存占用稳定 } } }这个点实训不需要做但如果报告里能作为扩展知识点提到再配合连接池的讲解整个实训报告的深度就会拉开一个档次。6. 实训报告的写作结构照着搭就能拿高分6.1 报告各章节怎么安排实训报告的评分标准虽然各校不同但核心逻辑是相通的就是要看你是否真正理解了技术方案。我建议按下面这个结构组织内容章节核心内容篇幅建议引言项目背景、实训目标、技术选型概述500字左右需求分析要实现哪些功能、业务流程是什么400字左右数据库设计建表语句、ER图、表字段说明500字左右系统设计分层结构、类图、各层的职责800字左右核心代码实现CRUD、DAO、工具类的关键代码不要全文贴1500字左右问题与解决方案遇到的坑、排查过程、最终解决方式800字左右总结实训收获、不足与改进方向400字左右核心代码实现这一段只贴关键代码和注释不要整个项目源码都塞进去。每一段代码前面写清楚这段代码要解决什么问题后面写清楚运行效果。我评审报告时最反感的就是大段大段贴代码却不加解释。6.2 报告里哪些内容能体现“我不仅会写还懂原理”实训报告想拿高分光是写对还不够要写出“为什么是这样”的理解。我总结几个很有效的加分位第一在数据库设计部分主动说明为什么把create_time字段设成DEFAULT CURRENT_TIMESTAMP减少应用层的一次赋值操作。第二在DAO设计部分说明为什么用接口来定义数据访问契约这样未来可以从StudentDaoImpl替换成MyBatis或JPA实现而业务层不用变化。第三在异常处理部分说明为什么SQLException运行时需要被包装成RuntimeException抛出因为受检异常会让业务层代码被try-catch淹没而业务层不应该处理数据库层面的异常。第四也是最容易被忽略的放一个“项目目录结构”说明展示你的代码是如何分包的entity包放Javabeandao包放接口dao.impl包放实现util包放工具类。一个清晰的包结构比你在文档里写一百遍“我遵循分层设计”都有说服力。7. 实训做完之后JDBC的学习还差哪一步实训报告交上去不代表这轮学习就结束了。JDBC是理解Java操作数据库的底层通道在JDBC基础上再往上走你会接触到连接池的更多实现、MyBatis这种ORM框架以及Spring管理事务的方式。但不管框架怎么封装最终落到数据库操作还是走Connection、PreparedStatement、ResultSet这套核心链路。如果你学有余力我推荐实训后做两件事一是把项目中的DAO层换成MyBatis实现对比一下CRUD代码量从几十行降到几个注解之后的差异感受框架解决的是哪一部分重复劳动二是把连接池从Druid换成HikariCP对比两者的配置参数差异理解为什么HikariCP被Spring Boot选为默认连接池。理解了差异才算真正理解了数据库访问这一层的设计逻辑。这次实训做到最后我自己最有感触的一个点是JDBC写的CRUD本身并不复杂复杂的是资源管理、事务边界和异常处理这些“看不见”的部分。实训的价值不在于完成一个可以运行的小项目而在于让你第一次认真面对这些问题。把这些基础打扎实了后面学MyBatis、学Spring的声明式事务都会轻松不少。
RELATED

相关推荐

智能体开发实战:解耦Agent、Tool与LLM的工程方法论

智能体开发实战:解耦Agent、Tool与LLM的工程方法论

1. 这不是“又一个AI教程”,而是2026年真实工程现场的智能体开发切片你点开这个标题,大概率是被“薪资翻倍”“最全最细”“手把手”这些词勾住的。但我想先说句实话:过去两年我带过17个从零起步做智能体的工程师,其中12个在第三周…

📅 2026/9/29 17:00:21
FDE工程师实战指南:从需求翻译到企业级交付的完整工作流

FDE工程师实战指南:从需求翻译到企业级交付的完整工作流

1. 这不是“速成课”,而是一份FDE工程师真实工作流的完整切片 如果你在B站搜“FDE教程”,大概率会看到两类内容:一类是3分钟讲完“什么是FDE”,配个PPT截图就叫“企业级”;另一类是直接甩出一堆Agent框架代码&#xff…

📅 2026/9/29 17:00:21
告别Makefile:用Ceedling打造嵌入式C单元测试自动化流水线

告别Makefile:用Ceedling打造嵌入式C单元测试自动化流水线

上个月给一套电机控制模块补单元测试,我在Makefile里加了第四个源文件路径,链接器立刻开始报一堆undefined reference。排查了快二十分钟,最后发现是模块间依赖顺序写错了。这已经不是第一次了:每加一个测试文件,得手动…

📅 2026/9/29 17:00:21
MORE NEWS

更多资讯

📰

DuoPlus更新解析:代理批量检测与RPA自动化如何赋能多账号运营

这款工具把"多账号隔离"和"RPA自动化"焊在同一个工作流里,是我拿到更新日志后最直观的感受。过去做批量登录、批量采集,代理和自动化脚本是两套系统,代理挂了你根本不知道,脚本跑了一小时全在做无用功。这次D…

📰

MCP协议实战指南:从原理到MCP Server开发与踩坑

1. 从一个让人抓狂的对接场景说起如果你最近在开发者社区里晃悠,大概率会反复撞见三个字母:MCP。有人把它比作"AI 界的 USB-C",有人说它是"Agent 时代的 HTTP 协议",还有人干脆在群里甩一句"不懂 MCP 就…

📰

LS-DYNA弹丸穿孔仿真:Lagrange网格与SPH粒子混合建模实战

弹丸穿孔这类工况,算起来一直是最折腾人的。最早我用纯Lagrange网格算12.7mm穿甲弹打钢板,前几十微秒还算正常,弹体一进靶板,网格就开始肉眼可见地畸变,最后直接负体积计算中止。后来换纯SPH粒子,计算量大不…

📰

React Native适配OpenHarmony实战:随机推荐页面的开发与踩坑

如果你在一个小团队里同时维护三端应用,最近又接到了“把 App 搬到 OpenHarmony 上”的需求,大概率会和我一样,先盯着 React Native 的版本号发呆好一阵。我在 AnimeHub 这个追番社区项目里负责随机推荐页面的开发,表面上看&#…

📰

SPH与Lagrange混合建模破解穿孔仿真单元畸变难题

我刚接到这个模拟任务的时候,第一版模型用的是纯Lagrange网格,弹丸和靶板全部划分六面体单元。前几十微秒跑得挺正常,弹丸头部刚压到靶板表面,靶板迎弹面单元就开始剧烈畸变,紧接着就报出negative volume,计…

📰

Java 8 Stream流式编程:从集合处理到函数式思维的实战指南

1. 流式编程的本质与设计思路 1.1 Java 8之前,我们是怎么写集合代码的 在Java 8正式把Stream推上台面之前,大部分Java开发者处理集合数据的姿势,就是for循环加if判断,一层套一层。比如要统计一个订单列表里每个品类的销售总额&am…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬