背景
- 公司后端是java,所以全栈选择java。期望一人开发一个模块,减少沟通成本。
- 借助ai,我的学习成本降低
- 我的学习方式:是通过案例查漏补缺,有阶段性学习成果。而不是全面学习,避免过于枯燥
- 本文所有案列也是参考一位道友的项目,结合实际场景,进行调整。
java环境和编码工具
-
java环境:直接去官网下载 www.oracle.com/cn/java/tec...
-
编码工具:trae cn
-
导入java项目,提示我安装 Extension Pack for Java(微软官方包,一键装全)
| 子插件 | 作用 |
|---|---|
| Language Support for Java(TM) by Red Hat | Java 语法高亮、代码补全、跳转、重构(JDT.LS 语言服务器) |
| Debugger for Java | 断点调试、变量查看、Call Stack |
| Maven for Java | pom.xml 识别、依赖下载、生命周期命令 |
| Gradle for Java | build.gradle / settings.gradle 支持 |
| Test Runner for Java | JUnit 4/5、TestNG 单测运行 |
| Project Manager for Java | 左侧 JAVA PROJECTS 面板,管理多模块 |
另外提示我安装了
| 子插件 | 作用 |
|---|---|
| Spring Boot Extension Pack | 做 Spring Boot / Spring Cloud 项目,含 Bean 导航、Boot Dashboard、YAML 补全 |
| Spring Initializr Java Support | 新建 Spring Boot 项目向导 |
项目目录结构
perl
java-road/
├── README.md # 项目说明文档
└── study-h2 # 学习笔记 - H2 数据库
└── ... 后续补充
pom.xml # 项目全局依赖配置文件
案例代码地址:gitee.com/banmaxiaoba...
熟悉pom.xml文件
这里我让ai告诉如何了解,基于我是前端开发者的身份。它用了类比法告诉我。
maven项目类似前端里pnpm workspace,是一个多项目的结构。根目录下有个全局的pom.xml文件,用于管理所有子项目的依赖、插件等。每个子项目的pom.xml文件用于管理该子项目的依赖、插件等。pom.xml具体结构如下:
json
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wy.study</groupId>
<artifactId>java-road</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
这块一个文件协议,项目名称是java-road,版本号是1.0,打包方式是pom。
json
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
继承使用spring boot框架,版本号是2.7.18。
json
<modules>
<module>study-h2</module>
</modules>
定义了它的所有子模块,其实每一个子模块都可以单独部署。因为继承父模块。往上也继承spring boot的配置。
json
<properties>
<maven.compiler.source>8</maven.compiler.source>
</properties
全局变量定义区,比如版本号,后面的依赖包可以直接引用变量
json
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
定义项目中依赖包,类似前端里的 npm 包。这里有个<dependencyManagement>包裹的依赖包,不会实际下载。只是用于定义全局的依赖版本号。引入地方不写版本号,会自动引用全局的版本号。
json
<build>
<finalName>JPAApp</finalName>
<plugins>
<!--指定spring boot项目打包后启动的入口-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.wy.study.h2.H2App</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal><!--创建一个自动可执行的jar或war文件 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
java的打包配置,可以设置包名、插件配置、入口文件
案例一:Spring Boot + H2 + MyBatis-Plus:从 0 跑通一个嵌入式数据库 Demo
这篇文章记录我用 H2 嵌入式数据库 + MyBatis-Plus 搭第一个能跑通的 CRUD 的全过程。 没有理论堆砌,全是踩过的坑和跑通的代码。
0. 为什么选 H2?
写后端学习 Demo,你不想每次都去装一套 MySQL 吧?H2 就是为这种场景量身定做的:
- 纯 Java 写的,只有一个 jar。引入依赖就能用,零安装。
- 三种模式任你挑 :
- 内存模式(
jdbc:h2:mem:)--- 重启就没,做单元测试绝配。 - 文件模式(
jdbc:h2:file:D:/workspace/h2/study-db)--- 持久化到磁盘,重启还在。 - 服务模式(
jdbc:h2:tcp://localhost/...)--- 像 MySQL 一样独立跑。
- 内存模式(
- 自带 Web 控制台。启动浏览器就能看表看数据,不用装 Navicat。
一句话:H2 就是 Java 世界里的 SQLite。
1. 项目长什么样?
先把项目目录摆出来,让你一眼看明白:
bash
study-h2/
├── pom.xml # Maven 依赖
└── src/main/
├── java/com/wy/study/h2/
│ ├── H2App.java # 启动类(main 方法在这里)
│ ├── config/
│ │ └── H2MybatisPlusConfig.java # 分页插件配置
│ ├── controller/
│ │ └── H2UserController.java # 对外的 HTTP 接口
│ ├── entity/
│ │ └── H2UserEntity.java # 用户表对应的 Java 类
│ └── service/
│ ├── H2UserMapper.java # MyBatis Mapper 接口(写 SQL 的地方)
│ └── H2UserService.java # 业务层(调用 Mapper)
└── resources/
├── application.yml # 数据库连接 + H2 控制台配置
└── mapper/
└── H2UserMapper.xml # 复杂 SQL 写这里(可选)
2. 第一步:加依赖(3 行搞定)
父 pom 已经统一管版本了,子模块只写坐标。pom.xml 里就加了这两个:
xml
<dependencies>
<!-- MyBatis-Plus:MyBatis 的增强版,自带 CRUD、分页 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- H2 数据库本体 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
就这。不用加版本号,因为父 pom 的 dependencyManagement 已经统一管理好了。
3. 第二步:连数据库(yml 就够了)
打开 src/main/resources/application.yml:
yaml
server:
port: 9203 # 端口
servlet:
context-path: /h2 # 所有接口前面都要加 /h2
spring:
datasource:
url: jdbc:h2:file:D:/workspace/h2/study-db # 文件模式,持久化到这个目录
driver-class-name: org.h2.Driver # H2 的驱动类
username: root
password: root
h2:
console:
enabled: true # 打开 Web 控制台
path: /h2-console # 浏览器访问 http://localhost:9203/h2/h2-console
settings:
web-allow-others: true # 允许非本机访问
启动成功后,浏览器打开:
bash
http://localhost:9203/h2/h2-console
JDBC URL 填上面的 jdbc:h2:file:D:/workspace/h2/study-db,点 Connect,就能看到 H2 自带的 SQL 编辑器了。
4. 重点来了:MyBatis 的三层结构怎么操作数据库?
这部分是我折腾最久的。先给你一张"点菜类比图",一秒看懂:
markdown
┌────────────────────────────────────────────────────┐
│ Controller(服务员) │
│ → 接收客人请求,转给后厨 │
│ 对应:H2UserController.java │
└──────────────────────┬─────────────────────────────┘
│ 调用
┌──────────────────────▼─────────────────────────────┐
│ Service(后厨领班) │
│ → 决定业务流程,指挥 Mapper 干活 │
│ 对应:H2UserService.java │
└──────────────────────┬─────────────────────────────┘
│ 调用
┌──────────────────────▼─────────────────────────────┐
│ Mapper(厨师) │
│ → 真正写 SQL,直接跟数据库打交道 │
│ 对应:H2UserMapper.java + H2UserMapper.xml │
└────────────────────────────────────────────────────┘
加一个"服务员不想给错菜"------Controller 不直接写 SQL; 加一个"后厨不做脏活"------Service 不直接写 SQL; SQL 只在 Mapper 里写。这就是分层的意义。
下面三层从上到下逐个看代码。
第一层:Entity --- 表结构的 Java 镜像
H2 里有一张用户表 STUDY_H2_USER,对应的 Java 类就是 H2UserEntity:
java
@Data // Lombok:自动生成 getter/setter/toString
@TableName("STUDY_H2_USER") // 对应数据库里的表名
public class H2UserEntity {
@TableId(type = IdType.ASSIGN_UUID) // 主键,用雪花算法/UUID 生成
private String id;
private String userName;
private String password;
private String email;
private String phone;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}
这一层零逻辑,就负责把"数据库表字段"和"Java 类属性"一一对应起来。
第二层:Mapper --- 写 SQL 的地方
MyBatis-Plus 最爽的地方来了:你不用写单表的增删改查 SQL 。只要继承 BaseMapper:
java
@Mapper
public interface H2UserMapper extends BaseMapper<H2UserEntity> {
// 这俩是我自己加的自定义方法
@Select("SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES " +
"WHERE TABLE_NAME = 'STUDY_H2_USER'")
int checkTableExists();
void createTable(); // SQL 写在 XML 里
}
继承了 BaseMapper,你自动就有了 17 个方法可以直接调:
scss
insert() → 插入
deleteById() → 按 id 删
updateById() → 按 id 更新
selectById() → 按 id 查
selectList() → 查列表
selectPage() → 分页查
...
那 createTable() 这种 DDL 写在哪?复杂 SQL 放 XML 文件:
src/main/resources/mapper/H2UserMapper.xml
xml
<mapper namespace="com.wy.study.h2.service.H2UserMapper">
<!-- id 必须跟接口里的方法名一模一样 -->
<update id="createTable">
CREATE TABLE STUDY_H2_USER
(
ID VARCHAR(50) PRIMARY KEY,
USER_NAME VARCHAR(50) NOT NULL,
PASSWORD VARCHAR(100) NOT NULL,
EMAIL VARCHAR(100),
PHONE VARCHAR(20),
CREATE_TIME DATETIME
)
</update>
</mapper>
一个容易踩的坑:
namespace必须写 Mapper 接口的全限定名。写错了 MyBatis 找不到方法跟 XML 对应,直接 500。
第三层:Service --- 业务逻辑的集散地
java
@Service
public class H2UserService
extends ServiceImpl<H2UserMapper, H2UserEntity> // 继承,自动拥有通用方法
implements IService<H2UserEntity> { // 实现,面向接口编程
// 目前业务还简单,所以类体是空的也没关系
// 但这里会是你未来写复杂逻辑的地方:
// - 下单要扣库存 + 加订单(事务)
// - 调用多个 Mapper 组合出一个复杂 DTO
// - 加缓存、发消息队列
}
继承 ServiceImpl 之后,Service 里就能直接调 userService.save(user) / userService.page(page) 这些方法,不用自己再去调 userMapper.insert()。
为啥还要这一层?直接在 Controller 里调 Mapper 不行吗?
可以,但业务一复杂你会后悔。比如一个"下单"操作要:检查库存 → 扣库存 → 写订单 → 发通知 → 记日志,这一串逻辑不可能全堆在 Controller 里。Controller 应该越薄越好。
第四层:Controller --- 对外暴露 HTTP 接口
java
@RestController
@RequestMapping("user")
public class H2UserController {
@Resource
private H2UserService userService;
// http://localhost:9203/h2/user/add
@GetMapping("add")
public void add() {
H2UserEntity user = new H2UserEntity();
user.setUserName("wuyang");
user.setEmail("wuyang@example.com");
user.setPhone("13800000000");
user.setPassword("simple");
user.setCreateTime(LocalDateTime.now());
userService.save(user); // 一行调用,新增就完事了
}
// http://localhost:9203/h2/user/get?pageNum=1&pageSize=10
@GetMapping("get")
public Map<String, Object> get(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
Page<H2UserEntity> page = new Page<>(pageNum, pageSize);
IPage<H2UserEntity> result = userService.page(page); // 一行调用,分页就完事了
Map<String, Object> response = new HashMap<>();
response.put("pageSize", pageSize);
response.put("currentPage", pageNum);
response.put("totalPages", result.getPages());
response.put("totalCount", result.getTotal());
response.put("data", result.getRecords());
return response;
}
}
跑起来看效果:
bash
# 新增一条数据
curl http://localhost:9203/h2/user/add
# 分页查询
curl "http://localhost:9203/h2/user/get?pageNum=1&pageSize=10"
# 返回:
# {
# "pageSize": 10,
# "currentPage": 1,
# "totalPages": 1,
# "totalCount": 1,
# "data": [{"id": "a1b2c3...", "userName": "wuyang", ...}]
# }
5. 自动建表:启动时偷偷做的一件事
我不想手动去 H2 控制台跑 CREATE TABLE,就在启动类里加了一段:
java
@SpringBootApplication
public class H2App {
@Resource
private H2UserMapper userMapper;
public static void main(String[] args) {
SpringApplication.run(H2App.class, args);
// 打几行日志,告诉你控制台地址、接口地址
}
// Spring Boot 启动完之后触发
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
// 先查表有没有
if (userMapper.checkTableExists() == 0) {
userMapper.createTable(); // 没有就建一张
log.info("Table created.");
} else {
log.info("Table already exists.");
}
}
}
用 @EventListener(ApplicationReadyEvent.class) 监听启动完成事件,跑表检查 + 自动建表,这样你第一次启动就直接能用了。
6. 分页插件配置(不配置分页会变成查全量)
MyBatis-Plus 自带分页,但要注册一个拦截器才生效。H2MybatisPlusConfig.java:
java
@Configuration
public class H2MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.setInterceptors(
Collections.singletonList(paginationInnerInterceptor()));
return interceptor;
}
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor() {
PaginationInnerInterceptor p = new PaginationInnerInterceptor();
p.setMaxLimit(-1L); // 不限制每页最大条数
p.setDbType(DbType.H2); // 告诉它是 H2 方言
p.setOptimizeJoin(true); // join 查询优化
return p;
}
}
踩坑提醒:不加这个配置,
userService.page(page)表面能跑,实际发出的 SQL 没有 LIMIT,会把整张表数据都查出来。开发环境感觉不到,生产会炸。
7. 跑起来的全链路
从浏览器点进去到数据存进 H2,一共走了这么多路:
sql
浏览器
↓ GET /h2/user/add
Tomcat(9203 端口)
↓ 路由到
H2UserController.add()
↓ 调用
H2UserService.save(user) ← ServiceImpl 里其实就是调了 Mapper
↓ 调用
H2UserMapper.insert(user) ← BaseMapper 自带的方法
↓ MyBatis 拼 SQL
JDBC
↓
H2 文件数据库(D:/workspace/h2/study-db.mv.db)
分层的好处就在这:每一层只干自己那点事,改 SQL 只动 Mapper,加新业务只动 Service,换数据库(比如换成 MySQL)只改 yml 就行。
8. 踩过的坑清单(希望你跳过)
| 坑 | 症状 | 解决 |
|---|---|---|
| Mapper XML 的 namespace 写错 | 启动 OK,调接口报 Invalid bound statement | 写成接口的全限定名,复制粘贴 |
| 没加分页插件 | page() 不报错但没分页 SQL |
必须注册 MybatisPlusInterceptor |
| 依赖不加版本号但父 pom 没管版本 | 父 pom 没配 dependencyManagement | 在父 pom 里统一版本 |
H2 URL 写成 mem: |
重启数据就没了 | 想要持久化就用 file: |
| Windows 路径反斜杠 | JDBC URL 写 D:\xxx 被当转义字符 |
用正斜杠 D:/xxx |
Mapper 没加 @Mapper |
Spring 找不到这个 Bean,注入报错 | 加 @Mapper 或者启动类加 @MapperScan |
9. 总结一下
- H2:嵌入式数据库,一个 jar 搞定,自带 Web 控制台,学习/Demo 首选。
- 三层结构:Controller(收请求)→ Service(业务逻辑)→ Mapper(写 SQL)。各司其职。
- MyBatis-Plus :继承
BaseMapper就有全套单表 CRUD,继承ServiceImpl业务层也不用自己写。 - XML 写复杂 SQL :简单注解
@Select/@Insert够用,复杂 DDL/多表联查丢 XML 里。
下一篇打算写另外一种持久化方法 JPA