OpenMetadata 词表术语关系类型 CSV 导入导出:设计计划与源码实现全解析
【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata
本文围绕 OpenMetadata 仓库中的规划文档 csv-relation-types-plan.md,系统讲解词表(Glossary)术语 CSV 导入/导出增强方案:从旧格式丢失关系类型的数据问题出发,完整呈现relationType:termFQN新格式的设计、解析规则与向后兼容策略,并对照 CsvUtil.java 与 GlossaryRepository.java 的当前源码,还原该方案在导出、导入两侧的实际实现、边界处理与单元测试验证方式。读完后,你将能够编写带关系类型前缀的术语 CSV 文件,理解导入解析器如何区分"关系类型前缀"与"含冒号的 FQN",并掌握其单元测试的组织思路。
一、问题背景:旧版 CSV 格式丢失关系类型
OpenMetadata 的词表术语实体通过relatedTerms字段维护与其他术语的关系,每个关系由TermRelation对象承载,包含目标术语引用(term)和关系类型(relationType)两个部分。在旧版 CSV 导入/导出实现中:
- 导出侧:只导出关联术语的 FQN,形如
Glossary.Term1;Glossary.Term2,关系类型信息被丢弃; - 导入侧:对所有解析出的关系硬编码
withRelationType("relatedTo")。
这导致两类数据损失:
- 术语本身带有
synonym、broader、narrower或自定义关系类型时,导出的 CSV 无法表达; - 将导出的 CSV 再次导入后,所有关系类型都会退化为
relatedTo。
而数据库中实际上一直正确存储着关系类型——损失只发生在 CSV 序列化/反序列化环节。因此该增强方案的核心目标是:在不破坏旧格式兼容性的前提下,让 CSV 成为无损往返(round-trip)的载体。
二、新 CSV 格式设计:relationType:termFQN
2.1 格式定义
新格式采用relationType:termFQN键值对,多个值之间以分号(;,即FIELD_SEPARATOR)分隔。这与 OpenMetadata CSV 体系中既有的type:value编码惯例保持一致(例如 owner 字段的team:marketing、extension 字段的key:value)。
规划文档 csv-relation-types-plan.md 中给出的三种典型形态:
# 新格式:带关系类型前缀 relatedTerms synonym:Finance.Revenue;broader:Finance.Income;narrower:Finance.Net Revenue # 向后兼容:无前缀时默认 relatedTo relatedTerms Finance.Revenue;Finance.Income # 混合格式(新旧条目共存) relatedTerms synonym:Finance.Revenue;Finance.Income;broader:Finance.Gross Income2.2 默认关系类型
方案定义了以下内置关系类型:
| 关系类型 | 说明 |
|---|---|
relatedTo | 通用关联术语(默认值) |
synonym | 同义术语 |
broader | 更宽泛的上位词 |
narrower | 更具体的下位词 |
antonym | 反义术语 |
partOf | 部分/组件关系 |
hasPart | 包含关系 |
2.3 解析规则
- 若条目包含
:,且冒号前的前缀是有效的关系类型→ 使用该关系类型,冒号后的部分作为术语 FQN; - 若无
:,或前缀不是有效关系类型 → 默认按relatedTo处理,整个字符串视为 FQN; - 有效关系类型的判定依据
glossaryTermRelationSettings(关系类型配置)或内置默认值——这一条在最终实现中得到了动态化落地,见下文 4.2 节。
2.4 向后兼容行为矩阵
| CSV 格式 | 导入行为 |
|---|---|
Glossary.Term1;Glossary.Term2 | 全部关系 →relatedTo |
synonym:Glossary.Term1;Glossary.Term2 | 前者 →synonym,后者 →relatedTo |
synonym:Glossary.Term1;broader:Glossary.Term2 | 两个关系类型均保留 |
三、导出侧实现:CsvUtil.addTermRelations
3.1 当前源码
该方案的导出增强已在 CsvUtil.java 中落地。方法职责与 Javadoc 描述:
/** * Add term relations to CSV record with relation type prefix. * Format: "relationType:termFQN" for non-default relations, or just "termFQN" for "relatedTo". * Example: "synonym:Glossary.Term1;broader:Glossary.Term2;Glossary.Term3" */ public static List<String> addTermRelations( List<String> csvRecord, List<org.openmetadata.schema.type.TermRelation> termRelations) { csvRecord.add( nullOrEmpty(termRelations) ? null : termRelations.stream() .map( tr -> { String relationType = tr.getRelationType(); String fqn = tr.getTerm().getFullyQualifiedName(); // Include relation type prefix for non-default relations if (relationType != null && !relationType.isEmpty() && !relationType.equals("relatedTo")) { return relationType + ENTITY_TYPE_SEPARATOR + fqn; } return fqn; }) .sorted() .collect(Collectors.joining(FIELD_SEPARATOR))); return csvRecord; }实现要点(对照规划文档中的"New"版本代码):
- 省略默认前缀:仅当
relationType非空且不等于relatedTo时,才拼接relationType:前缀;默认关系保持裸 FQN 输出,使导出文件与旧格式视觉上兼容; - 复用既有分隔符常量:前缀与 FQN 之间使用
ENTITY_TYPE_SEPARATOR(值为":"),条目之间使用FIELD_SEPARATOR(值为";"),两者定义于 CsvUtil.java; - 排序保证确定性:拼接前对条目做
.sorted(),确保同一份数据多次导出结果一致(对比旧实现只排序 FQN,新实现排序的是"前缀 + FQN"整体字符串); - 空值处理:
nullOrEmpty(termRelations)为真时写入null,与工具类中addEntityReferences、addTagLabels等其他字段方法的行为一致。
3.2 导出调用链
导出入口在 GlossaryRepository.java 的addRecord方法中,relatedTerms位于词表术语 CSV 的第 6 列(下标 5):
CsvUtil.addFieldList(recordList, entity.getSynonyms()); // 列 4:synonyms addTermRelations(recordList, entity.getRelatedTerms()); // 列 5:relatedTerms addField(recordList, termReferencesToRecord(...)); // 列 6:references从源码结构看,词表术语 CSV 的完整列布局为:0parent、1name、2displayName、3description、4synonyms、5relatedTerms、6references、7tags、8reviewers、9owner、10glossaryStatus、11color、12iconURL、13domains、14extension。
3.3 导出侧单元测试
CsvUtilTest.java 覆盖了规划文档"Phase 2: Testing"中的导出用例,且断言比规划更具体:
| 测试方法 | 验证点 |
|---|---|
testAddTermRelationsHandlesNullAndEmptyInputs | null与空列表均输出单一null字段 |
testAddTermRelationsOmitsRelatedToPrefix | relatedTo关系导出为裸 FQN:Glossary.Alpha |
testAddTermRelationsTreatsNullRelationTypeAsRelatedTo | 关系类型为null时按默认处理,不产生前缀 |
testAddTermRelationsEmitsPrefixForNonDefaultType | 非默认类型输出synonym:Glossary.Alpha |
testAddTermRelationsSortsAndMixesTypes | 混合类型排序后输出Glossary.Alpha;broader:Glossary.Beta;synonym:Glossary.Zeta |
最后一个用例同时验证了"排序作用于带前缀的完整字符串"这一行为,可认为是对规划文档中"New"版导出代码的回归验证。
四、导入侧实现:GlossaryRepository.getTermRelationsFromCsv
4.1 解析主流程
导入侧实现位于 GlossaryRepository.java(GlossaryTerm内部导入器的第 6 列解析,由第 320 行withRelatedTerms(getTermRelationsFromCsv(printer, csvRecord, 5))调用):
/** * Parse term relations from CSV field with support for relation type prefix. * Format: "relationType:termFQN" or just "termFQN" (defaults to "relatedTo"). * Example: "synonym:Glossary.Term1;broader:Glossary.Term2;Glossary.Term3" */ private List<TermRelation> getTermRelationsFromCsv( CSVPrinter printer, CSVRecord csvRecord, int fieldNumber) throws IOException { if (!processRecord) { return null; } String fieldValue = csvRecord.get(fieldNumber); if (nullOrEmpty(fieldValue)) { return null; } List<TermRelation> termRelations = new ArrayList<>(); String[] entries = fieldValue.split(FIELD_SEPARATOR); for (String entry : entries) { String relationType = "relatedTo"; // Default relation type String termFqn = entry.trim(); // Check for relationType:fqn format int colonIndex = entry.indexOf(':'); if (colonIndex > 0) { String prefix = entry.substring(0, colonIndex).trim(); String suffix = entry.substring(colonIndex + 1).trim(); if (isValidRelationType(prefix)) { relationType = prefix; termFqn = suffix; } else if (!prefix.contains(".")) { // Prefix has no dots, so it looks like an intended relation type, not part of an FQN importFailure( printer, invalidField( fieldNumber, String.format( "Invalid relation type '%s' in entry '%s'. " + "Valid types: %s", prefix, entry.trim(), getValidRelationTypeNames())), csvRecord); continue; } // If prefix contains dots, it's likely part of an FQN — treat entire string as FQN } // Resolve the term FQN to an EntityReference EntityReference termRef = getEntityReference(printer, csvRecord, fieldNumber, GLOSSARY_TERM, termFqn); if (termRef != null) { GlossaryTerm resolvedTerm = Entity.getEntity(GLOSSARY_TERM, termRef.getId(), "", Include.NON_DELETED); if (resolvedTerm.getEntityStatus() != null && resolvedTerm.getEntityStatus() != EntityStatus.APPROVED) { importFailure( printer, invalidField( fieldNumber, String.format( "Glossary term '%s' must have APPROVED status. Current: %s", termFqn, resolvedTerm.getEntityStatus())), csvRecord); processRecord = false; continue; } termRelations.add(new TermRelation().withTerm(termRef).withRelationType(relationType)); } } return termRelations.isEmpty() ? null : termRelations; }逐段对照规划文档的"New"版本代码,实现的要点与增强如下:
(1)冒号定位与三元判定逻辑
解析器对每个条目查找第一个冒号(entry.indexOf(':')),并按前缀特征做三分支判定:
colonIndex > 0且前缀命中有效关系类型集合 → 前缀作为关系类型、冒号后作为 FQN;- 前缀不在有效集合中,但前缀不含点号→ 判定用户"本意是写关系类型但写错了",通过
importFailure记录字段级错误(报错信息中列出全部合法类型名)并跳过该条目; - 前缀含点号→ 大概率是 FQN 自身的一部分(例如
Database:Schema.Table这类含冒号的 FQN),整个字符串按 FQN 处理,关系类型回落为relatedTo。
规划文档中"Edge Cases"第 1 条(FQN 含冒号)与第 2 条(无效关系类型)正是由此覆盖。需要注意实现与规划的一处差异:规划建议"无效前缀时整体按 FQN +relatedTo处理",而实现引入了"前缀无点号则报错"的更严格分支——这使拼写错误的关系类型(如synonm:...)不会静默降级,而是在导入报告中显式暴露。另外,规划中"空关系类型:Glossary.Term默认relatedTo"的场景对应colonIndex == 0不满足> 0条件的路径,整个条目按 FQN 处理,行为与规划一致。
(2)关系类型白名单的动态化
规划文档给出的实现草案使用硬编码集合加占位方法:
private static final Set<String> VALID_RELATION_TYPES = Set.of( "relatedTo", "synonym", "broader", "narrower", "antonym", "partOf", "hasPart" );而当前源码改为从关系类型 DAO 动态加载(GlossaryRepository.java):
private boolean isValidRelationType(String relationType) { return validRelationTypeNames.contains(relationType); } private String getValidRelationTypeNames() { return validRelationTypeNames.stream().sorted().collect(Collectors.joining(", ")); } private static Set<String> loadValidRelationTypeNames() { RelationshipTypeResolver resolver = new RelationshipTypeResolver(Entity.getCollectionDAO().relationshipTypeDAO()); return resolver.list().stream() .map(RelationshipType::getName) .collect(Collectors.toUnmodifiableSet()); }白名单由 RelationshipTypeResolver.java 基于relationshipTypeDAO列出全部关系类型(RelationshipType::getName)。这意味着规划文档"Edge Cases"第 4 条——自定义关系类型——在实现中是真实生效的:只要实例的关系类型配置(glossaryTermRelationSettings)中登记了自定义类型,CSV 前缀即可使用该类型,无需修改代码;对应的解析与报错行为由 RelationshipTypeResolverTest.java 所在模块的测试保障。
(3)关联术语状态校验
实现中还包含规划文档未提及的一层校验:每个被关联的术语解析后,会通过Entity.getEntity回查其实体状态,若状态非APPROVED则报字段级错误。这保证了 CSV 导入不会把草稿或已弃用术语建立为关系目标,属于导入质量约束的加强。
五、字段文档更新:glossaryCsvDocumentation.json
规划文档"Phase 1 / 1.3"要求同步更新术语 CSV 的字段文档。当前 glossaryCsvDocumentation.json 中的relatedTerms字段说明:
{ "name": "relatedTerms", "required": false, "description": "List of related glossary terms with optional relation type. Format: `relationType:termFQN` or just `termFQN` (defaults to `relatedTo`). Multiple terms separated by `;`. Valid relation types: `relatedTo` (default), `synonym`, `broader`, `narrower`, `antonym`, `partOf`, `hasPart`.", "examples": [ "`Business terms.Client Identifier;Support.Subscriber Id` - Both default to relatedTo", "`synonym:Business terms.Client Identifier;broader:Support.Subscriber Id` - With explicit relation types", "`synonym:Finance.Revenue;Finance.Income;narrower:Finance.Net Revenue` - Mixed format" ] }该文档与规划中提出的"格式说明 + 三类示例(纯旧格式 / 显式类型 / 混合格式)"要求一一对应,且示例使用真实术语 FQN,可直接复制到 CSV 文件中验证。
六、边界场景与迁移说明
6.1 边界场景核对(对照规划"Phase 3: Edge Cases")
| 场景 | 规划预期 | 当前源码实现 |
|---|---|---|
FQN 含冒号(如Database:Schema.Term) | 前缀校验失败时整体按 FQN 处理 | 前缀含点号 → 整体按 FQN 处理,默认relatedTo |
| 无效关系类型 | 整体按 FQN +relatedTo | 前缀无点号 → 显式importFailure报错(更严格);前缀含点号 → 按 FQN 处理 |
空关系类型(:Glossary.Term) | 默认relatedTo | colonIndex == 0不进入前缀分支,整体按 FQN 处理 |
| 自定义关系类型 | 依赖glossaryTermRelationSettings查询 | loadValidRelationTypeNames()经RelationshipTypeResolver动态加载,真实生效 |
6.2 迁移说明(规划"Migration Notes")
- 无需数据库迁移:数据库早已正确存储关系类型,改动只涉及 CSV 序列化层;
- 存量 CSV 继续可用:旧格式文件(无前缀)导入后全部映射为
relatedTo,行为不变; - 新导出:非默认关系自动带类型前缀,导出→再导入的往返过程不再丢失
synonym、broader、narrower等语义。
6.3 涉及文件清单(对照规划"Files to Modify")
| 文件 | 变更 | 当前仓库状态 |
|---|---|---|
| CsvUtil.java | addTermRelations()输出关系类型前缀 | 已落地 |
| GlossaryRepository.java | getTermRelationsFromCsv()解析关系类型 | 已落地,且白名单动态化 |
| glossaryCsvDocumentation.json | 字段说明与示例更新 | 已落地 |
| CsvUtilTest.java | 导出侧 5 个单元用例 | 已落地 |
| RelationshipTypeResolverTest.java | 关系类型解析器测试 | 已落地 |
七、小结
这份"CSV Import/Export Enhancement for Glossary Term Relations"方案的价值在于用最小的格式扩展解决了语义丢失问题:
- 通过
relationType:termFQN前缀在导出/导入两侧无损保留关系类型; - 默认关系省略前缀,与旧格式 CSV 完全向后兼容,存量文件无需迁移;
- 解析器以"冒号位置 + 前缀是否含点号"双特征区分关系类型前缀与含冒号 FQN,并在前缀拼写错误时显式报错;
- 有效关系类型白名单由
RelationshipTypeResolver从关系类型配置动态加载,自定义关系类型无需改码即可参与 CSV 往返; - 导出确定性(排序)、空值语义、字段文档与单元/集成测试均按规划逐项落地。
对使用者的直接建议:编写或审查词表术语 CSV 时,relatedTerms列中凡是需要表达同义、上下位、反义等语义的条目都应使用显式前缀,而普通关联可继续书写裸 FQN;导入前若不确定合法类型名,可参考导入错误信息中列出的Valid types清单,或核对该实例的关系类型配置。
【免费下载链接】OpenMetadataThe Open Context Layer for Data and AI , OpenMetadata is the open platform for building trusted data context and business semantics for humans, AI assistants, and agents.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMetadata
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考