一、学习目标
- 掌握关系型数据库 核心概念:库、表、主键、外键、索引、范式。
- 熟练编写常用 SQL:CRUD、JOIN、聚合、分页、索引使用注意点。
- 理解 Spring Boot 数据源配置、HikariCP 连接池与多环境配置。
- 会用 MyBatis Plus 完成实体映射、基础 CRUD 与条件构造器查询。
- 理解 事务 的 ACID 语义与
@Transactional的基本用法。 - 能把第38天订单 API、第39天测试,延伸到 真实落库 的完整链路。
- 了解 慢查询排查、N+1 问题与简单表设计规范。
二、关系型数据库核心概念
2.1 为什么要用数据库
- 内存中的对象应用重启即丢失;数据库提供 持久化。
- 多用户并发读写需要 事务 与 锁 保证一致性。
- 通过 索引 与 SQL 在海量数据中快速检索。
2.2 核心术语
| 术语 | 含义 | 示例 |
|---|---|---|
| 数据库 Database | 逻辑容器 | order_db |
| 表 Table | 同类记录的集合 | orders、users |
| 行 Row | 一条记录 | 订单 id=1001 |
| 列 Column | 字段 | status、created_at |
| 主键 Primary Key | 唯一标识一行 | id 自增或雪花 ID |
| 外键 Foreign Key | 引用另一表主键 | orders.user_id -> users.id |
| 索引 Index | 加速查询的数据结构 | idx_orders_user_id |
| 唯一约束 Unique | 列值不可重复 | users.email |
2.3 范式(实用理解)
- 第一范式:列不可再拆(不要把多个手机号塞进一个字段用逗号拼接)。
- 第二范式:非主键列完全依赖主键(订单明细表不要冗余商品名称以外的无关用户字段)。
- 第三范式 :非主键列不依赖其他非主键列(用户姓名放
users,不要散落在每张业务表)。
实际项目:适度反范式 换查询性能(如订单表冗余 user_name 快照),但要有同步策略。
三、MySQL 表设计实战(订单域)
3.1 用户表
sql
CREATE TABLE users (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
email VARCHAR(128) NOT NULL COMMENT '邮箱',
nickname VARCHAR(64) NOT NULL DEFAULT '' COMMENT '昵称',
status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户';
3.2 订单表(衔接第38天 REST 资源)
sql
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '下单用户',
status VARCHAR(32) NOT NULL DEFAULT 'CREATED' COMMENT 'CREATED/PAID/CANCELLED',
total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_orders_user_id (user_id),
KEY idx_orders_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单';
3.3 订单明细表
sql
CREATE TABLE order_items (
id BIGINT NOT NULL AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(12,2) NOT NULL,
PRIMARY KEY (id),
KEY idx_order_items_order_id (order_id),
CONSTRAINT fk_order_items_order FOREIGN KEY (order_id) REFERENCES orders(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细';
3.4 幂等键表(衔接第38天 Idempotency-Key)
sql
CREATE TABLE idempotency_records (
id BIGINT NOT NULL AUTO_INCREMENT,
idempotency_key VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
request_hash VARCHAR(64) NOT NULL,
response_body TEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
expire_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_idem_key_user (idempotency_key, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='幂等记录';
设计要点:
- 金额用
DECIMAL,不用FLOAT/DOUBLE。 - 时间统一
DATETIME或TIMESTAMP,应用层用Instant/LocalDateTime。 - 状态字段用 枚举字符串 或 TINYINT 字典,团队统一即可。
- 高频查询字段建索引,但 索引不是越多越好(写操作变慢)。
四、常用 SQL 语句
4.1 基础 CRUD
sql
-- 插入
INSERT INTO users (email, nickname) VALUES ('a@example.com', 'Alice');
-- 查询
SELECT id, email, nickname FROM users WHERE status = 1 ORDER BY id DESC LIMIT 20 OFFSET 0;
-- 更新
UPDATE orders SET status = 'PAID', version = version + 1 WHERE id = 1001 AND version = 2;
-- 删除(生产多用逻辑删除)
DELETE FROM orders WHERE id = 1001;
4.2 JOIN 查询
sql
SELECT o.id, o.status, o.total_amount, u.email
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE o.status = 'PAID'
ORDER BY o.created_at DESC
LIMIT 20;
4.3 聚合统计
sql
SELECT status, COUNT(*) AS cnt, SUM(total_amount) AS amount_sum
FROM orders
WHERE created_at >= '2026-06-01'
GROUP BY status;
4.4 分页两种写法
sql
-- MySQL 经典
SELECT * FROM orders ORDER BY id DESC LIMIT 20 OFFSET 40;
-- 深分页优化思路:用 id 游标代替大 offset
SELECT * FROM orders WHERE id < 10000 ORDER BY id DESC LIMIT 20;
4.5 EXPLAIN 看执行计划
sql
EXPLAIN SELECT * FROM orders WHERE user_id = 1 AND status = 'PAID';
关注:type 是否 ALL 全表扫描、key 是否命中索引、rows 扫描行数。
五、Spring Boot 数据源配置
5.1 依赖(Maven)
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
5.2 application.yml
yaml
spring:
application:
name: order-service
datasource:
url: jdbc:mysql://localhost:3306/order_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
5.3 多环境配置
yaml
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/order_db_dev?...
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://prod-host:3306/order_db?...
启动:--spring.profiles.active=dev。密码放环境变量,不要写进 Git。
5.4 HikariCP 要点
- Spring Boot 默认连接池就是 HikariCP。
maximum-pool-size不是越大越好,常见公式参考:CPU核数 * 2 + 磁盘数,还要压测调优。- 连接泄漏会导致池耗尽,务必 及时关闭 或使用框架管理连接。
六、MyBatis Plus 实体与 Mapper
6.1 实体类
java
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@TableName("orders")
public class OrderEntity {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String status;
private BigDecimal totalAmount;
@Version
private Integer version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// getter / setter
}
6.2 Mapper 接口
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrderMapper extends BaseMapper<OrderEntity> {
}
BaseMapper 已提供:insert、deleteById、updateById、selectById、selectPage 等。
6.3 启动类扫描
java
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.company.app.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
七、Repository 层与第38天 API 对接
7.1 自定义分页查询
java
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
@Repository
public class OrderRepository extends ServiceImpl<OrderMapper, OrderEntity> {
public Page<OrderEntity> findPage(int page, int size, String status) {
int safeSize = Math.min(Math.max(size, 1), 100);
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(status)) {
wrapper.eq(OrderEntity::getStatus, status);
}
wrapper.orderByDesc(OrderEntity::getCreatedAt);
return page(new Page<>(page + 1, safeSize), wrapper);
}
}
说明:MyBatis Plus 的 Page 页码从 1 开始;若 API 约定从 0 开始,Service 层做 page + 1 转换,并在 OpenAPI 文档写清楚。
7.2 OrderQueryService 改造
java
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.stereotype.Service;
@Service
public class OrderQueryService {
private final OrderRepository orderRepository;
public OrderQueryService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public PageResult<OrderSummaryDTO> findPage(int page, int size, String status) {
Page<OrderEntity> entityPage = orderRepository.findPage(page, size, status);
return PageResult.from(entityPage, this::toSummary);
}
private OrderSummaryDTO toSummary(OrderEntity e) {
OrderSummaryDTO d = new OrderSummaryDTO();
d.setId(e.getId());
d.setStatus(e.getStatus());
d.setTotalAmount(e.getTotalAmount());
return d;
}
}
7.3 统一分页返回 DTO
java
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class PageResult<T> {
private List<T> content;
private long totalElements;
private int totalPages;
private int page;
private int size;
public static <E, T> PageResult<T> from(Page<E> page, Function<E, T> mapper) {
PageResult<T> r = new PageResult<>();
r.content = page.getRecords().stream().map(mapper).collect(Collectors.toList());
r.totalElements = page.getTotal();
r.totalPages = (int) page.getPages();
r.page = (int) page.getCurrent() - 1; // 对外仍从 0 开始
r.size = (int) page.getSize();
return r;
}
// getter / setter
}
八、MyBatis Plus 分页插件配置
java
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
不配此插件时,selectPage 可能无法生成正确 LIMIT 语句。
九、事务与创建订单
9.1 ACID 简述
- 原子性 Atomicity:一组操作要么全成功要么全回滚。
- 一致性 Consistency:数据满足业务约束(金额不为负、外键存在)。
- 隔离性 Isolation:并发事务互不不当干扰。
- 持久性 Durability:提交后数据不丢。
9.2 创建订单(写操作 + 事务)
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
@Service
public class OrderCommandService {
private final OrderRepository orderRepository;
private final OrderItemMapper orderItemMapper;
public OrderCommandService(OrderRepository orderRepository,
OrderItemMapper orderItemMapper) {
this.orderRepository = orderRepository;
this.orderItemMapper = orderItemMapper;
}
@Transactional(rollbackFor = Exception.class)
public OrderDetailDTO create(CreateOrderRequest req, String idempotencyKey) {
OrderEntity order = new OrderEntity();
order.setUserId(req.getUserId());
order.setStatus("CREATED");
order.setTotalAmount(calcTotal(req.getItems()));
orderRepository.save(order);
for (OrderItemRequest item : req.getItems()) {
OrderItemEntity line = new OrderItemEntity();
line.setOrderId(order.getId());
line.setProductId(item.getProductId());
line.setQuantity(item.getQuantity());
line.setUnitPrice(item.getUnitPrice());
orderItemMapper.insert(line);
}
return toDetail(order);
}
private BigDecimal calcTotal(List<OrderItemRequest> items) {
return items.stream()
.map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private OrderDetailDTO toDetail(OrderEntity order) {
OrderDetailDTO dto = new OrderDetailDTO();
dto.setId(order.getId());
dto.setStatus(order.getStatus());
dto.setTotalAmount(order.getTotalAmount());
return dto;
}
}
要点:
@Transactional加在 Service 层,不要加在 Controller。- 默认只对
RuntimeException回滚; checked 异常需rollbackFor = Exception.class。 - 同类内部方法互调 事务可能失效(自调用不走代理),需提取到另一个 Bean 或注入自身代理。
9.3 乐观锁更新示例
java
@Transactional
public void payOrder(Long orderId) {
OrderEntity order = orderRepository.getById(orderId);
if (order == null) {
throw new IllegalArgumentException("订单不存在");
}
if (!"CREATED".equals(order.getStatus())) {
throw new IllegalStateException("订单状态不允许支付");
}
order.setStatus("PAID");
boolean ok = orderRepository.updateById(order); // version 字段自动参与 WHERE
if (!ok) {
throw new IllegalStateException("并发更新冲突,请重试");
}
}
@Version 会在 UPDATE 时带 WHERE version = ?,更新成功后 version + 1,冲突时返回 false。
十、条件构造器进阶
10.1 LambdaQueryWrapper 常用写法
java
LambdaQueryWrapper<OrderEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderEntity::getUserId, userId)
.in(OrderEntity::getStatus, List.of("CREATED", "PAID"))
.ge(OrderEntity::getCreatedAt, startTime)
.le(OrderEntity::getTotalAmount, maxAmount)
.orderByDesc(OrderEntity::getCreatedAt);
List<OrderEntity> list = orderRepository.list(wrapper);
10.2 动态 SQL 替代手写 XML
复杂报表仍可写 @Select 或 XML Mapper;日常 CRUD 优先 Wrapper,减少 XML 维护成本。
10.3 逻辑删除
实体加字段:
java
import com.baomidou.mybatisplus.annotation.TableLogic;
@TableLogic
private Integer deleted;
调用 removeById 时执行 UPDATE ... SET deleted=1,select 自动带 deleted=0 条件。
十一、数据库迁移(Flyway 入门)
11.1 依赖
xml
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
11.2 迁移脚本命名
路径:src/main/resources/db/migration/
markdown
V1__create_users.sql
V2__create_orders.sql
V3__create_order_items.sql
启动时自动执行未跑过的版本,团队环境表结构一致。
11.3 配置
yaml
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
十二、与第39天测试结合
12.1 使用 H2 内存库跑 Repository 测试
java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderRepositoryTest {
@Autowired
private OrderRepository orderRepository;
@Test
void saveAndFind() {
OrderEntity order = new OrderEntity();
order.setUserId(1L);
order.setStatus("CREATED");
order.setTotalAmount(new java.math.BigDecimal("99.00"));
orderRepository.save(order);
OrderEntity found = orderRepository.getById(order.getId());
assertThat(found.getStatus()).isEqualTo("CREATED");
}
}
application-test.yml 指向 H2 或 Testcontainers MySQL,与第39天思路一致。
12.2 MockMvc + 真实 Service 切片
集成测可 @SpringBootTest + @AutoConfigureMockMvc,创建订单后查库断言 orders、order_items 各有记录。
十三、常见问题与最佳实践
13.1 N+1 查询
错误做法:查 20 个订单,循环里每个订单再查一次用户。
java
// 反例
for (OrderEntity o : orders) {
UserEntity u = userMapper.selectById(o.getUserId());
}
改进:一次 JOIN 或 IN 批量查用户再 Map 组装。
13.2 慢查询
- 开启慢查询日志:
long_query_time。 - 避免
SELECT *在大表上无索引条件扫描。 - 分页深翻页考虑游标或搜索引擎。
13.3 字段类型与 Java 类型映射
| MySQL | Java 推荐 |
|---|---|
| BIGINT | Long |
| VARCHAR | String |
| DECIMAL | BigDecimal |
| DATETIME | LocalDateTime |
| TINYINT(1) 布尔语义 | Boolean 或 Integer |
13.4 连接与字符集
- 统一
utf8mb4,支持 emoji。 - 时区在 JDBC URL 与 JVM 保持一致。
13.5 安全
- 应用账号 最小权限(不要给 DROP 权限)。
- 所有外部输入走 参数绑定,禁止字符串拼接 SQL。
十四、学习总结
- 表设计:主键、外键、索引、金额与时间类型要一次做对。
- SQL:CRUD、JOIN、分页、EXPLAIN 是日常必备。
- Spring Boot:HikariCP 数据源 + 多环境配置 + 密码外置。
- MyBatis Plus:实体注解、BaseMapper、LambdaQueryWrapper、分页插件。
- 事务 :写操作放 Service,
@Transactional保证订单与明细同生共死。 - 乐观锁 :
@Version处理并发更新,冲突返回 409 给前端重试。 - Flyway:版本化迁移,避免手工改表不一致。
- 测试:Repository 集成测 + MockMvc 端到端,延续第39天体系。
十五、实践建议(建议 4-5 小时)
任务 1:建库建表(约 1 小时)
- 本地安装 MySQL 8,创建
order_db。 - 执行本文
users、orders、order_items建表 SQL。 - 手动插入 2 个用户、3 笔订单,练习 JOIN 与分页 SQL。
任务 2:接入 MyBatis Plus(约 1.5 小时)
- 在现有 Spring Boot 项目加入依赖与
application-dev.yml。 - 创建
OrderEntity、OrderMapper、OrderRepository。 - 配置分页插件,实现
findPage(page, size, status)。
任务 3:打通创建订单 API(约 1 小时)
OrderCommandService.create事务写入orders+order_items。POST /api/v1/orders返回 201,body 含真实数据库 id。GET /api/v1/orders从数据库分页查询,不再是 Mock 数据。
任务 4:测试与迁移(约 1 小时)
- 用 Flyway 把建表 SQL 迁到
db/migration。 - 写
OrderRepositoryTest与一条 MockMvc 创建订单集成测。 - 故意制造乐观锁冲突,确认 Service 抛出可映射为 409 的异常。