Java学习第40天 - 数据库基础、表设计与 Spring Boot 数据访问入门

一、学习目标

  • 掌握关系型数据库 核心概念:库、表、主键、外键、索引、范式。
  • 熟练编写常用 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 同类记录的集合 ordersusers
行 Row 一条记录 订单 id=1001
列 Column 字段 statuscreated_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
  • 时间统一 DATETIMETIMESTAMP,应用层用 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 已提供:insertdeleteByIdupdateByIdselectByIdselectPage 等。

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=1select 自动带 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,创建订单后查库断言 ordersorder_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。

十四、学习总结

  1. 表设计:主键、外键、索引、金额与时间类型要一次做对。
  2. SQL:CRUD、JOIN、分页、EXPLAIN 是日常必备。
  3. Spring Boot:HikariCP 数据源 + 多环境配置 + 密码外置。
  4. MyBatis Plus:实体注解、BaseMapper、LambdaQueryWrapper、分页插件。
  5. 事务 :写操作放 Service,@Transactional 保证订单与明细同生共死。
  6. 乐观锁@Version 处理并发更新,冲突返回 409 给前端重试。
  7. Flyway:版本化迁移,避免手工改表不一致。
  8. 测试:Repository 集成测 + MockMvc 端到端,延续第39天体系。

十五、实践建议(建议 4-5 小时)

任务 1:建库建表(约 1 小时)

  1. 本地安装 MySQL 8,创建 order_db
  2. 执行本文 usersordersorder_items 建表 SQL。
  3. 手动插入 2 个用户、3 笔订单,练习 JOIN 与分页 SQL。

任务 2:接入 MyBatis Plus(约 1.5 小时)

  1. 在现有 Spring Boot 项目加入依赖与 application-dev.yml
  2. 创建 OrderEntityOrderMapperOrderRepository
  3. 配置分页插件,实现 findPage(page, size, status)

任务 3:打通创建订单 API(约 1 小时)

  1. OrderCommandService.create 事务写入 orders + order_items
  2. POST /api/v1/orders 返回 201,body 含真实数据库 id。
  3. GET /api/v1/orders 从数据库分页查询,不再是 Mock 数据。

任务 4:测试与迁移(约 1 小时)

  1. 用 Flyway 把建表 SQL 迁到 db/migration
  2. OrderRepositoryTest 与一条 MockMvc 创建订单集成测。
  3. 故意制造乐观锁冲突,确认 Service 抛出可映射为 409 的异常。
相关推荐
咖啡八杯1 小时前
GoF设计模式——解释器模式
java·后端·spring·设计模式
掘金码甲哥1 小时前
这块终端神器, 必须吹爆!
后端
糖果店的幽灵2 小时前
【DeepAgents 从入门到精通】Context Management 上下文管理
java·人工智能·后端·spring·中间件·langgraph·deepagents
Csvn2 小时前
📊 SQL 入门 Day 8:集合操作 — 用 SQL 做数学里的"并交差"
后端·sql
码事漫谈5 小时前
告别数据孤岛与AI“水土不服”:金仓多模融合时序库如何让数据真正服务于业务
后端
IT_陈寒5 小时前
Redis的持久化配置把我坑惨了:你以为数据安全了?
前端·人工智能·后端
星栈5 小时前
Node 接口该写同步还是异步?
后端·node.js
红烧大青虫5 小时前
HarmonyOS应用开发实战:小事记 - UIAbility 的冷启动/热启动/后台启动三种场景与 launchParam 解析
后端·华为·harmonyos·鸿蒙系统
码事漫谈5 小时前
AI Token 缓存:命中省 10 倍,不命中白扔钱
后端
65岁退休Coder5 小时前
LangChain v1.3.4 笔记 - 04 Agent 中间件
后端