背景:13张表是怎么把人逼疯的?
做毕设时,导师一句“比赛日程系统,功能要全”,听起来简单,一拆表就傻眼:
赛事、项目、队伍、队员、裁判、场地、日程、积分、公告、报名、轮空、消息、日志——13张表瞬间到位。
大多数同学第一反应是“堆字段”,外键到处拉,结果:
- 循环引用:A 表删不掉,B 表查不动,C 表一更新,D 表全锁行
- N+1 查询:赛程列表接口,for 循环里顺手
getTeam().getPlayers(),生产环境 200 ms 能飙到 5 s - 事务边界模糊:一个“生成日程”按钮,里层调了 4 个 Service,外层没加
@Transactional,回滚只回一半,数据直接乱套
毕设答辩前夜,一边改 SQL 一边哭的场景,懂的都懂。
技术选型:MyBatis 还是 JPA?
多表关联场景,两条路线都能走,但踩坑姿势不同:
| 维度 | MyBatis | Spring Data JPA |
|---|---|---|
| SQL 可控 | 手写 XML,复杂 Join 一目了然 | 靠@Query或方法名,调试要开 SQL 日志 |
| 缓存/懒加载 | 无默认,自己写 | 一级缓存+懒加载,一不小心 N+1 |
| 分页 | 手写 count 查询 | Pageable一行代码搞定 |
| 代码量 | 每个表 4 个文件(XML+Mapper) | 注解实体+Repository 接口即可 |
| 学习曲线 | 低,但后期 SQL 爆炸 | 前期爽,后期要懂实体状态、flush、detach |
结论:毕设周期短、表关联深、导师要求“规范”,JPA 更香;若后续要极端优化,可再局部写 SQL,两者混用 Spring 也支持。
核心模型:13 张表的关系长这样
先放 ER 简图,混个眼熟:
关键关系一句话总结:
赛事 1-N 项目,项目 1-N 日程,日程 N-N 队伍(中间表 schedule_team),日程 1-1 场地,队伍 N-N 队员(中间表 team_player)……
下面用 JPA 注解落地,只贴核心片段,能跑即可。
1. 赛事实体
@Entity @Table(name = "t_event") public class Event { @Id @GeneratedValue private Long id; private String name; private LocalDate startDate; private LocalDate endDate; @OneToMany(mappedBy = "event", cascade = CascadeType.ALL, orphanRemoval = true) private List<Item> items = new ArrayList<>(); }2. 项目实体
@Entity @Table(name = "t_item") public class Item { @Id @GeneratedValue private Long id; private String itemName; // 如“男子篮球” @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "event_id") private Event event; @OneToMany(mappedBy = "item") private List<Schedule> schedules = new ArrayList<>(); }3. 日程实体(最复杂)
@Entity @Table(name = "t_schedule") public class Schedule { @Id @GeneratedValue private Long id; private LocalDateTime startTime; private LocalDateTime endTime; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "item_id") private Item item; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "venue_id") private Venue venue; @ManyToMany @JoinTable(name = "schedule_team", joinColumns = @JoinColumn(name = "schedule_id"), inverseJoinColumns = @JoinColumn(name = "team_id")) private Set<Team> teams = new HashSet<>(); }注意:
- 全部用
LAZY,只在 Service 里写JOIN FETCH解决 N+1 - 多对多再建中间表,避免双向
List造成无限递归 JSON - 实体类绝不暴露
toString()含关联字段,栈溢出警告
DTO 转换:不让 Entity 裸奔
Controller 直接返回 Entity,会:
- 把懒加载代理拖进 Jackson,秒抛
LazyInitializationException - 暴露内部字段,循环引用 500
套路:MapStruct 一行注解搞定
@Mapper(componentModel = "spring") public interface ScheduleMapper { ScheduleDto toDto(Schedule s); List<ScheduleDto> toDto(List<Schedule> list); }DTO 里只留venueName、teamNames等扁平字段,前端开心,后端安全。
事务与性能:生成日程的正确姿势
需求:根据“项目+轮次”一键生成 48 条日程,涉及 4 张表写操作。
下面代码演示“事务+批量+幂等”三位一体:
@Service @RequiredArgsConstructor public class ScheduleGenService { private final ScheduleRepository scheduleRepository; private final VenueRepository venueRepository; private final IdGenerator idGenerator; // 雪花算法,保证幂等 @Transactional // 1. 事务边界 public List<Schedule> generate(Item item, int rounds){ // 2. 幂等:先查重 if(scheduleRepository.countByItemId(item.getId()) > 0){ throw new BizException("日程已存在,禁止重复生成"); } List<Venue> venues = venueRepository.findAllBySport(item.getSport()); List<Schedule> batch = new ArrayList<>(rounds * venues.size()); for(int i=0; i<rounds; i++){ for(Venue v : venues){ Schedule s = new Schedule(); s.setId(idGenerator.nextId()); // 3. 提前设主键,批量插入可走 JDBC 批处理 s.setItem(item); s.setVenue(v); s.setStartTime(calcStartTime(i, v)); batch.add(s); } } // 4. 批量保存 scheduleRepository.saveAll(batch); return batch; } }要点:
- 事务只加在写服务,读接口不加,减少锁范围
- 提前分配主键,MySQL 批插入
rewriteBatchedStatements=true秒级 1w+ - 接口幂等靠业务键(itemId+round),不是单纯依赖数据库唯一索引
安全三板斧
- SQL 注入:JPA 只要用方法名或参数绑定
:xxx,基本免疫;手写@Query也杜绝拼接 - 批量更新:MySQL 的
on duplicate key update配合JpaRepository的@Modifying注意分片,一次 500 条最稳 - 接口幂等:除业务判断外,前端点“生成”按钮后置灰+UUID 令牌,后端用 Redis
SETNX做双重校验,防重放
生产环境避坑清单
- 双向关联绝不写
cascade = CascadeType.ALL了事,级联删除一跑,半库数据蒸发 - 枚举字段统一
@Enumerated(STRING),防止序号移位全崩 - 逻辑删除加
deleted字段,手写WHERE deleted = 0拦截,JPA 2 级缓存不会自动过滤,记得配@Where - 多对多中间表别加业务字段,一旦加字段就升成实体,否则后续补字段全表锁
- 生产环境打开
spring.jpa.show-sql=false,用 datasource-proxy 慢查询日志替代,别让控制台刷屏把性能吃光
可扩展思考:赛制说改就改,怎么办?
当前模型只支持“单循环+积分制”,如果导师突然说“加淘汰赛、加复活赛”,硬编码if/else直接爆炸。
留给读者的作业:
- 把“赛制”抽象成 Strategy 接口,提供
ScheduleStrategy.generate() - 项目表加
strategy_type字段,与 Spring 的@Strategy自动装配联动 - 新建模块
schedule-strategy-elimination,遵循相同 DTO,做到“热插拔”
先动手把生成日程 Service 拆成“规则引擎+执行器”,跑通单元测试,你就能在答辩时自信回答“系统支持任意赛制”——导师微笑,你稳过。
把 13 张表拆干净、事务扣稳、接口拍平,比赛日程系统就不再是“毕设噩梦”,而是简历上能吹的亮点。
代码给你了,坑也标好了,下一步要不要把“淘汰赛”模块真正写出来,就看你想不想让自己的毕设从 80 分跳到 95 分。