尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot + MyBatis-Plus多表关联实战:海贼王角色库重构

Spring Boot + MyBatis-Plus多表关联实战:海贼王角色库重构 1. 项目背景为什么叫 Heart Operation先说项目名。One Piece: Heart Operation是一个以《海贼王》世界观为素材的 Web 实战项目系列。“Heart Operation”直译是“心脏手术”放在项目里有两层意思第一这是围绕核心数据模块做的一次重构就像给系统做手术第二动漫角色“特拉法尔加·罗”的招牌技能也与心脏有关算是彩蛋式的命名。所以这个系列每期都围绕一个核心模块展开什么时候该拆表、什么时候该加缓存、什么时候该把大接口拆成细粒度接口这些都属于“手术”范畴。第 07 期标题是Set Sailビッグ・マム海賊団直译是“起航BIG MOM 海贼团”。放在业务上理解就是本期要把BIG MOM海贼团这个数据域正式接入系统。以前系统里只有“角色单表”的简易设计海贼团名称、角色职位、能力描述全部堆在一个表里查询时既不好筛选也不方便维护。这一期的目标就是完成一次典型的“单表大字段 → 多表关联”的模型重构并让数据成功入库、可在页面上展示、可通过接口查询。本文适合下面几类读者正在学 Spring Boot MyBatis-Plus MySQL 的后端初学者想找一个完整项目练手、但不想只写“增删改查”的同学想在项目中引入分页、统一返回、异常处理、数据初始化等工程化思路的开发者对动漫题材开发项目感兴趣、想做一个资料站或角色库的人。读完本文后你将掌握多表数据模型设计的基本思路Spring Boot 整合 MyBatis-Plus 的完整写法分页查询、详情聚合、数据初始化等常见功能实现使用 Docker Compose 将前后端项目一键启动常见报错排查和工程化最佳实践。2. 环境准备与工程结构2.1 开发环境我的建议是使用以下环境版本可根据你本机实际安装情况调整重点是思路要通软件版本建议说明JDK1.8 或 11Spring Boot 2.7.x 兼容性较好Maven3.6用于项目构建和依赖管理Spring Boot2.7.x本文示例以 2.7.18 为例MyBatis-Plus3.5.x在 MyBatis 基础上简化 CRUDMySQL5.7 或 8.0数据库Node.js16运行前端 Vue 项目IntelliJ IDEA2021开发 IDE也可以使用 Eclipse如果你使用的是 Spring Boot 3.x需要注意一个细节javax.*包名变成了jakarta.*代码中涉及的javax.validation、javax.annotation等需要同步调整。本文示例以 Spring Boot 2.7.x 为准。2.2 项目目录结构整个项目分为backend和frontend两个子目录部署时通过 Docker Compose 统一编排。one-piece-heart-operation ├── backend │ ├── pom.xml │ └── src/main │ ├── java/com/onepiece/heartoperation │ │ ├── HeartOperationApplication.java │ │ ├── config/MybatisPlusConfig.java │ │ ├── common/Result.java │ │ ├── common/GlobalExceptionHandler.java │ │ ├── common/BizException.java │ │ ├── controller/PirateCrewController.java │ │ ├── service/PirateCrewService.java │ │ ├── mapper/PirateCrewMapper.java │ │ ├── mapper/CharacterInfoMapper.java │ │ ├── mapper/AbilityInfoMapper.java │ │ ├── entity/PirateCrew.java │ │ ├── entity/CharacterInfo.java │ │ ├── entity/AbilityInfo.java │ │ ├── vo/CharacterDetailVO.java │ │ └── runner/DataInitializer.java │ └── resources/application.yml ├── frontend │ └── src │ ├── api/crew.js │ └── views/CrewPage.vue ├── mysql/init │ └── 01_init.sql ├── docker-compose.yml └── Dockerfile后面所有的代码片段都会对应到这个目录结构里方便你对照复制。3. 数据模型设计三表关联取代单表大字段3.1 重构前后对比很多初学者在做资料站时习惯把“角色”的表设计成下面这样字段类型说明idbigint主键namevarchar角色名crew_namevarchar海贼团名称role_typevarchar职位abilityvarchar能力描述bountydecimal赏金descriptionvarchar简介这张表的问题非常明显crew_name在多个角色之间重复存储如果海贼团改名需要批量 UPDATEability是字符串后续如果要按能力类型筛选SQL 会很痛苦同一个角色拥有多个能力时只能写成能力1、能力2无法结构化查询想展示“海贼团详情页 成员列表”时数据建模不够清晰。第 07 期的“心脏手术”就是把这张“大而全”的单表拆成pirate_crew、character_info、ability_info三张表。拆表之后数据关系变得非常直观pirate_crew是海贼团主表character_info是角色表通过crew_id关联海贼团ability_info是能力表通过character_id关联角色。这样设计后新增一个“海贼团维度”的查询就非常自然先查海贼团再根据crew_id查成员列表最后按character_id查能力列表。这也是多表关联最常见的落地方案。3.2 建表 SQL在mysql/init/01_init.sql中写入以下 SQL。这个文件会被 Docker Compose 挂载到 MySQL 容器的初始化目录首次启动容器时自动执行。CREATE DATABASE IF NOT EXISTS one_piece DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE one_piece; CREATE TABLE pirate_crew ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, crew_name VARCHAR(64) NOT NULL COMMENT 海贼团名称, captain_name VARCHAR(64) COMMENT 船长名称, region VARCHAR(128) COMMENT 活动区域, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_crew_name (crew_name) ) ENGINE InnoDB COMMENT 海贼团表; CREATE TABLE character_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, crew_id BIGINT NOT NULL COMMENT 所属海贼团id, char_name VARCHAR(64) NOT NULL COMMENT 角色名, role_type VARCHAR(64) COMMENT 职位, bounty DECIMAL(15, 2) COMMENT 悬赏金, description VARCHAR(512) COMMENT 角色简介, sort_order INT NOT NULL DEFAULT 0 COMMENT 排序字段, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常0停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_crew_id (crew_id), KEY idx_char_name (char_name) ) ENGINE InnoDB COMMENT 角色信息表; CREATE TABLE ability_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, character_id BIGINT NOT NULL COMMENT 角色id, ability_type VARCHAR(64) COMMENT 能力类型恶魔果实/霸气/体术, ability_name VARCHAR(128) COMMENT 能力名称, remark VARCHAR(256) COMMENT 备注, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_character_id (character_id) ) ENGINE InnoDB COMMENT 角色能力表;3.3 设计要点说明这里解释几个容易被忽略的点。第一为什么不用数据库外键单看表关系可以用FOREIGN KEY去约束character_info.crew_id指向pirate_crew.id。但在实际互联网项目中很少会真的建物理外键。原因在于物理外键会在每次 INSERT、UPDATE、DELETE 时产生额外的检查开销而且在分库分表、数据迁移、幂等重放等场景下容易成为瓶颈。更普遍的做法是在应用层通过 Service 保证数据一致性表结构只保留普通索引。第二为什么保留status字段status是软删除/上下架的常见写法。资料站里的角色资料可能存在误录入或者角色后续有状态变化。直接用DELETE物理删除一旦误删数据就找不回来了。用status0表示逻辑停用要比物理删除安全得多。如果业务需要真正的“删除”也可以把字段命名为deleted配合 MyBatis-Plus 的TableLogic注解实现自动逻辑删除。第三为什么表和字段名不直接叫charactercharacter在 SQL 里是保留字直接作为表名会引发语法问题。另外description、status、order等词在部分数据库或 ORM 框架中也可能存在歧义所以我们在设计时给表名加了后缀比如character_info、ability_info。这不是必须的但能减少很多隐藏问题。4. 后端开发核心模块“心脏手术”4.1 创建 Spring Boot 工程并添加依赖后端使用 Maven 构建。在backend/pom.xml中引入 Spring Boot Web、MyBatis-Plus、MySQL 驱动、Lombok 和参数校验依赖。?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.onepiece/groupId artifactIdheart-operation/artifactId version0.0.1-SNAPSHOT/version nameheart-operation/name descriptionOne Piece Heart Operation project/description properties java.version1.8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin /plugins /build /project这里用spring-boot-starter-parent统一管理依赖版本所以mysql-connector-java不需要显式写版本号。MyBatis-Plus 因为不在 Spring Boot 官方依赖管理范围内所以单独指定版本。4.2 配置文件在backend/src/main/resources/application.yml中配置数据源和 MyBatis-Plus。server: port: 8080 spring: application: name: heart-operation datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:one_piece}?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: ${DB_USERNAME:root} password: ${DB_PASSWORD:root123} mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto配置说明数据库连接串里的DB_HOST、DB_PORT等使用环境变量方式读取本地运行时默认连localhost:3306Docker 部署时会覆盖成mysql容器名map-underscore-to-camel-case: true表示把数据库下划线字段自动映射到 Java 驼峰属性log-impl设置为StdOutImpl后控制台会打印 SQL 日志开发调试非常方便。4.3 实体类与 Mapper创建实体类对应三张表。// 文件路径src/main/java/com/onepiece/heartoperation/entity/PirateCrew.java package com.onepiece.heartoperation.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(pirate_crew) public class PirateCrew { TableId(type IdType.AUTO) private Long id; private String crewName; private String captainName; private String region; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }// 文件路径src/main/java/com/onepiece/heartoperation/entity/CharacterInfo.java package com.onepiece.heartoperation.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data TableName(character_info) public class CharacterInfo { TableId(type IdType.AUTO) private Long id; private Long crewId; private String charName; private String roleType; private BigDecimal bounty; private String description; private Integer sortOrder; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }// 文件路径src/main/java/com/onepiece/heartoperation/entity/AbilityInfo.java package com.onepiece.heartoperation.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(ability_info) public class AbilityInfo { TableId(type IdType.AUTO) private Long id; private Long characterId; private String abilityType; private String abilityName; private String remark; private LocalDateTime createTime; }Mapper 接口非常简单继承BaseMapperT之后MyBatis-Plus 会自动提供单表 CRUD 方法。以PirateCrewMapper为例// 文件路径src/main/java/com/onepiece/heartoperation/mapper/PirateCrewMapper.java package com.onepiece.heartoperation.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.onepiece.heartoperation.entity.PirateCrew; import org.apache.ibatis.annotations.Mapper; Mapper public interface PirateCrewMapper extends BaseMapperPirateCrew { }同理再创建CharacterInfoMapper和AbilityInfoMapper结构完全一样只是泛型分别对应CharacterInfo和AbilityInfo。4.4 统一返回与全局异常处理
返回列表