前阵子接了个数据迁移的活儿,需要把一套业务系统的上百张表结构完整摸清楚,还要生成能归档的数据字典。一开始用达梦自带的数据库管理工具倒也能导出,可表一多、频率一高,工具手动操作那套根本扛不住,导出来的格式也五花八门,没法自动做环境之间的结构对比。后来我干脆写了一套通用代码,直接基于达梦数据库(DM8)的JDBC驱动和系统视图把表结构读出来,表、字段、类型、精度、注释、主键、默认值全部解析成结构化数据,再按需输出成文档或者用于代码生成。
这套东西跑通之后省了不少事,中间也踩了不少坑,比如注释读不到、模式大小写不匹配、系统表混进来等问题。这篇文章把实现思路、完整步骤以及排查过程都记录下来,给准备做同样事情的同学一个可以直接参考的方案,不管你是要做数据字典、生成实体类,还是做多环境表结构对比,应该都用得上。
1. 项目背景与整体设计思路
1.1 为什么不用现成工具,要自己写代码
很多人第一反应是,达梦不是自带管理工具吗,右键表生成SQL脚本不就行了?确实可以,而且达梦也支持模式导出、表结构脚本导出这些功能。但真实项目里有两个问题绕不过去。
第一个是自动化问题。数据字典不是导一次就完事,开发环境、测试环境会频繁变更表结构,文档需要定期同步。还有人会拿两个环境的表结构做差异对比,找一个字段新增或长度变化。这些场景都要求程序能定时、批量去调用,管理工具这种手动操作没法纳入流水线。
第二个是定制化输出问题。我要的不是建表SQL,而是结构化的元数据:字段名、字段类型、长度、精度、小数位、是否可空、默认值、注释、主键顺序。建表SQL虽然信息完整,但还得做二次解析,文本解析非常容易出错。而JDBC的元数据接口和数据字典视图能直接以结构化表格返回,拿到的就是干净的字段清单。
所以结论很明确:与其写一堆SQL解析脚本,不如直接用标准接口去读元数据。这套代码不绑定某个具体导出场景,后续要生成实体类、生成Markdown文档、做结构对比,都复用同一份读取结果。
1.2 通用代码的定位:一次读取,多库兼容
标题里强调“通用代码”,意思不是只针对达梦写死一套逻辑,而是上层用统一的数据模型收口,数据库差异只隔离在读取层。
具体来说,我设计的时候把表结构抽象成这么两样东西:
- TableInfo:表名、模式、类型(表/视图)、表注释、主键列清单、字段列表
- ColumnInfo:字段名、字段序号、类型、长度、精度、小数位、是否可空、默认值、注释、是否自增
后面所有功能都围绕这两个模型来写。这样换数据库时,只需要在读取层做适配:MySQL有INFORMATION_SCHEMA,Oracle和达梦有ALL_TAB_COLUMNS这组视图,PostgreSQL有pg_catalog。只要能把这些不同来源的数据填进同一个模型,上层逻辑完全不用改动。
这也是为什么我建议不要把类型做成固定枚举。数据库之间的类型差异太大了,VARCHAR2、VARCHAR、TEXT、LONG VARCHAR、NVARCHAR2,硬枚举会把代码锁死,用字符串原样保存才是最稳的。
1.3 技术方案选型:标准JDBC接口还是系统视图
实际操作中有两条路可以拿到达梦的表结构:
一条是JDBC标准接口DatabaseMetaData,里面的getTables、getColumns、getPrimaryKeys就是专门干这个的,理论上所有数据库通用。另一条是直接查达梦的数据字典视图,比如ALL_TABLES、ALL_TAB_COLUMNS、ALL_COL_COMMENTS、ALL_CONSTRAINTS。
我一开始想偷懒,全用JDBC标准接口,毕竟代码最干净。结果实测发现,达梦的JDBC驱动对getTables和getColumns支持还可以,但有些列的返回值会“残缺”,最典型的就是列注释REMARKS经常读不到,主键信息也得看驱动版本。这直接导致我用JDBC拿回来的数据缺胳膊少腿。
然后我补查了系统视图,发现注释、约束、自增这些信息还是得靠数据字典视图才稳。两种方案的优缺点对比如下:
| 维度 | 标准JDBC元数据接口 | 数据字典视图 |
|---|---|---|
| 跨数据库通用性 | 高 | 低,依赖达梦Oracle兼容模式 |
| 表、字段、类型信息 | 基本可用 | 很完整 |
| 注释信息 | 部分版本为空 | 完整 |
| 主键与约束 | 依赖驱动实现 | 完整 |
| 自增列 | 有IS_AUTOINCREMENT列但版本差异大 | 部分版本可用系统视图补齐 |
| 代码简洁度 | 高 | 需要写SQL |
所以说,完整方案不是二选一,而是“JDBC拿骨架 + 系统视图补细节”。先用JDBC把表清单和字段清单拉出来,再用系统视图把注释、主键、默认值这些细节填上。这样既保留了通用性,又能拿到完整信息。
2. 达梦数据库表结构信息藏在哪
2.1 核心数据字典视图:ALL_TABLES与ALL_TAB_COLUMNS
达梦数据库在Oracle兼容模式下,系统视图和Oracle非常像,这也是很多老Oracle开发能快速上手达梦的原因。读取表结构最核心的几张视图是ALL_TABLES和ALL_TAB_COLUMNS。
ALL_TABLES 里记录当前用户能访问的所有表,常用字段有OWNER(模式名)、TABLE_NAME(表名)、TABLESPACE_NAME(表空间)、STATUS(状态)。如果只想看某模式下的表,用OWNER过滤就行。
ALL_TAB_COLUMNS 是字段信息的主力视图,每条记录对应表里的一个字段,关键字段如下:
- OWNER、TABLE_NAME:定位是哪张表的哪个字段
- COLUMN_ID:字段序号,从1开始,排序就靠它
- COLUMN_NAME:字段名
- DATA_TYPE:字段类型,比如VARCHAR2、CHAR、DECIMAL、INTEGER、DATE、TIMESTAMP
- DATA_LENGTH:字段长度,这个值在不同字符集下含义不一样,要注意
- DATA_PRECISION、DATA_SCALE:精度和小数位,DECIMAL/NUMBER类型用得上
- NULLABLE:是否允许为空,值为Y或N
实际查询的SQL是这样的:
SELECT TABLE_NAME, COLUMN_ID, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE FROM ALL_TAB_COLUMNS WHERE OWNER = UPPER('test') ORDER BY TABLE_NAME, COLUMN_ID;这里有个非常容易踩的坑:DATA_LENGTH在数据库里通常存的是字节长度,不是字符长度。比如一个字段定义成VARCHAR2(100),如果数据库字符集是UTF-8,DATA_LENGTH显示的可能就是400;如果字符集是GBK,一个汉字占2个字节,显示可能在200左右浮动。所以专门做文档时,宁可先用DATA_LENGTH做展示,再人工校对,也别理所当然地以为它就是“字符数”。
2.2 表注释与列注释:ALL_TAB_COMMENTS和ALL_COL_COMMENTS
达梦的注释信息和字段信息是分开存放的,这一点和Oracle完全一致。表注释在ALL_TAB_COMMENTS里,列注释在ALL_COL_COMMENTS里。
-- 表注释 SELECT TABLE_NAME, COMMENTS FROM ALL_TAB_COMMENTS WHERE OWNER = UPPER('test'); -- 列注释 SELECT TABLE_NAME, COLUMN_NAME, COMMENTS FROM ALL_COL_COMMENTS WHERE OWNER = UPPER('test');我之所以专门提这两张视图,是因为它们在实操中的地位太重要了。很多项目要求数据字典必须包含中文注释,但JDBC的getColumns返回的REMARKS字段在达梦驱动上经常是空的,最后只能靠ALL_COL_COMMENTS来补。
我在程序里的做法是,把这两张视图一次查出来,放进Map<表名+字段名, 注释>,再和JDBC拿到的字段模型做合并。千万别说一条一条查,几百张表可能几千个字段,性能完全不一样。
2.3 主键、唯一约束与IDENTITY自增列
主键是表结构里绕不开的东西,生成实体类、做数据对比、生成DDL都依赖它。达梦里的主键信息不像MySQL的INFORMATION_SCHEMA那样直接放一张表里,而是分布在ALL_CONSTRAINTS和ALL_CONS_COLUMNS两张视图里。
ALL_CONSTRAINTS存约束主信息,包括约束名、约束类型、所属表、状态。约束类型用CONSTRAINT_TYPE表示,P是主键,U是唯一约束,C是检查约束,R是外键。ALL_CONS_COLUMNS存约束涉及的具体字段,以及字段在约束中的顺序POSITION。
查主键的SQL如下:
SELECT a.CONSTRAINT_NAME, a.TABLE_NAME, b.COLUMN_NAME, b.POSITION FROM ALL_CONSTRAINTS a JOIN ALL_CONS_COLUMNS b ON a.CONSTRAINT_NAME = b.CONSTRAINT_NAME AND a.OWNER = b.OWNER WHERE a.OWNER = UPPER('test') AND a.CONSTRAINT_TYPE = 'P' ORDER BY a.TABLE_NAME, b.POSITION;主键字段的顺序很重要,一定要按POSITION排序,不然生成实体类里复合主键的顺序就乱了。
至于IDENTITY自增列,达梦的不同版本表现差异比较明显。一部分场景下可以通过JDBC的getColumns结果集里的IS_AUTOINCREMENT列来判断,但这个字段的标准性存疑;另一部分场景则建议直接看建表DDL,或者用DBA级别的视图去确认。我的建议是,如果只是做数据字典,自增列不是关键信息,标注成可选扩展就行,别让它卡住主流程。
3. 实操过程:从零实现一套可用的读取器
3.1 环境准备:驱动、连接参数、本地依赖安装
在写代码之前,先把达梦JDBC驱动搞定。达梦8的JDBC驱动一般叫DmJdbcDriver18.jar,适用于JDK1.8及以上。如果你是连现成的达梦环境,直接找管理员要一份驱动包即可。如果本机安装了达梦数据库,驱动一般在安装目录下的drivers/jdbc文件夹里。
拿到jar之后,建议用maven命令安装到本地仓库,这样项目里直接引用坐标就行,不用维护一整个lib目录:
mvn install:install-file -Dfile=DmJdbcDriver18.jar -DgroupId=com.dameng -DartifactId=DmJdbcDriver18 -Dversion=8.1.2.192 -Dpackaging=jar然后pom里加依赖:
<dependency> <groupId>com.dameng</groupId> <artifactId>DmJdbcDriver18</artifactId> <version>8.1.2.192</version> </dependency>连接参数方面,达梦默认端口是5236,JDBC URL格式如下:
jdbc:dm://127.0.0.1:5236很多人在这里会有一个误解:JDBC URL没带schema,所以连接成功之后就认为直接能查到表。实际上,你不指定schema也能连上数据库,但在读元数据时必须显式传schema参数,否则很多接口会默认按当前登录用户过滤,查出来的结果往往不是你想要的。
3.2 定义统一数据模型:TableInfo和ColumnInfo
我前面说过,所有数据库差异在读取层消化,上层统一用模型。两个Java类的设计如下:
public class TableInfo { private String schema; // 模式名 private String tableName; // 表名 private String tableType; // TABLE / VIEW private String comment; // 表注释 private List<ColumnInfo> columns; // 字段列表 private List<String> primaryKeyColumns; // 主键字段,按顺序 } public class ColumnInfo { private String columnName; // 字段名 private int position; // 序号 private String dataType; // 原始类型名,如 VARCHAR2 private int length; // 长度 private Integer precision; // 精度 private Integer scale; // 小数位 private boolean nullable; // 是否可空 private String defaultValue; // 默认值 private String comment; // 字段注释 private boolean autoIncrement; // 是否自增 }我用Integer而不是int来表示精度和小数位,因为不是所有类型都有这两个概念,比如DATE类型读出来可能为null。用包装类型能避免很多判断上的麻烦。
3.3 基于JDBC DatabaseMetaData的通用读取实现
先看主流程,我直接读取所有表和字段:
public List<TableInfo> readByJdbcMetaData(Connection conn, String schema) throws SQLException { List<TableInfo> tables = new ArrayList<>(); DatabaseMetaData meta = conn.getMetaData(); try (ResultSet rs = meta.getTables(null, schema, "%", new String[]{"TABLE", "VIEW"})) { while (rs.next()) { TableInfo table = new TableInfo(); table.setSchema(rs.getString("TABLE_SCHEM")); table.setTableName(rs.getString("TABLE_NAME")); table.setTableType(rs.getString("TABLE_TYPE")); List<ColumnInfo> columns = new ArrayList<>(); try (ResultSet cols = meta.getColumns(null, schema, table.getTableName(), "%")) { while (cols.next()) { ColumnInfo col = new ColumnInfo(); col.setColumnName(cols.getString("COLUMN_NAME")); col.setPosition(cols.getInt("ORDINAL_POSITION")); col.setDataType(cols.getString("TYPE_NAME")); col.setLength(cols.getInt("COLUMN_SIZE")); col.setPrecision(cols.getInt("COLUMN_SIZE")); col.setScale(cols.getInt("DECIMAL_DIGITS")); col.setNullable(cols.getInt("NULLABLE") != DatabaseMetaData.columnNoNulls); col.setDefaultValue(cols.getString("COLUMN_DEF")); columns.add(col); } } table.setColumns(columns); tables.add(table); } } return tables; }这里有几个细节值得说:
- getTables的第一个参数是catalog,达梦驱动基本无视它,传null就行,真正起作用的是第二个参数schema。
- schema必须是实际模式名,而且通常是大写。如果输错了,结果集为空,但不会报异常,排查起来很费劲。
- COLUMN_SIZE对于VARCHAR2是字符长度,对于NUMBER则是精度,在达梦驱动下表现不太一致,所以我在填充length和precision时都先取COLUMN_SIZE,具体语义再根据类型微调。
这套逻辑最大的好处是不依赖任何数据库特有的SQL,MySQL、Oracle、PostgreSQL理论上都能用。局限性也很明显:注释经常拿不到,默认值也可能为空。这就是我为什么还要再走一遍系统视图。
3.4 基于达梦系统视图的增强读取实现
系统视图部分,核心是补全三块信息。第一是表注释:
public Map<String, String> readTableComments(Connection conn, String schema) throws SQLException { Map<String, String> map = new HashMap<>(); String sql = "SELECT TABLE_NAME, COMMENTS FROM ALL_TAB_COMMENTS WHERE OWNER = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, schema.toUpperCase()); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { map.put(rs.getString("TABLE_NAME"), rs.getString("COMMENTS")); } } } return map; }第二是列注释,SQL和表注释类似,只是视图换成ALL_COL_COMMENTS,条件多一个字段名。第三是主键信息,用前面写的ALL_CONSTRAINTS和ALL_CONS_COLUMNS关联查询。
我习惯把注释Map的key拼成“表名|字段名”,避免不同表有相同字段名导致覆盖:
map.put(rs.getString("TABLE_NAME") + "|" + rs.getString("COLUMN_NAME"), rs.getString("COMMENTS"));最后再写一个汇总方法,把JDBC读出来的骨架和增强信息合并:
public List<TableInfo> readTableStructure(Connection conn, String schema) throws SQLException { // 1. JDBC骨架 List<TableInfo> tables = readByJdbcMetaData(conn, schema); // 2. 补充注释 Map<String, String> tableComments = readTableComments(conn, schema); Map<String, String> columnComments = readColumnComments(conn, schema); Map<String, List<String>> primaryKeys = readPrimaryKeys(conn, schema); // 3. 合并到模型 for (TableInfo table : tables) { table.setComment(tableComments.get(table.getTableName())); for (ColumnInfo col : table.getColumns()) { String key = table.getTableName() + "|" + col.getColumnName(); col.setComment(columnComments.get(key)); } table.setPrimaryKeyColumns(primaryKeys.get(table.getTableName())); } return tables; }合并顺序值得注意:JDBC只是骨架,注释和主键一律以系统视图为准。这样写的好处是,将来要兼容MySQL,只需要换掉系统视图那部分实现,JDBC骨架部分可以原样复用。
3.5 按模式过滤:只读指定用户下的业务表
在真实数据库里,达梦会默认带一批系统模式,比如SYSDBA、SYSAUDITOR、SYSJOB,还有大量SYS开头的东西。如果不过滤,读出来的列表会非常杂乱。最稳妥的方式是白名单,明确只处理业务用户对应的模式。
SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = UPPER(?) AND TABLE_NAME NOT LIKE 'SYS%' ORDER BY TABLE_NAME;如果数据库里模式特别多,也可以通过查询先列出所有OWNER,人工确认一遍再写进配置:
SELECT DISTINCT OWNER FROM ALL_TABLES ORDER BY OWNER;这一步看起来不起眼,但在最终输出数据字典时非常关键,否则文档里混进系统表,使用方很容易误读。
3.6 最终输出:生成Markdown数据字典文档
读取完结构化模型后,输出只是体力活。我常输出两种格式,一种Markdown,一种Excel,这里演示Markdown:
public String toMarkdown(TableInfo table) { StringBuilder sb = new StringBuilder(); sb.append("# 表 ").append(table.getTableName()).append("\n\n"); sb.append("> 注释:").append(nullToEmpty(table.getComment())).append("\n\n"); sb.append("| 序号 | 字段名 | 类型 | 长度 | 精度 | 小数 | 可空 | 默认值 | 注释 |\n"); sb.append("| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n"); for (ColumnInfo col : table.getColumns()) { sb.append("| ") .append(col.getPosition()).append(" | ") .append(col.getColumnName()).append(" | ") .append(col.getDataType()).append(" | ") .append(col.getLength()).append(" | ") .append(nullToEmpty(col.getPrecision())).append(" | ") .append(nullToEmpty(col.getScale())).append(" | ") .append(col.isNullable() ? "Y" : "N").append(" | ") .append(nullToEmpty(col.getDefaultValue())).append(" | ") .append(nullToEmpty(col.getComment())).append(" |\n"); } if (table.getPrimaryKeyColumns() != null && !table.getPrimaryKeyColumns().isEmpty()) { sb.append("\n**主键:** ").append(String.join(", ", table.getPrimaryKeyColumns())).append("\n"); } return sb.toString(); }生成出来的数据字典可以直接放到项目Wiki或者文档管理系统里,格式清爽,别人看起来也直观。
4. 常见问题与排查技巧实录
4.1 查不到任何表:模式名大小写和Catalog传参问题
这是我在达梦上遇到最多的情况。代码跑完,List是空的,也不报错。一开始一脸懵,后来意识到问题出在大小写。
达梦在不加双引号的情况下,创建的表和模式会默认存为大写。如果你在JDBC调用时传了小写的schema,结果集自然为空。有些开发环境里,模式名是带引号创建的小写模式,这就更麻烦了。
排查方法很简单,先在数据库客户端执行。
SELECT DISTINCT OWNER FROM ALL_TABLES;看看实际存在的模式名长什么样,再决定代码里统一UPPER还是原样传入。另外,getTables的schema参数传null也不是不行,但那样会把所有能访问的模式都扫一遍,包含系统模式,输出会很乱。
4.2 列注释读不到:JDBC驱动对REMARKS支持不完整
我在测试环境用达梦的DmJdbcDriver18跑getColumns,盯着结果集里的REMARKS,几次返回都是null。一开始以为是表本身没写注释,后来发现用管理工具看注释是有的,这才确定是驱动的问题。
所以别再纠结JDBC能不能返回注释了,直接放弃REMARKS这条路,改查ALL_COL_COMMENTS。这里也顺带提醒:查ALL_COL_COMMENTS时,OWNER同样要大小写一致,最好先UPPER。
如果连ALL_COL_COMMENTS也查不到注释,先确认用户是否有权限访问这张视图,以及建表的时候是否真的执行了COMMENT ON语句。这两条都排除掉以后,一般就查得到。
4.3 字段类型显示为LONG而不是VARCHAR2
做数据字典时会发现,某些字段在JDBC里返回的类型名是LONG或LONG VARCHAR,尤其在建表时用了超长文本字段,或者达梦自己做了类型映射的情况下。
比如达梦里常见的CLOB类型,或者某些驱动版本对VARCHAR2的变长处理,传回来就是LONG VARCHAR。这不代表建表语句里真的写了LONG,而是JDBC驱动的类型映射结果。
我的处理方法是,展示阶段优先使用ALL_TAB_COLUMNS里的DATA_TYPE,而不是JDBC的TYPE_NAME。JDBC的类型只用来做Java类型映射,展示文档还是以数据字典视图为准。如果你一定要在JDBC层面处理,可以写一个类型归一化的方法,把LONG VARCHAR映射回VARCHAR2,但这属于驱动差异的补丁,通用性不高。
4.4 系统表混进来:过滤不干净
达梦默认的模式可不止一个,除了当前用户自己的模式,还有一堆系统相关模式。如果你直接查ALL_TABLES且不过滤OWNER,列表里会出现大量SYS开头的东西,其中很多是达梦内部的元数据表。
光过滤OWNER LIKE 'SYS%'还不够,因为某些配套工具会创建自己的模式,名字不一定以SYS开头。所以我建议用白名单:
AND OWNER NOT LIKE 'SYS%' AND OWNER NOT IN ('SYSDBA', 'SYSAUDITOR', 'SYSJOB')再严格一点的做法是,代码里配置一个可用的业务模式清单,比如只有TEST、APP两个模式,其他地方写死只读这些模式。这样就算以后有人新建了奇怪名字的模式,也不会混进数据字典。
4.5 驱动版本差异:DmJdbcDriver17和18行为不一致
同一个项目里,我先后接触过两套达梦环境,一套用的驱动老一点,一套用的DmJdbcDriver18。结果发现,老驱动在getColumns返回的列数量和执行效率上都有明显差异,注解信息缺得更多。
后来我统一用DmJdbcDriver18,问题少了很多。所以建议,新项目直接用18版驱动,如果碰到老环境,宁可想办法升级驱动也不要花大量时间在兼容老驱动程序上。驱动jar包最好在项目里固定版本,不要随便换,因为不同版本对元数据结果集的列名支持可能不一样,代码里硬编码的getString("REMARKS")在某个版本里可能直接抛异常。
5. 进阶扩展:表结构对比与实体类生成
5.1 基于结构化模型做表结构差异对比
读取表结构不单纯为了出文档,很多时候是为了多环境对比。有了TableInfo和ColumnInfo的模型,对比逻辑反而简单了,关键就是构建唯一键索引。
我对比两个库时,先把两个环境的表结构都读成Map<表名, TableInfo>,然后按键遍历,逐层比较:先对表清单,找新增表、删除表;再对同一个表的字段清单,按字段名找新增字段、删除字段;再检查字段的类型、长度、精度、可空、注释是否有变化。
伪代码思路如下:
public void compareTables(Map<String, TableInfo> source, Map<String, TableInfo> target) { for (String tableName : source.keySet()) { if (!target.containsKey(tableName)) { System.out.println("源库有表,目标库缺失:" + tableName); } else { compareColumns(source.get(tableName), target.get(tableName)); } } // 反过来找目标库多出来的表 }这个功能在数据库版本升级、分库分表前评估、数据迁移验证场景下特别实用。实际跑下来,几十张表的差异一眼就能看全,比人工对着管理工具点来点去高效太多。
5.2 生成Java实体类的基本思路
既然拿到了字段类型,生成实体类就是顺理成章的事。核心是类型映射表,提前把达梦的字段类型和Java类型对应好:
- VARCHAR、VARCHAR2、CHAR、TEXT -> String
- INTEGER、INT -> Integer
- BIGINT -> Long
- DECIMAL、NUMERIC、NUMBER -> BigDecimal
- DATE、TIMESTAMP -> LocalDateTime(或者java.util.Date)
- FLOAT、DOUBLE -> Double
然后根据ColumnInfo拼出字段定义和getter、setter,注释直接填字段的中文注释。这里有一点要注意:达梦字段名大小写不敏感时通常显示为大写,但Java命名规范希望是驼峰。写代码时需要自己处理字段名转驼峰,比如把USER_NAME转成userName。如果数据库建表时用了带引号的小写字段,那Java字段名直接用原字段名也行,别硬转。
这个扩展属于“一次读取,多处复用”的典型落地。我实际跑下来,生成实体类生成的准确率还不错,但表名和类名的映射规则还是得人工确认,尤其是多单词表名,惯例并不完全统一。
我自己在多个异构库之间切换测试之后,最大的感受是:别指望JDBC一次把所有信息拿全,也别把系统视图当成唯一方案,二者结合才是最优解。实现时不用追求一上来就做完整架构,先针对达梦这一个库把“完整读取一张表结构”跑通,再慢慢把通用模型抽出来,后面扩展自然顺滑。这段代码目前已经在我的几个周边项目里复用,后续如果再遇到国产数据库适配,这套结构还能继续演进出更多可能性。