
1. 从一张白纸到能跑通的库这个Demo到底在练什么很多刚接触JavaEE的朋友学到JDBC那一章就开始发懵——书上的例子东一榔头西一棒子建表语句给一半连接代码给一半跑起来不是报ClassNotFoundException就是Access denied。我当年也是这样对着屏幕折腾了一下午最后发现是驱动包压根没放进WEB-INF/lib。所以这个“小Demo——数据库建立”看着不起眼实际上它是整个JavaEE学习路线上第一个真正意义上的“闭环”从设计表结构、写建库脚本、配连接参数到用一段最小可运行的代码验证“我的程序真的能跟数据库说上话”。这件事做通了后面的DAO、连接池、事务才有落脚点做不通后面全是空中楼阁。这篇文章面向的是刚上手JavaEE、手里有一个能跑起来的Tomcat或普通Java工程、但还没独立建过一个业务库的读者。我会拿一个最典型的场景来贯穿全文——一个简易的“学生信息管理”小Demo包含学生表和班级表两张核心表。别小看这两张表主外键、索引、字符集、自增主键这些基础概念全都能覆盖到。我会把建库建表的完整SQL、连接参数的每一项含义、驱动加载的两种写法、以及我实际踩过的坑一条一条摊开讲。你照着做大概率能一次跑通跑不通第四节的排查表基本能兜住你。需要先说明一点本文所有表名、库名、示例数据都是虚构的你实际做的时候按自己的业务改就行重点是理解每一步“为什么这么干”而不是死记某条语句。数据库我以MySQL 8.x为主来讲因为这是目前教学和中小项目里最常见的组合其他数据库的差异我会在关键处点出来。2. 建库之前的功课表结构设计与脚本组织2.1 先想清楚“存什么”再动手写CREATE我见过太多新手一上来就CREATE DATABASE然后边写代码边ALTER TABLE加字段最后表结构乱得像一锅粥。正确的顺序是反过来的先在纸上或者任何文本编辑器里把实体和关系画清楚。拿学生信息这个Demo来说核心实体就两个——学生和班级。一个学生属于一个班级一个班级有多个学生这是一对多关系。那么学生表里放一个class_id作为外键指向班级表的主键这是最朴素也最够用的设计。字段类型的选择有几个容易翻车的点我逐个说。学生姓名用VARCHAR(50)足够别用CHARCHAR是定长存“张三”这种两个字的名字会补空格取出来比较的时候容易出幺蛾子。学号这种固定格式的标识如果确定永远是10位数字用CHAR(10)反而更合适定长查询效率略好但如果你不确定长度就别逞强。年龄用TINYINT范围-128到127存人的年龄绰绰有余比INT省空间。入学日期用DATE别用VARCHAR存字符串否则后面按日期范围查询你会哭。性别这种只有几个固定值的用TINYINT存0/1/2或者用ENUM都行我倾向于TINYINT加注释因为ENUM后期加选项要改表结构不够灵活。主键我统一用BIGINT AUTO_INCREMENT也就是自增长整型。为什么不用业务字段当主键因为学号可能会变比如录入错误要改而主键一旦被外键引用就不能随便动。自增主键的好处是插入时不用管它数据库自己分配简单可靠。代价是分布式场景下会有冲突风险但这个Demo是单库单表完全不用考虑那一步。2.2 建库建表脚本的完整写法与逐行注释下面是我实际用的建库脚本你可以直接复制到Navicat、DBeaver或者命令行的mysql客户端里执行。我习惯把库名、字符集、排序规则一次性定死避免后面出现中文乱码。-- 建库字符集用utf8mb4排序规则用通用ci CREATE DATABASE IF NOT EXISTS demo_school DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE demo_school; -- 班级表先建被引用的一方 CREATE TABLE t_class ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 班级主键, class_name VARCHAR(50) NOT NULL COMMENT 班级名称, grade_year SMALLINT NOT NULL COMMENT 入学年份, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_class_name (class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT班级信息表; -- 学生表再建引用的一方 CREATE TABLE t_student ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 学生主键, student_no CHAR(10) NOT NULL COMMENT 学号固定10位, student_name VARCHAR(50) NOT NULL COMMENT 姓名, age TINYINT UNSIGNED DEFAULT NULL COMMENT 年龄, gender TINYINT NOT NULL DEFAULT 0 COMMENT 性别0未知 1男 2女, class_id BIGINT NOT NULL COMMENT 所属班级ID, enroll_date DATE DEFAULT NULL COMMENT 入学日期, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no), KEY idx_class_id (class_id), CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES t_class (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表;逐行说几个关键决策。字符集选utf8mb4而不是utf8是因为MySQL里的utf8其实是阉割版最多存3字节存不了某些特殊字符和emojiutf8mb4才是真正的完整UTF-8。排序规则utf8mb4_general_ci里的ci是case insensitive比较时不区分大小写对姓名、班级名这类字段够用如果你对排序精度有要求可以换utf8mb4_0900_ai_ciMySQL 8默认但教学Demo用general就挺好。存储引擎必须用InnoDB因为只有它支持外键和事务。MySQL 5.5之后默认就是InnoDB了但显式写出来是个好习惯免得在别人的老环境里跑出问题。外键约束fk_student_class的作用是往学生表插数据时class_id必须在班级表里存在否则报错。这能防止出现“孤儿学生”。但要注意外键会让删除班级变得麻烦——得先删该班级下的学生或者用ON DELETE CASCADE级联删除。教学Demo里我建议先不加级联让你亲手体会一下约束的存在感。idx_class_id这个普通索引是给外键字段加的。MySQL在创建外键时会自动给引用列建索引但显式写出来意图更清晰而且有些版本的行为不完全一致自己加上更保险。uk_student_no唯一索引保证学号不重复这是业务规则在数据库层的落地。2.3 初始化数据的插入顺序有讲究建完表插数据顺序不能乱先插班级再插学生。因为学生表的class_id依赖班级表的主键。如果你先插学生外键约束会直接把你拦下来。INSERT INTO t_class (class_name, grade_year) VALUES (软件一班, 2023), (软件二班, 2023), (网络一班, 2022); INSERT INTO t_student (student_no, student_name, age, gender, class_id, enroll_date) VALUES (2023000001, 张小明, 20, 1, 1, 2023-09-01), (2023000002, 李小红, 19, 2, 1, 2023-09-01), (2023000003, 王大力, 21, 1, 2, 2023-09-01);这里有个小技巧插入班级时我没写id让自增分配所以软件一班的id是1软件二班是2。学生数据里class_id写1和2就对应上了。如果你中途删过数据再插自增ID可能不连续这时候别硬编码class_id先SELECT id FROM t_class WHERE class_name软件一班查出来再用。我见过有人删了又插结果class_id对不上查出来的学生全挂到错误的班级下排查半天才发现是自增ID跳号了。3. 让Java程序连上库驱动、URL与连接代码3.1 驱动包怎么放、放哪里这一步错后面全白搭Java连MySQL靠的是JDBC驱动本质是一个jar包里面实现了JDBC规范定义的接口。MySQL 8.x对应的驱动是mysql-connector-j-8.x.x.jar老版本叫mysql-connector-java。这个包必须出现在你程序的类路径classpath里否则运行时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。普通Java工程非Maven的话把jar包放到项目根目录下的lib文件夹然后在IDE里右键jar包选“Add as Library”。如果是Web工程跑在Tomcat里jar包要放到WEB-INF/lib目录下而不是项目的普通lib目录——因为Tomcat有自己的类加载器体系放错位置容器启动时找不到驱动。这个坑我踩过不止一次明明IDE里代码不报红一部署到Tomcat就报驱动找不到折腾半天才发现是jar包位置的问题。用Maven的话就省心了在pom.xml里加依赖dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.3.0/version /dependency版本号选和你数据库大版本匹配的即可8.x的驱动连8.x的库没问题连5.7的库也基本兼容但反过来5.x的驱动连8.x的库会报认证插件错误因为8.x默认用caching_sha2_password老驱动不认。所以原则是驱动版本不低于数据库大版本。3.2 JDBC URL的每个参数都值得掰开看连接字符串长这样jdbc:mysql://localhost:3306/demo_school?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue我逐个解释因为这里每个参数都对应一个真实的坑。jdbc:mysql://是协议前缀固定写法。localhost:3306是主机和端口本机默认3306如果你改过端口就换成实际的。demo_school是库名必须和你建库时的名字一致。useUnicodetruecharacterEncodingutf8这一对是告诉驱动用UTF-8编码传输数据防止中文乱码。虽然8.x驱动默认就是UTF-8但显式写上不亏尤其是连老库的时候。useSSLfalse是关闭SSL加密连接本地开发环境没必要开开了反而可能因为证书问题连不上生产环境该开还是要开但那是后话。serverTimezoneAsia/Shanghai这个参数在8.x驱动里几乎是必填的因为驱动需要知道数据库服务器所在的时区来正确转换时间类型不写的话可能报The server time zone value ... is unrecognized或者存进去的时间差8小时。allowPublicKeyRetrievaltrue是配合8.x的caching_sha2_password认证插件用的不写在某些环境下会报公钥检索失败。提示URL里的在XML配置文件比如某些框架的配置里要写成amp;在Java字符串里直接写就行。这个细节坑过不少人配置文件里写会导致解析错误。3.3 一段最小可运行的连接验证代码别急着写DAO先用一段最朴素的代码验证“能连上”。这段代码的作用相当于电工合闸前的验电笔。import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class DbConnectDemo { private static final String URL jdbc:mysql://localhost:3306/demo_school ?useUnicodetruecharacterEncodingutf8useSSLfalse serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 你的密码; public static void main(String[] args) { // 方式一显式加载驱动JDBC 4.0之后可省略但写上更直观 try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { System.err.println(驱动没找到检查jar包是否在classpath里); e.printStackTrace(); return; } // try-with-resources自动关闭连接避免资源泄漏 try (Connection conn DriverManager.getConnection(URL, USER, PASSWORD); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT id, student_name FROM t_student)) { System.out.println(连接成功查询结果); while (rs.next()) { System.out.println(rs.getLong(id) - rs.getString(student_name)); } } catch (Exception e) { System.err.println(连接或查询失败); e.printStackTrace(); } } }关于Class.forName要不要写这里多说两句。JDBC 4.0之后引入了SPI机制驱动jar包里的META-INF/services/java.sql.Driver文件会被自动发现理论上不写Class.forName也能连。但实际项目里我建议保留原因有二一是显式加载能让“驱动缺失”这个错误在启动阶段就暴露而不是等到第一次getConnection才报二是一些老容器或特殊类加载环境下SPI可能失效显式加载更稳。代价几乎为零何乐而不为。try-with-resources这个写法必须养成习惯。Connection、Statement、ResultSet都是需要关闭的资源传统写法要在finally里逐个判空关闭又臭又长还容易漏。try-with-resources会在代码块结束时按声明的逆序自动关闭干净利落。注意关闭顺序是ResultSet→Statement→Connection声明顺序反过来正好对应。4. 实操全流程复盘与高频问题排查4.1 从零到跑通的完整操作序列我把整个流程按顺序列一遍你对照着走哪一步卡住了就停下来查。第一步确认MySQL服务在跑。命令行执行mysql -u root -p能进去就说明服务正常。进不去先查服务状态Windows在服务管理器里看MySQL服务是否启动Linux用systemctl status mysql。第二步执行第2节的建库建表脚本。建议把脚本存成一个.sql文件用source命令执行比一行行粘贴靠谱。执行完用SHOW TABLES;确认两张表都在用DESC t_student;看看字段类型对不对。第三步确认驱动jar包位置正确。普通工程看lib目录Web工程看WEB-INF/lib。用Maven的话在IDE的依赖树里能看到就行。第四步把第3节的验证代码跑起来。用户名密码换成你自己的。如果打印出学生列表恭喜闭环打通了。第五步故意改错一个参数试试。比如把库名改成不存在的demo_schoolx看报什么错把密码改错看报什么错。这一步是让你建立“错误信息→原因”的映射后面真出问题时不慌。4.2 常见报错速查表下面这张表是我这些年攒下来的基本覆盖了这个Demo阶段能遇到的所有典型错误。看到报错先来这张表里对号入座。报错信息关键词大概率原因解决方向ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动jar不在classpath检查lib或WEB-INF/libMaven检查依赖是否下载成功Access denied for user rootlocalhost用户名或密码错核对密码确认该用户有远程/本地访问权限Unknown database xxx库名写错或库没建SHOW DATABASES;确认库存在核对URL里的库名The server time zone value ... is unrecognized没配serverTimezoneURL里加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowed8.x认证插件需要公钥URL里加allowPublicKeyRetrievaltrueCannot add or update a child row: a foreign key constraint fails插入的class_id在班级表不存在先插班级或核对class_id值Duplicate entry xxx for key uk_student_no学号重复换一个学号或先删掉重复记录中文显示成问号或乱码字符集不一致库、表、连接三处字符集统一为utf8mb4Communications link failure数据库服务没起或端口不通检查服务状态和端口防火墙放行4.3 几个只有踩过才知道的细节第一个关于allowPublicKeyRetrievaltrue的安全性。这个参数让客户端在需要时向服务器请求公钥方便但不加密传输公钥理论上存在中间人风险。本地开发无所谓生产环境应该配合SSL使用或者改用mysql_native_password认证插件不推荐8.x已标记废弃。教学阶段先跑通安全意识后面再补。第二个关于自增主键的回填。插入一条学生记录后如果你需要立刻拿到它的自增ID比如要往关联表插数据用Statement.RETURN_GENERATED_KEYS配合getGeneratedKeys()。别用SELECT MAX(id)去查并发下会拿到别人的ID。这个技巧在Demo阶段用不上但提前知道能少走弯路。第三个关于连接不关的后果。我见过有同学在循环里反复getConnection却从不关闭跑几百次之后报Too many connections。MySQL默认最大连接数是151每个连接都占内存。try-with-resources是底线再往上就是连接池的事了那是下一个阶段的内容。第四个关于DROP DATABASE的肌肉记忆。练习时反复建库删库很正常但养成“删之前先SELECT确认一下”的习惯。我有一次在测试环境手滑DROP了一个名字很像的库虽然数据能重建但那半天白干了。现在我的习惯是删库语句先注释掉确认无误再取消注释执行。5. 这个Demo往后能怎么长把数据库建起来、连接跑通只是JavaEE这条线的起点。接下来最自然的延伸是把这段裸JDBC代码封装成DAO层——一个StudentDao类把增删改查包成方法让业务代码不直接碰SQL。再往后是引入连接池比如HikariCP解决频繁建连的性能问题然后是事务管理保证多条SQL要么全成功要么全回滚。每一步都建立在这个Demo打好的地基上。我个人在实际操作中的体会是数据库这块最忌讳“跳步”。表结构没设计好就急着写代码后面改字段改到怀疑人生连接参数没搞明白就上框架框架报的错比裸JDBC还难懂。反而是像这样老老实实建两张表、写一段验证代码、把每个报错都亲手触发一遍基础打得扎实后面学什么都快。最后分享一个小习惯每次新建一个Demo库我都会在库名后面加日期后缀比如demo_school_0315练完不删留着过段时间回头看能清楚看到自己当时的设计思路和现在的差距比任何教程都直观。