Agent Scope Java 2.x 系列【28】Harness:MysqlSkillRepository 源码解读
·
文章目录
1. 概述
MysqlSkillRepository 是 AgentSkillRepository 的 MySQL 持久化实现。
整体定位:
- 分两张表存储:技能主表 + 资源文件表;
- 兼容新旧表结构:自动检测是否存在
metadata_json字段,做新旧版本平滑兼容; - 支持事务、批量插入、防
SQL注入、读写权限控制; - 负责把
Skill完整元数据 + 所有资源文件持久入库,作为技能市场的后端存储。

2. 表结构设计
2.1 主表 agentscope_skills
存储 Skill 核心元信息与完整元 JSON :
| 字段 | 作用 |
|---|---|
| id | 自增主键 |
| name | 唯一技能名,作为业务主键 |
| description | 简介(注入到system prompt的available_skills) |
| skill_content | SKILL.md正文内容 |
| source | 来源标识,用来计算缓存路径<source> |
| metadata_json | 完整元数据树(新版本扩展字段) |
| created_at/updated_at | 自动时间戳 |
建表语句:
CREATE TABLE IF NOT EXISTS agentscope_skills (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT NOT NULL,
skill_content LONGTEXT NOT NULL,
source VARCHAR(255) NOT NULL,
metadata_json LONGTEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
2.2 资源附表 agentscope_skill_resources
存储 skill 目录下所有资源(style-guide.md、脚本文件等):
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | NOT NULL | 关联技能主表主键ID,外键 |
| resource_path | VARCHAR(500) | NOT NULL | 资源文件相对路径,如 scripts/run.sh、SKILL.md |
| resource_content | LONGTEXT | NOT NULL | 文件完整文本内容 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
建表语句:
CREATE TABLE IF NOT EXISTS agentscope_skills (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
description TEXT NOT NULL,
skill_content LONGTEXT NOT NULL,
source VARCHAR(255) NOT NULL,
metadata_json LONGTEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
索引与外键:
- 联合主键:
PRIMARY KEY (id, resource_path) - 外键约束:
FOREIGN KEY (id) REFERENCES agentscope_skills(id) ON DELETE CASCADE
删除技能主数据时,自动清理全部关联资源记录。
3. 构造函数
核心初始化流程:
- 入参校验
- 非空校验
DataSource; - 用正则校验库名、表名,防止标识符
SQL注入(表名无法预编译,只能白名单正则)。
- 非空校验
- 库&表自动初始化 / 存在性校验
createIfNotExist=true:执行建库、建表语句,字符集强制utf8mb4;createIfNotExist=false:去INFORMATION_SCHEMA校验库和表必须已存在,否则抛异常。
- 探测表结构版本
执行元信息查询,判断是否有metadata_json列,结果缓存到成员变量。 - 输出初始化日志,标记当前库、两张表名称。
构造函数源码:
private MysqlSkillRepository(
DataSource dataSource,
String databaseName,
String skillsTableName,
String resourcesTableName,
boolean createIfNotExist,
boolean writeable) {
// 数据源非空校验
if (dataSource == null) {
throw new IllegalArgumentException("DataSource cannot be null");
}
this.dataSource = dataSource;
this.writeable = writeable;
// 为空则使用默认库名、表名,并去除首尾空格
this.databaseName =
(databaseName == null || databaseName.trim().isEmpty())
? DEFAULT_DATABASE_NAME
: databaseName.trim();
this.skillsTableName =
(skillsTableName == null || skillsTableName.trim().isEmpty())
? DEFAULT_SKILLS_TABLE_NAME
: skillsTableName.trim();
this.resourcesTableName =
(resourcesTableName == null || resourcesTableName.trim().isEmpty())
? DEFAULT_RESOURCES_TABLE_NAME
: resourcesTableName.trim();
// 校验库名、表名字符串,防止标识符SQL注入
validateIdentifier(this.databaseName, "Database name");
validateIdentifier(this.skillsTableName, "Skills table name");
validateIdentifier(this.resourcesTableName, "Resources table name");
if (createIfNotExist) {
// 自动创建库与数据表
createDatabaseIfNotExist();
createTablesIfNotExist();
} else {
// 校验库和数据表必须已经存在
verifyDatabaseExists();
verifyTablesExist();
}
// 自动探测是否包含metadata_json扩展字段,用于新旧表结构兼容
this.metadataJsonColumnSupported = detectMetadataJsonColumnSupport();
logger.info(
"MysqlSkillRepository initialized with database: {}, skills table: {},"
+ " resources table: {}",
this.databaseName,
this.skillsTableName,
this.resourcesTableName);
}
4. 方法介绍
4.1 查询能力
4.1.1 getSkill(name):单条技能加载
执行两步查询:
- 根据技能
name查主表,拿到自增id+ 内容 +metadata_json; - 用
skill.id关联查询资源表,把所有resource_path -> content读到Map; - 调用
buildSkill组装对象:- 有
metadata_json:JSON反序列化还原完整元数据,再强制用数据库内的name、description覆盖(以库内字段为准,防止JSON脏数据); - 无此字段:只保留
name+description核心字段。
- 有
源码说明:
/**
* 根据技能名称查询单个Skill完整信息,包含主配置与所有附属资源文件。
* <p>
* 执行逻辑:
* 1. 校验技能名称合法性;
* 2. 动态拼接SQL,表结构支持metadata_json字段则查询该扩展列;
* 3. 先查询技能主表,获取主键ID、基础信息与元数据JSON;
* 4. 通过技能ID关联查询资源附表,把文件路径与文件内容加载到Map;
* 5. 组装为完整AgentSkill对象返回。
*
* @param name 技能唯一名称
* @return 封装完成的AgentSkill实例
* @throws IllegalArgumentException 技能名称非法 / 技能不存在时抛出
* @throws RuntimeException 数据库查询异常统一包装抛出
*/
@Override
public AgentSkill getSkill(String name) {
// 校验技能名称格式,拦截非法字符与超长内容
validateSkillName(name);
// 动态SQL:存在metadata_json字段则追加该列,兼容新旧表结构
String selectSkillSql =
"SELECT id, name, description, skill_content, source"
+ (metadataJsonColumnSupported ? ", metadata_json" : "")
+ " FROM "
+ getFullTableName(skillsTableName)
+ " WHERE name = ?";
// 根据技能主键ID查询所有附属资源文件
String selectResourcesSql =
"SELECT resource_path, resource_content FROM "
+ getFullTableName(resourcesTableName)
+ " WHERE id = ?";
// try-with-resources自动关闭连接、语句、结果集,避免资源泄漏
try (Connection conn = dataSource.getConnection()) {
long skillId;
String description;
String skillContent;
String source;
String metadataJson = null;
// 查询技能主记录
try (PreparedStatement stmt = conn.prepareStatement(selectSkillSql)) {
stmt.setString(1, name);
try (ResultSet rs = stmt.executeQuery()) {
if (!rs.next()) {
throw new IllegalArgumentException("Skill not found: " + name);
}
skillId = rs.getLong("id");
description = rs.getString("description");
skillContent = rs.getString("skill_content");
source = rs.getString("source");
// 仅在新版表结构下读取扩展元数据
if (metadataJsonColumnSupported) {
metadataJson = rs.getString("metadata_json");
}
}
}
// 加载当前Skill下全部资源文件(脚本、文档、SKILL.md等)
Map<String, String> resources = new HashMap<>();
try (PreparedStatement stmt = conn.prepareStatement(selectResourcesSql)) {
stmt.setLong(1, skillId);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
String path = rs.getString("resource_path");
String content = rs.getString("resource_content");
resources.put(path, content);
}
}
}
// 把数据库记录组装为业务Skill对象
return buildSkill(name, description, skillContent, source, metadataJson, resources);
} catch (SQLException e) {
throw new RuntimeException("Failed to load skill: " + name, e);
}
}
4.1.2 getAllSkills():全量拉取所有技能
做分两次全量查询,避免 N+1:
- 一次性查出全部主表数据,存入
Map<skillId, 临时记录>; - 一次性查出全部资源记录,根据
skillId回填到对应记录的resources; - 循环批量构建
AgentSkill对象。
源码说明:
/**
* 一次性查询库中全部技能,包含技能主体与所有资源文件,避免N+1查询性能问题。
* <p>
* 优化方案:分两次全表查询
* <ol>
* <li>批量查询所有技能主表数据,存入Map,以技能ID作为Key;</li>
* <li>批量查询全部资源附表数据,通过技能ID回填到对应技能的资源集合;</li>
* <li>遍历临时记录,统一组装成AgentSkill对象集合;</li>
* <li>对于不存在主记录的孤立资源,打印告警日志。</li>
* </ol>
* 自动兼容新旧表结构,存在metadata_json字段时读取扩展元数据。
*
* @return 全部技能对象列表
* @throws RuntimeException 数据库异常统一包装抛出
*/
@Override
public List<AgentSkill> getAllSkills() {
// 动态拼接查询列,新版表追加metadata_json字段
String selectAllSkillsSql =
"SELECT id, name, description, skill_content, source"
+ (metadataJsonColumnSupported ? ", metadata_json" : "")
+ " FROM "
+ getFullTableName(skillsTableName)
+ " ORDER BY name";
// 一次性查出所有资源文件,后续按ID关联
String selectAllResourcesSql =
"SELECT id, resource_path, resource_content FROM "
+ getFullTableName(resourcesTableName);
try (Connection conn = dataSource.getConnection()) {
// 临时存储所有技能主记录,key = 技能主键ID
Map<Long, LoadedSkillRecord> skillRecords = new HashMap<>();
// 第一步:加载全部技能主体信息
try (PreparedStatement stmt = conn.prepareStatement(selectAllSkillsSql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
long skillId = rs.getLong("id");
String name = rs.getString("name");
String description = rs.getString("description");
String skillContent = rs.getString("skill_content");
String source = rs.getString("source");
String metadataJson =
metadataJsonColumnSupported ? rs.getString("metadata_json") : null;
skillRecords.put(
skillId,
new LoadedSkillRecord(
name, description, skillContent, source, metadataJson));
}
}
// 第二步:加载全部资源,根据skillId回填到对应技能
try (PreparedStatement stmt = conn.prepareStatement(selectAllResourcesSql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
long skillId = rs.getLong("id");
String resourcePath = rs.getString("resource_path");
String resourceContent = rs.getString("resource_content");
LoadedSkillRecord record = skillRecords.get(skillId);
if (record != null) {
record.resources.put(resourcePath, resourceContent);
} else {
// 存在无主资源,打印警告
logger.warn("Found orphaned resource for non-existent id: {}", skillId);
}
}
}
// 统一构建AgentSkill实例
List<AgentSkill> skills = new ArrayList<>(skillRecords.size());
for (LoadedSkillRecord record : skillRecords.values()) {
try {
skills.add(
buildSkill(
record.name,
record.description,
record.skillContent,
record.source,
record.metadataJson,
record.resources));
} catch (Exception e) {
logger.warn("Failed to build skill: {}", e.getMessage(), e);
}
}
return skills;
} catch (SQLException e) {
throw new RuntimeException("Failed to load all skills", e);
}
}
4.1.3 getAllSkillNames()
只查询 name 字段,用于生成 <available_skills> 精简元列表,轻量化获取技能目录。
源码说明:
/**
* 查询所有技能名称,仅拉取name字段,用于组装可用技能清单,轻量化加载。
* <p>
* 该接口主要用于生成System Prompt中的available_skills元数据列表,只查询名称不加载完整内容,减少IO开销。
*
* @return 按名称升序排列的技能名称集合
* @throws RuntimeException 数据库查询异常时抛出
*/
@Override
public List<String> getAllSkillNames() {
// 只查询技能名称,按名称排序
String selectSql =
"SELECT name FROM " + getFullTableName(skillsTableName) + " ORDER BY name";
List<String> skillNames = new ArrayList<>();
// try-with-resources自动释放数据库资源
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(selectSql);
ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
skillNames.add(rs.getString("name"));
}
} catch (SQLException e) {
throw new RuntimeException("Failed to list skill names", e);
}
return skillNames;
}
4.1.4 skillExists()
预校验技能是否存在,用于 save 时冲突判断。
源码说明:
/**
* 校验指定名称的技能是否已存在。
* <p>
* 仅做轻量级主键查询,用于保存技能前的冲突前置校验。
*
* @param skillName 技能名称
* @return 存在返回true,不存在或入参为空直接返回false
* @throws RuntimeException 数据库执行异常时包装抛出
*/
@Override
public boolean skillExists(String skillName) {
// 空字符串直接判定不存在,避免无效查询
if (skillName == null || skillName.isEmpty()) {
return false;
}
try (Connection conn = dataSource.getConnection()) {
// 复用内部方法,支持复用现有连接
return skillExistsInternal(conn, skillName);
} catch (SQLException e) {
throw new RuntimeException("Failed to check skill existence: " + skillName, e);
}
}
/**
* 在已有数据库连接上执行技能存在性校验,支持事务内复用连接。
* <p>
* SQL增加LIMIT 1命中索引,查到第一条立即终止扫描,提升查询性能。
*
* @param conn 已打开的数据库连接
* @param skillName 待校验的技能名称
* @return 技能存在返回true
* @throws SQLException SQL执行异常
*/
private boolean skillExistsInternal(Connection conn, String skillName) throws SQLException {
// 只查询常量1,减少数据传输;LIMIT 1避免全表扫描
String checkSql =
"SELECT 1 FROM " + getFullTableName(skillsTableName) + " WHERE name = ? LIMIT 1";
try (PreparedStatement stmt = conn.prepareStatement(checkSql)) {
stmt.setString(1, skillName);
try (ResultSet rs = stmt.executeQuery()) {
// 有结果行即代表技能已存在
return rs.next();
}
}
}
4.2 写入能力
前置校验(事务外提前拦截,避免事务回滚开销):
- 校验只读标识,只读库直接返回
false; - 批量校验所有
skill名称、资源路径长度与非法字符; force=false时提前批量查重,一旦存在冲突直接抛出异常,不开启事务。
事务原子保障:
- 关闭自动提交,开启事务;
- 循环处理每条
Skill:- 已存在 +
force=true:先执行删除(级联清空资源); - 插入主表,拿到自增主键skillId;
- 使用
JDBC批量Batch插入全部资源文件,减少网络IO;
- 已存在 +
- 全部成功则
commit,任意异常rollback; finally恢复autoCommit状态。
save 方法源码:
/**
* 批量持久化多个技能,支持覆盖写入与事务原子提交。
*
* <p>【前置校验:全部放在事务外部,减少事务回滚带来的性能损耗】
* <ol>
* <li>先判断读写权限:只读仓库直接返回false,不执行后续逻辑;</li>
* <li>批量校验所有Skill名称、资源路径的长度与非法字符,提前拦截脏数据;</li>
* <li>当force=false时,提前批量查重,一旦发现重名技能直接抛出异常,不开启数据库事务。</li>
* </ol>
*
* <p>【事务原子保障】
* <ol>
* <li>关闭连接自动提交,开启事务;</li>
* <li>遍历每一条Skill:
* <ul>
* <li>如果技能已存在并且force=true,先删除原有记录,依靠外键级联自动清理资源数据;</li>
* <li>插入技能主记录,获取数据库自增主键skillId;</li>
* <li>使用JDBC Batch批量插入该技能下全部资源文件,减少多次网络IO开销;</li>
* </ul>
* </li>
* <li>全部写入无异常执行commit;任意异常执行rollback,保证主表与资源附表写入原子性;</li>
* <li>finally块强制恢复autoCommit连接属性,避免连接池连接状态异常。</li>
* </ol>
*
* <p>【新旧表版本写入分支兼容】
* <ol>
* <li>新版本表(存在metadata_json字段):将完整的元数据Map序列化为JSON字符串存入扩展字段;</li>
* <li>旧版本表(无metadata_json字段):如果检测到存在name、description之外的扩展元数据,则打印降级告警,仅持久化基础字段,丢弃扩展元数据。</li>
* </ol>
*
* @param skills 待保存的技能集合
* @param force true:覆盖已存在的同名技能;false:重名直接报错禁止覆盖
* @return 写入成功返回true;入参为空/仓库只读返回false
* @throws IllegalStateException force=false时存在重名技能抛出冲突异常
* @throws RuntimeException SQL执行异常统一包装抛出
*/
@Override
public boolean save(List<AgentSkill> skills, boolean force) {
if (skills == null || skills.isEmpty()) {
return false;
}
// 前置校验1:只读仓库直接拦截写入
if (!writeable) {
logger.warn("Cannot save skills: repository is read-only");
return false;
}
try (Connection conn = dataSource.getConnection()) {
// 前置校验2:事务开启前完成全部参数合法性校验,避免事务开启后回滚
for (AgentSkill skill : skills) {
validateSkillName(skill.getName());
Map<String, String> resources = skill.getResources();
if (resources != null && !resources.isEmpty()) {
for (String path : resources.keySet()) {
validateResourcePath(path);
}
}
}
// 前置校验3:非强制覆盖模式下,提前批量检查冲突,事务外快速失败
if (!force) {
List<String> existingSkills = new ArrayList<>();
for (AgentSkill skill : skills) {
if (skillExistsInternal(conn, skill.getName())) {
existingSkills.add(skill.getName());
}
}
if (!existingSkills.isEmpty()) {
String conflictingSkills = String.join(", ", existingSkills);
throw new IllegalStateException(
"Cannot save skills: the following skills already exist and"
+ " force=false: "
+ conflictingSkills
+ ". Use force=true to overwrite existing skills.");
}
}
// 开启事务,保证主表+资源附表写入原子性
conn.setAutoCommit(false);
try {
for (AgentSkill skill : skills) {
String skillName = skill.getName();
boolean exists = skillExistsInternal(conn, skillName);
// 强制覆盖:先删除旧数据,资源通过外键ON DELETE CASCADE自动清空
if (exists) {
deleteSkillInternal(conn, skillName);
logger.debug("Deleted existing skill for overwrite: {}", skillName);
}
// 插入主表记录,拿到自增主键
long skillId = insertSkill(conn, skill);
// Batch批量插入全部资源文件
insertResources(conn, skillId, skill.getResources());
logger.info("Successfully saved skill: {} (id={})", skillName, skillId);
}
conn.commit();
return true;
} catch (Exception e) {
// 任意异常回滚事务
conn.rollback();
throw e;
} finally {
// 强制恢复连接自动提交状态,归还连接池时保持连接属性正常
restoreAutoCommit(conn);
}
} catch (SQLException e) {
logger.error("Failed to save skills", e);
throw new RuntimeException("Failed to save skills", e);
}
}
insertSkill 方法源码:
/**
* 插入技能主记录,自动区分新旧表结构做兼容写入。
* <p>版本分支:
* 1. 表包含metadata_json:序列化完整元数据存入扩展字段;
* 2. 表无metadata_json:若存在扩展元数据则打印警告,仅持久化name、description基础字段。
*
* @param conn 数据库事务连接
* @param skill 待写入技能对象
* @return 数据库自增主键ID
* @throws SQLException SQL执行异常
*/
private long insertSkill(Connection conn, AgentSkill skill) throws SQLException {
String insertSql =
"INSERT INTO "
+ getFullTableName(skillsTableName)
+ (metadataJsonColumnSupported
? " (name, description, skill_content, source, metadata_json)"
+ " VALUES (?, ?, ?, ?, ?)"
: " (name, description, skill_content, source) VALUES (?, ?, ?,"
+ " ?)");
try (PreparedStatement stmt =
conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) {
stmt.setString(1, skill.getName());
stmt.setString(2, skill.getDescription());
stmt.setString(3, skill.getSkillContent());
stmt.setString(4, skill.getSource());
// 新版表写入完整元JSON
if (metadataJsonColumnSupported) {
stmt.setString(5, serializeMetadata(skill.getMetadata()));
} else if (hasExtendedMetadata(skill.getMetadata())) {
// 旧表无扩展字段,额外元数据无法持久化,输出告警日志
logger.warn(
"metadata_json column not found in {}.{}; extended metadata for skill '{}'"
+ " will not be persisted",
databaseName,
skillsTableName,
skill.getName());
}
stmt.executeUpdate();
try (ResultSet generatedKeys = stmt.getGeneratedKeys()) {
if (generatedKeys.next()) {
return generatedKeys.getLong(1);
} else {
throw new SQLException(
"Failed to get generated id for skill: " + skill.getName());
}
}
}
}
批量插入所有资源文件方法源码:
/**
* 使用JDBC批量Batch一次性插入所有资源文件,减少多次网络往返。
*
* @param conn 事务连接
* @param skillId 技能主表主键
* @param resources 资源文件路径+内容Map
* @throws SQLException 批量写入失败抛出异常触发事务回滚
*/
private void insertResources(Connection conn, long skillId, Map<String, String> resources)
throws SQLException {
if (resources == null || resources.isEmpty()) {
logger.debug("No resources to insert for id: {}", skillId);
return;
}
String insertSql =
"INSERT INTO "
+ getFullTableName(resourcesTableName)
+ " (id, resource_path, resource_content) VALUES (?, ?, ?)";
try (PreparedStatement stmt = conn.prepareStatement(insertSql)) {
for (Map.Entry<String, String> entry : resources.entrySet()) {
String path = entry.getKey();
String content = entry.getValue();
stmt.setLong(1, skillId);
stmt.setString(2, path);
stmt.setString(3, content);
stmt.addBatch();
}
int[] results = stmt.executeBatch();
int insertedCount = 0;
for (int i = 0; i < results.length; i++) {
if (results[i] > 0 || results[i] == Statement.SUCCESS_NO_INFO) {
insertedCount++;
} else if (results[i] == Statement.EXECUTE_FAILED) {
logger.error("Failed to insert resource at batch index {}", i);
}
}
logger.debug(
"Batch inserted {} resources for id '{}' (total: {})",
insertedCount,
skillId,
resources.size());
if (insertedCount != resources.size()) {
throw new SQLException(
"Failed to insert all resources for id '"
+ skillId
+ "'. Expected: "
+ resources.size()
+ ", Inserted: "
+ insertedCount);
}
}
}
写入分支(版本兼容,老版本以前建的表,没有 metadata_json 这一列):
- 新版本(有
metadata_json):把完整Metadata序列化为JSON存入字段; - 旧版本:检测是否存在扩展字段,如果有则打印告警,只保存基础字段。
4.3 删除能力
执行逻辑:
- 开启事务;
- 直接删除主表记录,资源表依靠外键
ON DELETE CASCADE自动清空,不需要手动删子表; - 成功提交,失败回滚。
源码说明:
/**
* 根据技能名称删除技能。
* <p>
* 1. 只读仓库直接拒绝删除操作;
* 2. 开启事务保证操作原子性;
* 3. 仅删除主表记录,资源附表依靠外键 ON DELETE CASCADE 自动级联删除,无需手动删资源;
* 4. 成功提交,失败回滚,最后恢复连接自动提交属性。
*
* @param skillName 待删除的技能名称
* @return true 删除成功;false 技能不存在或仓库只读
* @throws RuntimeException 数据库异常包装抛出
*/
@Override
public boolean delete(String skillName) {
// 前置判断:只读仓库禁止删除
if (!writeable) {
logger.warn("Cannot delete skill: repository is read-only");
return false;
}
// 校验技能名称合法性
validateSkillName(skillName);
try (Connection conn = dataSource.getConnection()) {
// 先判断技能是否存在
if (!skillExistsInternal(conn, skillName)) {
logger.warn("Skill does not exist: {}", skillName);
return false;
}
// 开启事务
conn.setAutoCommit(false);
try {
// 删除主表数据,资源表自动级联清空
deleteSkillInternal(conn, skillName);
conn.commit();
logger.info("Successfully deleted skill: {}", skillName);
return true;
} catch (Exception e) {
// 异常回滚
conn.rollback();
throw e;
} finally {
// 恢复连接自动提交,防止连接池连接状态异常
restoreAutoCommit(conn);
}
} catch (SQLException e) {
logger.error("Failed to delete skill: {}", skillName, e);
throw new RuntimeException("Failed to delete skill: " + skillName, e);
}
}
/**
* 内部执行删除SQL,只删除技能主表记录。
* 资源附表会通过外键约束 ON DELETE CASCADE 自动删除,不用额外执行DELETE。
*
* @param conn 事务数据库连接
* @param skillName 技能名称
* @throws SQLException SQL执行异常
*/
private void deleteSkillInternal(Connection conn, String skillName) throws SQLException {
String deleteSkillSql =
"DELETE FROM " + getFullTableName(skillsTableName) + " WHERE name = ?";
try (PreparedStatement stmt = conn.prepareStatement(deleteSkillSql)) {
stmt.setString(1, skillName);
stmt.executeUpdate();
}
}
4.4 辅助能力
除了增删改查核心业务,代码里封装了一整套辅助工具方法,用来做安全校验:
clearAllSkills():清空全表,测试使用;getSource():生成字符串标识mysql_库名_表名,用来区分不同数据源,自动处理重名仓库后缀,对应前文<source>字段;getRepositoryInfo:对外暴露存储类型与读写状态。
更多推荐

所有评论(0)