1. 整个系列怎么规划,第01篇要解决什么
MySQL 大概是这个星球上用得最广的开源关系型数据库,没有之一。小到个人博客,大到电商后台、物流系统、BI报表,一堆业务系统底层都是它扛着。这个系列取名叫“手把手教你MySQL”,第一篇我先把地基打牢:从安装、配置、基础操作一路干到第一个能跑起来的完整项目。
为什么把“完整项目”作为第一篇的终点?我见过太多人装完MySQL之后只会执行几个SELECT 1、show databases,然后就没有然后了。学数据库不跟实际业务挂钩,等于背了满脑子语法规律但一个场景套不进去。所以这一篇宁可步子迈大一点,直接用一个小而完整的 JavaWeb 员工管理系统,把建库、建表、写SQL、用JDBC连库、增删改查全部串一遍。整个过程走完,你手里就有了一个能运行、能演示、能拿去扩展成课程设计的真项目。
这个系列的内容我按一个新手最自然的成长路径来排的:
- 第01篇(本篇):安装 MySQL 8.0,做基础配置,完成建库建表与CRUD,最后用 JDBC 跑通第一个小项目。
- 后续篇:事务与隔离级别、索引与查询优化、存储过程与触发器、数据备份恢复、主从复制,以及用 Spring Boot + MyBatis 重构今天这个小项目。
学习环境方面,我建议 Windows 作为主力开发机,有条件的话再准备一台 Linux 服务器或者虚拟机做对照实验。原因很简单:日常开发在 Windows 上最顺手,可真实生产环境几乎都是 Linux,两边都熟悉才不容易被环境差异卡住。
关于版本选型,默认装 8.0.x。8.0 从 2018 年发布到现在已经足够成熟,窗口函数、CTE、原子DDL、更好用的优化器这些都比老版本强太多,而且是当前主流云数据库的默认底座版本。如果你因为历史项目被迫用 5.7,本篇安装思路同样适用,只是部分参数名有差异,我在关键位置会标注出来。
2. 环境准备与安装实操:Windows 和 Linux 两条路都踩一遍
2.1 Windows 安装 MySQL 8.0(MSI安装包方式)
Windows 上最省事的方式是直接用 MySQL Installer。去 MySQL 官网的 Downloads 页面,选 MySQL Community Server 8.0.x 对应的 Windows (x86, 32-bit), MSI Installer 版本。注意区分安装器体积大小:web版只有几 MB,安装时联网下载组件;完整版接近四百 MB,离线环境必须选这个。
安装时记得选Server only,那些 MySQL for Visual Studio、MySQL Workbench 之类的组件后面需要再补,第一遍尽量保持干净,减少干扰项。安装路径建议不要带中文和空格,比如D:\MySQL\Server\8.0,避免后面写配置文件和命令行操作时出现编码或转义问题。
进入配置环节有几个坑我展开说:
- 端口号默认
3306,如果本机已经装了旧版 MySQL 或者纳管数据库,先netstat -ano | findstr 3306查一下端口是不是被占用。被占用就改成 3307,后面所有连接串都要同步改。 - Authentication Method 选择Use Legacy Authentication还是Use Strong Password Encryption?8.0 默认推荐后者,加密方式是
caching_sha2_password。但如果你后面要接的客户端、驱动版本比较老(比如 5.x 的 JDBC 驱动),有可能连不上。新手阶段我建议直接选 Strong Password 即可,配合 8.0.x 新版驱动没有问题;如果连不上再把 root 的插件改成 mysql_native_password。 - Root 密码务必记住,这是你这台机器数据库的最高权限。大小写、数字、特殊符号都要有,后面项目连接也要用这个密码。密码忘了很麻烦,毕竟要跳过权限表、改配置重启才能重置。
配置完成后,可以在 Windows 服务列表里看到MySQL80这个服务,默认开机自启。命令行验证一下:
mysql -uroot -p输入密码后能进入mysql>提示符,说明安装成功。
2.2 Windows 安装另一种思路:ZIP 压缩包方式
MSI 安装器虽然傻瓜,但有时候会莫名卡在 “MySQL Installer” 的配置步骤上,或者你想把 MySQL 做成绿色版跟着项目走。这种情况我推荐用 ZIP 包手搭。
下载mysql-8.0.x-winx64.zip,解压到D:\MySQL\Server\8.0。然后在根目录新建my.ini,至少包含这些关键内容:
[mysqld] basedir=D:/MySQL/Server/8.0 datadir=D:/MySQL/Server/8.0/data port=3306 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-authentication-plugin=caching_sha2_password [client] default-character-set=utf8mb4注意 datadir 这个目录通常不会自动创建,你需要手动建一个空的data文件夹,或者让初始化命令帮你建。接着以管理员身份打开命令提示符,进入 bin 目录,执行初始化:
mysqld --initialize-insecure--initialize-insecure会生成一个密码为空的 root 账户,适合第一次登录后手动改密;如果你用--initialize,它会在日志文件里生成随机临时密码,找起来麻烦,新手不太推荐。
初始化完成后安装系统服务:
mysqld --install MySQL80 net start MySQL80然后登录改密码:
mysql -uroot --skip-password ALTER USER 'root'@'localhost' IDENTIFIED BY '你的新密码'; FLUSH PRIVILEGES;这种方式的好处是整个过程每一步都看得见,出问题也好定位,而且对后面理解 MySQL 的目录结构非常有帮助。我个人在教学时更倾向于先带大家走一遍这种方式。
2.3 Linux(CentOS / Ubuntu)安装 MySQL 8.0
生产环境基本都在 Linux,所以 Linux 安装至少要走一遍。
CentOS / RHEL 系先装官方 yum 源再安装:
wget https://dev.mysql.com/get/mysql80-community-release-el7-9.noarch.rpm rpm -ivh mysql80-community-release-el7-9.noarch.rpm yum install mysql-community-server -y systemctl start mysqld systemctl enable mysqld安装完成后,MySQL 8.0 的随机临时密码会自动写进日志文件:
grep 'temporary password' /var/log/mysqld.log拿到临时密码后登录并改密:
mysql -uroot -p ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';Ubuntu/Debian 系更简单:
apt update apt install mysql-server-8.0 -y systemctl start mysqlDebian 系安装时默认 root 是 auth_socket 认证,也就是说在系统命令行里直接用sudo mysql就能免密进去,要改成密码登录需要手动切换到 mysql_native_password 或 caching_sha2_password。新手阶段如果喜欢用客户端工具连,建议执行一下切换:
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY '你的密码';2.4 安装后的基线检查
无论哪种方式装完,都建议花三分钟做下面的检查:
SELECT VERSION(); -- 版本号 SHOW VARIABLES LIKE 'port'; -- 端口 SHOW VARIABLES LIKE 'character_set_server'; -- 服务端字符集 SHOW VARIABLES LIKE 'lower_case_table_names'; -- 表名大小写策略我用这组命令排查过不少环境问题。比如字符集如果还是 latin1,后面插入中文直接乱码;lower_case_table_names在 Windows 上是 1(不区分大小写)、Linux 上是 0(区分大小写),这直接决定你项目里写表名时要不要统一大小写。建议项目里全部小写表名,谁写谁知道省多少事。
注意:
lower_case_table_names在 8.0 里只有初始化时能改,装完再改不生效,所以一定要在初始化前想清楚。Linux 默认 0,如果你就是要 Windows 和 Linux 两套环境跑同一个项目,建议初始化时加上--lower-case-table-names=1。
3. 数据库基础:建库建表与 CRUD 实操
3.1 root 权限太大,先建一个普通用户
上来就用 root 操作业务库是非常不好的习惯。root 的权限是整个 MySQL 实例,一旦误删了系统表,后果很难收拾。我们在项目开始前先创建专用账号:
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'App_User_123'; CREATE USER 'app_user'@'%' IDENTIFIED BY 'App_User_123'; GRANT ALL PRIVILEGES ON employee_db.* TO 'app_user'@'localhost'; GRANT ALL PRIVILEGES ON employee_db.* TO 'app_user'@'%'; FLUSH PRIVILEGES;注意'app_user'@'%'和'app_user'@'localhost'是两个独立账号,%表示允许从任意主机连接,适合远程调试;localhost 用于本机连接。两个都建的好处是让你彻底理解 MySQL 的“账号=用户名+主机”模型。
3.2 建库建表:用员工表把字段类型讲明白
我们这个项目是一套极简员工信息管理系统(Employee Management System,简称 EMS),数据库名employee_db。以员工表为主表,同时设计一张部门表,两张表之间用外键关联,既简单又能把常见关系表设计带出来:
CREATE DATABASE IF NOT EXISTS employee_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE employee_db; CREATE TABLE department ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '部门ID', dept_name VARCHAR(50) NOT NULL UNIQUE COMMENT '部门名称', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB; CREATE TABLE employee ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '员工ID', emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', emp_name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT DEFAULT 1 COMMENT '1-男 0-女', salary DECIMAL(10,2) DEFAULT 0.00 COMMENT '薪资', dept_id INT NOT NULL COMMENT '所属部门ID', hire_date DATE COMMENT '入职日期', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES department(id) ) ENGINE=InnoDB;这里每个字段选择都有讲究:
emp_no用 VARCHAR 而不是 INT,因为工号往往带前缀或补零,纯数字类型会把000123变成123。gender用 TINYINT 存 0/1,比直接存汉字节省空间,而且代码层面好判断。salary必须用 DECIMAL,不能用 FLOAT/DOUBLE。浮点数存金额会出精度问题,这是做财务系统的经典教训。hire_date用 DATE 类型,只关注年月日;如果还要存具体时分秒,就换 DATETIME。- 引擎统一指定 InnoDB,因为它支持事务、外键、行级锁,是现阶段最稳妥的选择。
- 外键约束我建了,是为了让新手理解表关联;但等后面讲性能优化的时候我们会讨论为什么生产环境有时要刻意去掉外键。
3.3 最常用的 CRUD 语句,一条条过
往部门表插数据:
INSERT INTO department (dept_name) VALUES ('技术部'), ('产品部'), ('人事部');往员工表插数据:
INSERT INTO employee (emp_no, emp_name, gender, salary, dept_id, hire_date) VALUES ('A0001', '张伟', 1, 15000.00, 1, '2022-03-15'), ('A0002', '李娜', 0, 13000.00, 2, '2022-06-01'), ('A0003', '王强', 1, 9000.00, 3, '2023-01-10');查询最核心的一条 SQL,把员工表和部门表关联起来:
SELECT e.emp_no, e.emp_name, e.gender, e.salary, d.dept_name, e.hire_date FROM employee e INNER JOIN department d ON e.dept_id = d.id WHERE e.salary > 10000 ORDER BY e.salary DESC;这条语句几乎涵盖了单表多表查询的全部基础知识点:别名、关联条件、过滤条件、排序。能一眼看懂,说明前面的 JOIN 概念基本过关。
更新和删除:
UPDATE employee SET salary = 16000.00 WHERE emp_no = 'A0001'; DELETE FROM employee WHERE emp_no = 'A0003';注意 UPDATE 和 DELETE 一定要带 WHERE。忘了带条件的后果就是全表被改、全表被删。我见过不止一次新手在测试库里UPDATE employee SET salary = 0然后整脸懵的场景。如果不小心发生了,赶紧看有没有开事务或者备份,否则只能祈祷。
3.4 让 MySQL 自动管理时间
上面两张表里都有create_time字段,默认值设为CURRENT_TIMESTAMP,这样插入数据时不用手动传时间,MySQL 会自动记录。员工表的hire_date是业务字段,必须手动传入,因为它代表“入职日期”这样一个有业务含义的时间点,不能用系统当前时间替代。这个区分是初学者最容易混淆的地方:系统字段与业务字段要分开,不能偷懒全用当前时间。
4. 第一个完整项目实操:JavaWeb + JDBC 员工管理系统
4.1 为什么第一个项目选 JavaWeb + JDBC
很多人会问,现在写后端不是都用 Spring Boot + MyBatis 吗?为什么第一个项目反而用一个看起来“老掉牙”的组合?我的答案是:正因为 JDBC 是底层,它能把数据库连接怎么建立、SQL 怎么执行、结果集怎么映射讲得明明白白。用 Spring Boot 的时候,这些东西全被框架封装了,你只会写 Mapper 接口,出了个数据库连接问题根本不知道怎么排查。
JDBC 相当于让你亲手把数据传输管道接一遍,等你知道这条管道长什么样了,再用 MyBatis 不过是把“管道工”换成“自动管道机”。这个项目我会保留最底层的连接管理逻辑,用纯 Servlet + JSP + JDBC 实现,学完这个再去看 Spring Boot,你会觉得很多东西似曾相识。
4.2 项目结构准备
环境要求:JDK 8 及以上、Tomcat 9、MySQL 8.0、IDEA 社区版或专业版都可。项目用 Maven 管理依赖,核心只需要一个依赖:
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>这个mysql-connector-j就是 8.0 系列里替换老mysql-connector-java的新坐标,驱动类是com.mysql.cj.jdbc.Driver。如果你用旧坐标,依赖也能解析,但建议直接用新名字。
项目目录结构如下:
employee-web/ ├── pom.xml ├── src/main/java/com/example/ems/ │ ├── util/DBUtil.java │ ├── dao/EmployeeDao.java │ ├── model/Employee.java │ └── servlet/EmployeeServlet.java ├── src/main/resources/db.properties └── src/main/webapp/ ├── WEB-INF/web.xml ├── list.jsp └── add.jsp4.3 数据库连接工具类:配置外置与连接复用
JDBC 的第一步是获取连接。最蠢但也最常见的写法是每执行一次 SQL 就DriverManager.getConnection()一次,用完就关。这种方式在本地测试没问题,但并发一上来就会把数据库连接数打满。我们先用连接池的思路简化实现,但保留DBUtil作为统一入口,后面可以平滑替换成 Druid 或 HikariCP。
db.properties放在 resources 目录下:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/employee_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=app_user jdbc.password=App_User_123注意 URL 里三个参数:
useSSL=false:本地开发不需要 SSL 加密,走明文即可,加上这个参数免得 8.0 连本地时弹出 SSL 告警。serverTimezone=Asia/Shanghai:8.0 驱动对时区敏感,不指定会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized这类时区乱码错误。characterEncoding=utf8:让驱动和数据库之间的传输字符集统一为 UTF-8,避免中文乱码。
DBUtil类的写法:
package com.example.ems.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); Class.forName(props.getProperty("jdbc.driver")); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置初始化失败:" + e.getMessage()); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, username, password); } }这里有几个容易踩的坑:
Class.forName()在 8.0 驱动里其实可以省略,因为 JDBC 4.0 以上采用 SPI 机制自动注册驱动。但我仍然写了,因为老项目迁移时多这一行能少很多莫名其妙的“No suitable driver found”问题。- 配置加载用类加载器读取 classpath 资源,不要用绝对路径,否则换环境就崩。
- 静态块里初始化失败要尽早抛异常,不要让项目启动后带着一个坏连接池运行。
4.4 JDBC 增删改查:PreparedStatement 是底线
写 JDBC 时永远用PreparedStatement,不要用Statement字符串拼接 SQL。原因有两个:预编译可以防 SQL 注入,还方便处理参数类型。下面是EmployeeDao的核心方法:
package com.example.ems.dao; import com.example.ems.model.Employee; import com.example.ems.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class EmployeeDao { public List<Employee> findAll() { List<Employee> list = new ArrayList<>(); String sql = "SELECT e.id, e.emp_no, e.emp_name, e.gender, e.salary, e.hire_date, d.dept_name " + "FROM employee e INNER JOIN department d ON e.dept_id = d.id ORDER BY e.id"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Employee emp = new Employee(); emp.setId(rs.getInt("id")); emp.setEmpNo(rs.getString("emp_no")); emp.setEmpName(rs.getString("emp_name")); emp.setGender(rs.getInt("gender")); emp.setSalary(rs.getBigDecimal("salary")); emp.setHireDate(rs.getDate("hire_date")); emp.setDeptName(rs.getString("dept_name")); list.add(emp); } } catch (Exception e) { e.printStackTrace(); } return list; } public int insert(Employee emp) { String sql = "INSERT INTO employee (emp_no, emp_name, gender, salary, dept_id, hire_date) VALUES (?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, emp.getEmpNo()); ps.setString(2, emp.getEmpName()); ps.setInt(3, emp.getGender()); ps.setBigDecimal(4, emp.getSalary()); ps.setInt(5, emp.getDeptId()); ps.setDate(6, new java.sql.Date(emp.getHireDate().getTime())); return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } } public int deleteById(int id) { String sql = "DELETE FROM employee WHERE id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } } }这里我用了 try-with-resources,Connection、PreparedStatement、ResultSet 自动关闭,比 finally 块里手动 close 干净得多。注意rs.getDate返回的是java.sql.Date,如果要转java.util.Date,直接赋值给Date类型属性即可,JDBC 驱动内部做了转换。
4.5 Servlet 控制层:把 HTTP 请求转换成业务动作
Servlet 这部分其实不复杂,它就是接收前端请求,调用 DAO 层处理数据,再根据操作结果跳转页面。核心的“查询全部员工列表”请求处理如下:
package com.example.ems.servlet; import com.example.ems.dao.EmployeeDao; import com.example.ems.model.Employee; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.util.List; @WebServlet("/employee") public class EmployeeServlet extends HttpServlet { private EmployeeDao dao = new EmployeeDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if ("delete".equals(action)) { int id = Integer.parseInt(req.getParameter("id")); dao.deleteById(id); } List<Employee> list = dao.findAll(); req.setAttribute("employees", list); req.getRequestDispatcher("/list.jsp").forward(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); Employee emp = new Employee(); emp.setEmpNo(req.getParameter("empNo")); emp.setEmpName(req.getParameter("empName")); emp.setGender(Integer.parseInt(req.getParameter("gender"))); emp.setSalary(new java.math.BigDecimal(req.getParameter("salary"))); emp.setDeptId(Integer.parseInt(req.getParameter("deptId"))); emp.setHireDate(java.sql.Date.valueOf(req.getParameter("hireDate"))); dao.insert(emp); resp.sendRedirect("employee"); } }一个细节:req.setCharacterEncoding("UTF-8")必须写在读取任何参数之前,否则 POST 请求里的中文名称会乱码。GET 请求的参数编码在 Tomcat 9 里默认已经处理好了,但 POST 不会,这是 JavaWeb 老生常谈的坑。
4.6 前端展示:用 JSP 把结果渲染出来
list.jsp要做的事情很简单:从 request 作用域里取出员工列表,用 JSTL 渲染成表格。核心片段:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <h2>员工列表</h2> <table border="1" cellpadding="8" cellspacing="0"> <tr> <th>工号</th><th>姓名</th><th>性别</th><th>薪资</th><th>部门</th><th>入职日期</th><th>操作</th> </tr> <c:forEach items="${employees}" var="emp"> <tr> <td>${emp.empNo}</td> <td>${emp.empName}</td> <td>${emp.gender == 1 ? '男' : '女'}</td> <td>${emp.salary}</td> <td>${emp.deptName}</td> <td>${emp.hireDate}</td> <td><a href="employee?action=delete&id=${emp.id}" onclick="return confirm('确定删除?')">删除</a></td> </tr> </c:forEach> </table> <br/> <a href="add.jsp">新增员工</a> </body> </html>add.jsp里表单的action="employee"、method="post",这样和后端doPost对应起来。到这里,项目的请求链路已经完整:
浏览器发起请求 → Tomcat 根据@WebServlet("/employee")找到 Servlet → Servlet 调 DAO → DAO 通过 JDBC 操作 MySQL → 数据返回给 Servlet 存到 request → forward 到 JSP → 渲染 HTML 返回浏览器。
4.7 把项目完整跑起来
在 IDEA 里配置 Tomcat,把项目打成 war 包部署,启动 Tomcat,浏览器访问http://localhost:8080/employee就能看到员工列表。再走一遍新增流程,页面上能查到刚插入的数据,这个项目就算完整跑通了。
跑通之后我强烈建议你亲手做一次“端到端追踪”:在findAll()方法里打一个断点,浏览器刷新页面,看请求怎么进入 Servlet、SQL 怎么执行、结果集怎么封装,这比看一百遍教程都管用。我自己带新人的时候,这一步是最能帮助人建立全局感的。
5. 安装配置与项目联调常见问题排查
5.1 表结构没问题,why 中文乱码?
八成是字符集问题。先执行:
SHOW VARIABLES LIKE 'character_set_%';重点看character_set_server和character_set_database。如果都是 utf8mb4,再检查 JDBC URL 有没有加characterEncoding=utf8,以及页面contentType有没有写charset=UTF-8。乱码问题通常出在这三层中的某一层,逐层排查即可。
5.2 root 密码忘了
Windows 上以管理员身份停掉 MySQL 服务,然后在 my.ini 里加一行skip-grant-tables,重启服务后免密登录:
mysql -uroot进去后先给 root 设新密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码'; FLUSH PRIVILEGES;设置完成后改回配置文件,去掉skip-grant-tables,重启服务。注意这个操作期间任何程序都能免密连库,必须在局域网或本机操作,搞完马上恢复。
5.3 连接时报 Access denied for user
如果是Access denied for user 'app_user'@'localhost',优先检查密码是否正确;如果确认密码对,那就检查账号的主机限制。用 root 登录后执行:
SELECT user, host FROM mysql.user WHERE user='app_user';如果你只有'app_user'@'%'的记录,本地连接时 MySQL 会尝试用'app_user'@'localhost'匹配,匹配不上就拒绝。所以本地跟远程两个账号到底建没建齐,一查便知。
5.4 服务起不来 / 端口冲突
net start MySQL80报错,先看data目录下的.err日志文件,这个文件里写的错误信息通常是最直接的。如果端口冲突,则netstat -ano | findstr 3306看谁占着端口,改端口或停掉占用进程。
5.5 JDBC 驱动报 ClassNotFound / No suitable driver
绝大多数情况是依赖没下载成功或作用域不对。Maven 工程里检查target/classes下有没有驱动 jar,再看看pom.xml里面依赖的 scope 是否意外写成了provided或test。还有个小概率是 IDEA 缓存问题,执行mvn clean后再 reimport 一次。
5.6 建表报主键冲突,id 不是自增吗?
把建表语句里的id INT AUTO_INCREMENT PRIMARY KEY与原表对照看一下。如果原表建的时候没有 AUTO_INCREMENT,后面再ALTER TABLE employee MODIFY id INT AUTO_INCREMENT;补上即可。另外 DELETE 清空表后想重置自增计数,用TRUNCATE TABLE employee;而不是DELETE FROM employee;,前者会把自增序列一并重置。
6. 这一篇的实操心得与下一步建议
装 MySQL 本身不难,难的是装完之后你知不知道自己在装什么、怎么验证、怎么把它用起来。这一篇走完,你手里应该有四个成果:一台装了 MySQL 8.0 的机器、一套建好的员工库表、一份能跑通的基础 CRUD 技能、一个能演示给同学看的 JavaWeb 项目。把这四样东西握在手里,后面再学索引、事务、优化都会有着陆点,而不是悬在空中的概念。
最后分享两个我在实操中发现的小技巧。第一个是关于排查问题的思路:数据库报错时先看异常信息的前三行,绝大多数同学会直接滑到最后找Caused by,但真正有用的错误提示往往就在最开始。先读异常头部,能省下大量时间。第二个是关于学习节奏:MySQL 的知识点又杂又多,不要指望一遍全记住,我在带人时常说“先做后懂”,先把今天这个项目原样跑通,哪怕PreparedStatement的内部原理暂时理解不透,先用熟悉的手感,等你写烂了再回头抠原理,一切都豁然开朗。
下一篇我会带着大家把 JDBC 连接用连接池重构,再顺手把这套员工系统里埋的几个数据冗余和查询低效问题挨个拆掉,顺便引出索引和事务这两个重头戏。先把今天这篇的每一步复现一遍,下一篇我们继续。