Forest项目中将Derby数据库替换为MySQL
在开发Java Web应用时,我们常常会用到像Apache Derby这样的嵌入式数据库——启动快、配置简单,特别适合教学演示或本地调试。比如经典的Forest示例项目,默认就使用Derby作为持久化存储。但一旦进入类生产环境,问题就来了:连接不稳定、并发支持弱、缺乏运维工具……这时候就得换上真正“能打”的数据库,比如MySQL。
那怎么把一个原本跑在Derby上的Java EE项目,平滑迁移到MySQL?别担心,整个过程其实并不复杂,只要搞清楚几个关键点:数据源怎么改、SQL脚本有哪些坑、驱动包放哪儿。接下来我就带你一步步完成这次迁移,照着做就行,连初学者也能搞定。
从web.xml开始:重新定义数据源
Forest这类基于Java EE/Jakarta EE的应用,通常会在web.xml里通过JNDI注册全局数据源。原来的配置长这样:
<data-source> <name>java:global/ForestDataSource</name> <class-name>org.apache.derby.jdbc.EmbeddedDataSource</class-name> <database-name>forest</database-name> <property> <name>createDatabase</name> <value>create</value> </property> </data-source>现在我们要把它换成MySQL的连接方式。找到你的WEB-INF/web.xml文件,把上面那段替换成如下内容:
<data-source> <name>java:global/ForestDataSource</name> <class-name>com.mysql.cj.jdbc.MysqlDataSource</class-name> <server-name>localhost</server-name> <port-number>3306</port-number> <user>root</user> <password>admin</password> <property> <name>databaseName</name> <value>forest</value> </property> <property> <name>useSSL</name> <value>false</value> </property> <property> <name>allowPublicKeyRetrieval</name> <value>true</value> </property> </data-source>这里有几个细节要注意:
com.mysql.cj.jdbc.MysqlDataSource是MySQL Connector/J 8.x推荐的数据源实现类,比老式的DriverManager.getConnection()更适合容器管理。- 原来的
<database-name>标签其实是非标准写法,在新版本中应该用<property>包裹databaseName。 useSSL=false和allowPublicKeyRetrieval=true这两个参数在开发环境下很实用,避免因为证书验证或公钥获取失败导致连不上数据库(线上请务必开启SSL并妥善配置)。- 如果你MySQL不是跑在默认3306端口,记得改
<port-number>。
改完之后先别急着重启服务器——数据库还没建呢。
先动手:创建MySQL数据库与用户权限
在应用启动前,得确保目标数据库已经准备就绪。登录MySQL命令行或者用Navicat这类工具执行:
CREATE DATABASE IF NOT EXISTS forest CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;为什么强调utf8mb4?因为它是真正完整的UTF-8支持,能存emoji、特殊符号这些四字节字符。而所谓的utf8在MySQL里其实是阉割版,最多只支持三字节,容易出乱码。
如果你不想用root账户跑应用(强烈建议不要这么做),那就提前建个专用用户并授权:
CREATE USER 'forest_user'@'%' IDENTIFIED BY 'your_secure_password'; GRANT ALL PRIVILEGES ON forest.* TO 'forest_user'@'%'; FLUSH PRIVILEGES;然后回头去web.xml里把<user>和<password>改成对应值即可。
persistence.xml需要动吗?
大多数情况下,不需要。
Forest项目一般使用JPA(比如EclipseLink或Hibernate),它的持久化单元会自动根据连接的数据库类型选择合适的方言。典型的persistence.xml看起来是这样的:
<persistence-unit name="forest"> <jta-data-source>java:global/ForestDataSource</jta-data-source> <properties> <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/> </properties> </persistence-unit>只要<jta-data-source>指向的是正确的JNDI名称,Hibernate就能从JDBC URL推断出这是MySQL,并启用MySQL8Dialect。但如果你之前手动指定过方言,比如:
<property name="hibernate.dialect" value="org.hibernate.dialect.DerbyTenSevenDialect"/>那就一定要改成:
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL8Dialect"/>否则你会发现一些语法报错,比如分页查询用不了LIMIT,或者自增主键生成策略有问题。
别忘了加JDBC驱动包
GlassFish、Payara这类Java EE应用服务器不会自带MySQL驱动,所以你必须手动把mysql-connector-java.jar放进类路径。
最稳妥的位置是服务器的domain库目录:
domains/domain1/lib/mysql-connector-java-8.0.33.jar下载哪个版本?建议选8.0.x系列,兼容MySQL 5.7+和8.0+。如果是Maven项目,也可以直接打入WEB-INF/lib,但独立部署时还是推荐放在domain lib下统一管理。
✅ 放好后重启服务器,确认日志没有类找不到的错误。
SQL脚本适配:这才是最容易踩坑的地方
Forest项目通常附带三组初始化脚本:drop.sql、create.sql、data.sql。它们最初是为Derby写的,直接扔给MySQL可能会炸。
drop.sql:先关外键检查
Derby允许DROP TABLE IF EXISTS无脑删表,但MySQL对顺序敏感,尤其是有外键约束的时候。稳妥做法是先关闭约束检查:
SET FOREIGN_KEY_CHECKS = 0; DROP TABLE IF EXISTS ORDER_DETAIL; DROP TABLE IF EXISTS CUSTOMER_ORDER; DROP TABLE IF EXISTS PRODUCT; DROP TABLE IF EXISTS CATEGORY; DROP TABLE IF EXISTS PERSON_GROUPS; DROP TABLE IF EXISTS PERSON; DROP TABLE IF EXISTS GROUPS; DROP TABLE IF EXISTS ORDER_STATUS; SET FOREIGN_KEY_CHECKS = 1;这样不管依赖关系如何,都能干净清除旧结构。
create.sql:核心DDL重写指南
这是最关键的一步。下面是针对MySQL优化后的完整版本:
-- 创建并切换数据库 CREATE DATABASE IF NOT EXISTS forest CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE forest; -- 启用事务保证一致性 SET autocommit=0; START TRANSACTION; -- 类别表 CREATE TABLE CATEGORY ( ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(45) NOT NULL, TAGS VARCHAR(45) ); CREATE UNIQUE INDEX SQL_CATEGORY_ID_INDEX ON CATEGORY(ID); -- 用户表 CREATE TABLE PERSON ( ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY, FIRSTNAME VARCHAR(50) NOT NULL, LASTNAME VARCHAR(100) NOT NULL, EMAIL VARCHAR(45) NOT NULL UNIQUE, ADDRESS VARCHAR(45) NOT NULL, CITY VARCHAR(45) NOT NULL, PASSWORD VARCHAR(100), DTYPE VARCHAR(31) ); CREATE UNIQUE INDEX SQL_PERSON_EMAIL_INDEX ON PERSON(EMAIL); CREATE UNIQUE INDEX SQL_PERSON_ID_INDEX ON PERSON(ID); -- 组表 CREATE TABLE GROUPS ( ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(50) NOT NULL, DESCRIPTION VARCHAR(300) ); -- 用户-组关联表 CREATE TABLE PERSON_GROUPS ( GROUPS_ID INT NOT NULL, EMAIL VARCHAR(45) NOT NULL, CONSTRAINT PK_PERSON_GROUPS PRIMARY KEY (GROUPS_ID, EMAIL) ); ALTER TABLE PERSON_GROUPS ADD CONSTRAINT FK_PERSON_GROUPS_GROUP FOREIGN KEY (GROUPS_ID) REFERENCES GROUPS(ID); ALTER TABLE PERSON_GROUPS ADD CONSTRAINT FK_PERSON_GROUPS_PERSON FOREIGN KEY (EMAIL) REFERENCES PERSON(EMAIL); CREATE INDEX SQL_PERSONGROUPS_EMAIL_INDEX ON PERSON_GROUPS(EMAIL); CREATE INDEX SQL_PERSONGROUPS_ID_INDEX ON PERSON_GROUPS(GROUPS_ID); -- 订单状态表 CREATE TABLE ORDER_STATUS ( ID INT NOT NULL PRIMARY KEY, STATUS VARCHAR(45) NOT NULL, DESCRIPTION VARCHAR(200) ); CREATE UNIQUE INDEX SQL_ORDERSTATUS_ID_INDEX ON ORDER_STATUS(ID); -- 客户订单表 CREATE TABLE CUSTOMER_ORDER ( ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY, AMOUNT FLOAT(52) NOT NULL, DATE_CREATED TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, CUSTOMER_ID INT NOT NULL, STATUS_ID INT NOT NULL ); ALTER TABLE CUSTOMER_ORDER ADD CONSTRAINT FK_CUSTOMER_ORDER_ORDER_STATUS FOREIGN KEY (STATUS_ID) REFERENCES ORDER_STATUS(ID); ALTER TABLE CUSTOMER_ORDER ADD CONSTRAINT FK_CUSTOMER_ORDER_CUSTOMER FOREIGN KEY (CUSTOMER_ID) REFERENCES PERSON(ID); CREATE INDEX SQL_ORDER_STATUS_ID_INDEX ON CUSTOMER_ORDER(STATUS_ID); CREATE INDEX SQL_ORDER_CUSTOMER_ID_INDEX ON CUSTOMER_ORDER(CUSTOMER_ID); CREATE UNIQUE INDEX SQL_ORDER_ID_INDEX ON CUSTOMER_ORDER(ID); -- 商品表 CREATE TABLE PRODUCT ( ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(45) NOT NULL, PRICE DECIMAL(10,2) NOT NULL, DESCRIPTION VARCHAR(145) NOT NULL, IMG VARCHAR(45), CATEGORY_ID INT NOT NULL, IMG_SRC LONGBLOB ); ALTER TABLE PRODUCT ADD CONSTRAINT FK_PRODUCT_CATEGORY FOREIGN KEY (CATEGORY_ID) REFERENCES CATEGORY(ID); CREATE UNIQUE INDEX SQL_PRODUCT_ID_INDEX ON PRODUCT(ID); -- 订单明细表 CREATE TABLE ORDER_DETAIL ( ORDER_ID INT NOT NULL, PRODUCT_ID INT NOT NULL, QTY INT NOT NULL, CONSTRAINT SQL_ORDER_PRODUCT_PK PRIMARY KEY (ORDER_ID, PRODUCT_ID) ); ALTER TABLE ORDER_DETAIL ADD CONSTRAINT FK_ORDER_DETAIL_PRODUCT FOREIGN KEY (PRODUCT_ID) REFERENCES PRODUCT(ID); ALTER TABLE ORDER_DETAIL ADD CONSTRAINT FK_ORDER_DETAIL_ORDER FOREIGN KEY (ORDER_ID) REFERENCES CUSTOMER_ORDER(ID); CREATE UNIQUE INDEX SQL_ORDER_DETAIL_INDEX ON ORDER_DETAIL(ORDER_ID, PRODUCT_ID); CREATE INDEX SQL_ORDER_PRODUCT_ID_INDEX ON ORDER_DETAIL(PRODUCT_ID); CREATE INDEX SQL_ORDER_DETAIL_ID_INDEX ON ORDER_DETAIL(ORDER_ID); COMMIT;重点差异总结一下:
| 功能 | Derby 写法 | MySQL 调整 |
|---|---|---|
| 自增主键 | GENERATED ALWAYS AS IDENTITY | AUTO_INCREMENT |
| 大对象字段 | BLOB(1073741823) | 改用LONGBLOB更合理 |
| 字符集 | 默认Latin1 | 显式声明utf8mb4 |
| 时间戳默认值 | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | 支持,无需改动 |
特别提醒:IMG_SRC用来存图片二进制流,小图可能没问题,但如果上传大文件,记得检查MySQL的max_allowed_packet设置,否则会报“packet too large”错误。
data.sql:数据插入基本兼容
初始化数据这块改动不大,主要是注意字符串引号和日期格式。例如:
INSERT INTO CATEGORY (NAME, TAGS) VALUES ('Plants', 'Seeds, trees, flowers ...'); INSERT INTO PERSON (FIRSTNAME, LASTNAME, EMAIL, ADDRESS, CITY, PASSWORD, DTYPE) VALUES ('Robert', 'Exampler', 'robert@example.com', 'Example street', 'San Francisco', '81dc9bdb52d04dc20036dbd8313ed055', 'Customer');这类语句在两种数据库中都通用。唯一需要注意的是枚举型字段如DTYPE,要确保值合法且长度不超过定义。
最后一步:验证一切是否正常
做完所有修改后,重新构建并部署项目。打开浏览器访问首页,看看能不能加载商品列表、用户信息等数据。
同时可以进MySQL验证:
USE forest; SHOW TABLES; SELECT COUNT(*) FROM PERSON; SELECT * FROM PRODUCT LIMIT 5;如果表都建好了,数据也进去了,说明迁移成功!
小结
把Forest项目从Derby换成MySQL,本质上是一次典型的企业级数据库迁移实践。虽然步骤不多,但每一步都有潜在陷阱:
- 数据源配置要符合JNDI规范;
- SQL脚本不能照搬,特别是DDL语句;
- 驱动包必须正确放置;
- 字符集、外键、事务这些细节决定成败。
完成这次迁移后,你会发现应用更稳定了,也更容易集成监控、备份等运维流程。更重要的是,你对Java EE应用的数据层工作机制有了更深理解——这比单纯跑通一个demo要有价值得多。
至于文中提到的Z-Image-ComfyUI?那是另一个故事了,专用于AI图像生成推理,跟这套传统Web应用架构没啥关系。如果你感兴趣,可以另开一篇聊聊它的部署方案。