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 的异常。
相关推荐
iOS开发上架哦3 小时前
Jenkins 自动上传 IPA 到 App Store 把发布步骤融入 CI/CD
后端·ios
Java内核笔记3 小时前
SpringSecurity源码解析三:FilterChainProxy核心代理:智能路由、防火墙与请求分发
后端
神奇小汤圆3 小时前
告别“大泥球”:我在 Spring Boot 单体架构中实践的模块化隔离
后端
长大19883 小时前
Python 新手最容易踩的 10 个语法坑
后端
二月龙3 小时前
Python 迭代器与生成器精讲:大幅降低内存占用
后端
AINative软件工程4 小时前
Tool Schema 写得好,模型少出错:5 个工程师必知的设计原则
后端·openai
AINative软件工程4 小时前
AI 写的代码,Review 要怎么改?我们团队的 15 条 PR 检查清单
后端·openai
武子康4 小时前
Java-21 深入浅出 MyBatis 手写ORM框架2 手写Resources、MappedStatment、XMLBuilder等
java·后端
techdashen4 小时前
在 Fly.io 上使用 Rust 构建远程开发环境:从 Tokio 到 eBPF
开发语言·后端·rust