资讯详情

资讯详情

基于 JDBC 元数据接口的表结构获取:达梦数据库实战

1. 整体设计为什么用 JDBC 元数据接口1.1 先理清需求你要拿到的“表结构信息”到底是什么如果你只是随口说要“获取表结构”可能有两种完全不同的诉求。一种是运维人员想快速看一张表有哪些字段直接执行几条查询在客户端里看一眼就完了另一种是开发要做一个功能模块比如数据字典、元数据采集、ORM映射、代码生成器需要程序化地拿到“表清单—字段—类型—注释—主键”这套结构化信息并且要稳定、可重复执行。我之前遇到的场景就是这种某个数据中台项目要把业务库的元数据采集到中台里后续要同步字段血缘、自动生成测试数据、给前端数据字典页面做展示。第一个接入的库就是达梦数据库。这个场景下一次性脚本是撑不住的必须做一套可复用的取数逻辑于是核心问题就变成了这段代码怎么设计才能既兼容达梦又尽量不锁死在某一种数据库上。还有一个容易忽略的点字段明细、类型、长度、精度、是否可空、默认值、注释、主键、外键这些是常规信息但一旦落到达梦上还会引出许多附加问题。比如达梦支持定义模式下的大写表名、小写表名、加引号保留原始大小写不同用户访问同一个表schema 归属可能不同注释可能存在 COMMENT 语句里也可能需要走系统视图。这些细节在设计数据模型时就得先想清楚否则写出来的代码只能对付最简单的 demo稍微换个环境就各种查不到。1.2 三条技术路线为什么我倾向 DatabaseMetaData要程序化拿表结构通常有三条路。第一条是查系统表或系统视图。达梦的系统视图有 ALL_TABLES、USER_TABLES、ALL_TAB_COLUMNS、ALL_CONSTRAINTS 这些再深入一点还有 DBA_TABLES。MySQL 则是 information_schema.TABLES、information_schema.COLUMNS。问题一目了然每个数据库的系统表长得不一样换个库就要重写一堆 SQL。更麻烦的是达梦还有多种兼容模式不同模式下系统视图的暴露情况不同直接查系统表的代码很容易出现环境依赖。第二条路是写一层“数据库方言适配器”每种数据库实现一套查询逻辑最后统一对外输出。很多代码生成器就是这么做的。优点是可控性强可以拿到自增序列、分区信息这类特殊内容缺点是维护成本高每接一个库都要写一套映射而且系统视图在不同版本之间还会有字段差异。如果团队只维护一个库这条路很香如果明确要多库支持就需要斟酌。第三条路就是我用到的方案直接走 JDBC 标准规范里的 DatabaseMetaData 接口。JDBC 规范中定义了 getTables、getColumns、getPrimaryKeys、getImportedKeys 等方法各个数据库驱动厂商只要遵循标准就会把这些方法实现到本库的系统表之上。达梦的 JDBC 驱动DmJdbcDriver.jar也实现了这些方法。选它作为主干最大的好处是同一份代码在绝大多数关系型数据库上可以直跑遇到个别细节问题再针对达梦做少量补偿。有人会质疑元数据接口的性能。我的实测经验是在几千张表、几万个字段的规模下通过 JDBC 元数据接口读取耗时在几十秒到两三分钟这个量级完全能接受。相比之下写几十个精心优化的系统表查询省出来的时间并不多而且通用性带来的收益通常远超这几分钟的开销。1.3 达梦的“兼容”与“不兼容”达梦数据库有一个显著特点它提供 Oracle 兼容模式很多语法、数据类型、系统视图命名都向 Oracle 靠拢。这也导致一个常见误区——默认达梦和 Oracle 完全一致直接把 Oracle 那套代码搬过来就完事。实际上达梦的 JDBC 驱动在元数据行为上和 Oracle 驱动有不少差异。最典型的是模式名Schema的处理。达梦在用户登录后默认模式一般就是用户名对应的模式但如果配置了“模式名与用户名不一致”或者用高权限用户连接而你要读的是别人名下的表那调用 getTables 时catalog 和 schema 参数怎么传就特别讲究。达梦驱动有时候会把 catalog 和 schema 混在一起处理你只传 schema 不传 catalog结果可能为空你把 schema 当 catalog 传返回结果又可能出乎意料。我调试最久的就集中在这一块。另一个特殊点是大小写。达梦默认把不带引号的表名、字段名转成大写存储但建表时加了双引号名称会保留原始大小写。JDBC 元数据接口做匹配时getTables 的 tableNamePattern 参数是按数据库内部存储的字符串去匹配的。也就是说你拿小写字符串去匹配一个默认创建的大写表名很可能匹配不上。这个问题直接影响了后面所有查询逻辑我放到 2.4 节专门讲。2. 核心实现把“通用”做扎实的分层设计2.1 统一数据模型先行在写读表逻辑之前先要把结果的数据结构定义好。所谓“通用”不是说不考虑数据库差异而是先把差异挡在模型外面上层拿到的东西保持一致。我当时设计了四个基础模型TableInfo表名、表类型、所属 schema、注释、是否视图等基础属性。ColumnInfo字段名、JDBC 类型码、数据库原生类型字符串、长度、精度、是否可空、默认值、注释、字段序号。PrimaryKeyInfo主键名、主键字段集合、联合主键中每个字段的顺序。这个顺序很关键联合主键顺序错了生成索引 DDL 时会出问题。ImportedKeyInfo外键信息包含外键表名、外键字段、引用表名、引用字段、更新规则、删除规则。要注意一点模型里不要只存数据库原生类型字符串。因为达梦返回的 typeName 可能是 VARCHAR、VARCHAR2、NVARCHAR2 这样五花八门的值。比较类型时优先用 dataType 这个 JDBC 类型码再结合实际长度做判断。比如判断“字符串类型”不能只检查 typeName 是不是 varchar应该看 getColumnType() 返回的是 Types.VARCHAR 还是 Types.NVARCHAR再结合长度做后续分支。模型字段宜多不宜少。我吃过亏最初只设计了列名和类型后面数据字典页面需要展示默认值和自增标志又回来改模型。元数据采集这种功能一次把模型定义宽一点后面省很多事。2.2 读取层用 DatabaseMetaData 串起所有信息核心代码不多重点在于理解每一次调用的参数。我写一个不依赖数据库特性的类内部持有 Connection通过 connection.getMetaData() 拿到 DatabaseMetaData 对象然后逐层读取。获取表清单的核心方法如下public ListTableInfo listTables(Connection conn, String schemaPattern, String tableNamePattern) throws SQLException { ListTableInfo tables new ArrayList(); DatabaseMetaData metaData conn.getMetaData(); ResultSet rs metaData.getTables( conn.getCatalog(), // catalog达梦一般传 null 或者当前库 schemaPattern, // schema传模式名支持 % 通配符 tableNamePattern, // 表名模式支持 % 通配符 new String[]{TABLE, VIEW} // 只取表和视图 ); while (rs.next()) { TableInfo info new TableInfo(); info.setTableCat(rs.getString(TABLE_CAT)); info.setTableSchema(rs.getString(TABLE_SCHEM)); info.setTableName(rs.getString(TABLE_NAME)); info.setTableType(rs.getString(TABLE_TYPE)); info.setRemarks(rs.getString(REMARKS)); tables.add(info); } rs.close(); return tables; }这里有一个关键参数策略达梦下catalog 参数到底传什么。我实测下来最稳的做法是 catalog 传 nullschema 传要查询的模式名。部分版本的达梦驱动会把 catalog 当成 Oracle 的实例名处理传了反而查不到结果。而 schema 传大写模式名返回最稳定。如果拿不准模式名的大小写可以先传一个 % 通配符拿到当前用户可见的所有模式再从中过滤。然后是字段信息public ListColumnInfo listColumns(Connection conn, String schemaPattern, String tableName) throws SQLException { ListColumnInfo columns new ArrayList(); DatabaseMetaData metaData conn.getMetaData(); ResultSet rs metaData.getColumns(conn.getCatalog(), schemaPattern, tableName, %); while (rs.next()) { ColumnInfo col new ColumnInfo(); col.setColumnName(rs.getString(COLUMN_NAME)); col.setDataType(rs.getInt(DATA_TYPE)); col.setTypeName(rs.getString(TYPE_NAME)); col.setColumnSize(rs.getInt(COLUMN_SIZE)); col.setDecimalDigits(rs.getInt(DECIMAL_DIGITS)); col.setNullable(rs.getInt(NULLABLE)); col.setRemarks(rs.getString(REMARKS)); col.setOrdinalPosition(rs.getInt(ORDINAL_POSITION)); col.setDefaultValue(rs.getString(COLUMN_DEF)); columns.add(col); } rs.close(); return columns; }在达梦下getColumns 返回的 IS_AUTOINCREMENT 列也支持它返回 YES 或 NO可以用来判断自增字段。不过有些版本的驱动在这列取值上不稳定更严谨的做法是拿系统视图 ALL_TAB_COLUMNS 中 IDENTITY 相关字段做补充确认。这就是典型的“通用代码 方言补偿”配合通用主干保持干净方言细节藏在补偿分支里。主键读取public PrimaryKeyInfo getPrimaryKey(Connection conn, String schemaPattern, String tableName) throws SQLException { PrimaryKeyInfo pk new PrimaryKeyInfo(); ListString pkColumns new ArrayList(); DatabaseMetaData metaData conn.getMetaData(); ResultSet rs metaData.getPrimaryKeys(conn.getCatalog(), schemaPattern, tableName); while (rs.next()) { pk.setPkName(rs.getString(PK_NAME)); pkColumns.add(rs.getString(COLUMN_NAME)); // KEY_SEQ 表示联合主键中的顺序必须记录起来之后排序 } rs.close(); pk.setColumns(pkColumns); return pk; }主键这里有个容易被忽略的问题返回的 ResultSet 不保证按 KEY_SEQ 排序。你需要先把 KEY_SEQ 一起读进内存然后对列排序否则联合主键的列顺序是随机的。我后面实操时真遇上了第一次生成的 DDL 里联合主键顺序是反的。2.3 类型映射先判 JDBC 类型码再看原生类型通用代码搭起来之后马上会遇到第一个跨库不一致点类型映射。我建议用一个独立方法把 JDBC 类型码转换成语义明确的逻辑类型public static String mapJdbcType(int jdbcType, String typeName, int size, int decimalDigits) { switch (jdbcType) { case Types.INTEGER: case Types.SMALLINT: case Types.TINYINT: return int; case Types.BIGINT: return long; case Types.VARCHAR: case Types.NVARCHAR: return String; case Types.DECIMAL: case Types.NUMERIC: return decimal; case Types.DATE: return Date; case Types.TIMESTAMP: return Timestamp; case Types.CLOB: return text; case Types.BLOB: return binary; default: return typeName; } }关键点在顺序和优先级。先看 JDBC 类型码再看原生类型名。达梦里 DECIMAL(10,2) 的 JDBC 类型码是 Types.DECIMALNUMBER 类型有时候返回 Types.NUMERIC有时候返回 Types.DECIMAL两个 case 都要处理否则代码生成会漏分支。注释读取也有一个细节JDBC 元数据接口的 REMARKS 字段在达梦上能返回通过 COMMENT ON 语句写入的备注建表时指定了字段注释也能正常返回。如果你需要的不只是简单的字段注释而是表级注释甚至更复杂的说明getTables 的 REMARKS 可能不够用。我当时的做法是表级注释先用 REMARKS信息不全时再查 ALL_TAB_COMMENTS 做一轮补偿。补偿查询务必限定在某个 schema 下否则多用户模式下会查到别人表里的注释。2.4 大小写策略达梦里最容易被忽略的问题在达梦上表名和模式名的匹配规则与建表时的写法直接相关。默认情况下不带双引号的名称会被转成大写存储。所以库里的表名通过 getTables 返回的通常是 BUY_ORDER_DETAIL 这种全大写。如果你的应用层习惯用驼峰命名 buyOrderDetail直接拿这个名字做 pattern 去匹配是匹配不到的。我在设计通用代码时加了一个策略开关是否对入参表名做统一转大写处理。针对达梦默认开启“无引号名称转大写”的预处理而在 MySQL 这类存储时保留大小写的库上这个开关必须关闭。实现不复杂在调用 getTables 和 getColumns 之前做一次规范化private String normalizeName(String name, boolean upperCaseStrategy) { if (name null) return null; if (upperCaseStrategy) { return name.toUpperCase(); } return name; }这里还有更深一层的问题如果用户建表时用了双引号比如 create table BuyOrderDetail存储名称就是混合大小写直接转大写反而匹配不上。所以更稳的做法是先查一遍表清单把实际表名集合加载到内存应用层再做一次不区分大小写的匹配找到真实表名后再传给 getColumns。虽然多查询一次但换来了稳定。这个方案通用性最好。我最终保留了两层匹配逻辑第一层用规范后的名字直接查第二层查不到时把当前 schema 下的表清单全量拉出来做不区分大小写匹配找到真实表名后再去取列。这段逻辑对上层完全透明在达梦上靠它救了不少场景。3. 实操过程在达梦上完整跑通这套代码3.1 步骤一驱动、连接与版本确认达梦的 JDBC 驱动引用不复杂驱动类是 dm.jdbc.driver.DmDriver连接 URL 形如 jdbc:dm://192.168.x.x:5236/数据库名。驱动包通常是 DmJdbcDriver.jar不同版本内部类名基本稳定。连接参数里第一个要关注的坑是字符集。有的部署环境客户端字符集和数据库服务端不一致会导致元数据查询返回的字符串出现乱码。建议在连接串里显式指定字符集设置或者通过应用侧的连接配置来控制。具体写法依驱动版本略有差异稳妥的做法是先看官方连接参数说明确认支持哪些字符集参数再配置到连接池里。初次接入时强烈建议先写一个独立测试类把数据库版本和驱动版本打印出来DatabaseMetaData md conn.getMetaData(); System.out.println(md.getDatabaseProductName()); System.out.println(md.getDatabaseProductVersion()); System.out.println(md.getDriverName()); System.out.println(md.getDriverVersion());这一步值得做。它会直接告诉你驱动对应的兼容行为和版本后面遇到怪问题时版本信息是排查的重要依据。3.2 步骤二把表清单打印出来用前面的 listTables 方法在达梦实例上跑一次。我的测试过程是先用当前登录用户默认 schema 查询能正常返回一批表接着切换到另一个业务用户模式查询这时需要把 schemaPattern 参数传成对应的大写模式名否则结果为空。输出样例大致是下面这种形式TABLE_SCHEMADMUSER1, TABLE_NAMEBUY_ORDER_DETAIL, TYPETABLE, REMARKS订单明细表 TABLE_SCHEMADMUSER1, TABLE_NAMEPRODUCT_CATEGORY, TYPEVIEW, REMARKS商品分类视图如果 REMARKS 返回空先别急着怀疑代码。到数据库客户端里执行一句SELECT * FROM ALL_TAB_COMMENTS WHERE TABLE_NAME BUY_ORDER_DETAIL;看看能不能查到表注释。如果这里也是空说明表本身就没写注释代码逻辑再对也捞不到内容。3.3 步骤三读取列信息、主键、默认值与自增列信息是整个模块的核心。测试时我特意找了两张表一张是普通流水表字段有十来个类型覆盖 DATE、VARCHAR2、DECIMAL、NUMBER、CLOB另一张是带联合主键、带自增列、带外键的配置表。第一张表遍历结果非常正常类型码、长度、小数位数都按预期返回。第二张表的联合主键出了问题getPrimaryKeys 返回的主键列顺序是乱的KEY_SEQ 有 1、2也有 2、1。我在代码里补上排序逻辑后解决。自增列的判断更值得记录。同一张自增表里自增列在 getColumns 中返回的 IS_AUTOINCREMENT 字段在某些达梦版本里是 YES某些版本里是空。后来我用这种备用 SQL 做补偿SELECT COLUMN_NAME FROM ALL_TAB_IDENTITY_COLS WHERE OWNER UPPER(?) AND TABLE_NAME UPPER(?)这是很典型的思路当 JDBC 元数据拿不到 100% 准确的细节时用目标数据库的系统视图做定向补偿补偿代码用配置开关隔离尽量不让通用主干混入方言逻辑。3.4 步骤四生成一份可执行的 DDL 语句读完元数据后最直接的验证方法是把它反向生成建表语句。这也是判断代码是否“真正读对了”的硬标准。我写了一个简易生成器对每个字段拼出名称、类型、长度、默认值、注释最后拼主键约束。生成过程中遇到两个达梦专属问题。第一个是类型长度拼接。VARCHAR 类型需要长度NUMBER 类型需要精度和小数位但 CLOB、BLOB、DATE 这些类型不需要。如果统一模板处理就会出现 date(0)、clob(0) 这种错误。解决方式是在拼 SQL 时判断 dataTypeif (col.getDataType() Types.VARCHAR || col.getDataType() Types.NVARCHAR) { sb.append(col.getTypeName()).append(().append(col.getColumnSize()).append()); } else if (col.getDataType() Types.DECIMAL || col.getDataType() Types.NUMERIC) { if (col.getDecimalDigits() 0) { sb.append(col.getTypeName()).append(() .append(col.getColumnSize()).append(,) .append(col.getDecimalDigits()).append()); } else { sb.append(col.getTypeName()).append(().append(col.getColumnSize()).append()); } } else { sb.append(col.getTypeName()); }第二个是表名大小写。建表语句如果没加双引号达梦会直接把表名转大写但如果原表是用双引号建的混合大小写表名生成的 DDL 里去掉了引号就会导致重建出来的表名和原表不一致。所以我生成的 DDL 对表名、字段名默认都加双引号。这个做法验证下来最稳代价是数据库里所有对象名称都会被严格固定成引号内的大小写。4. 常见问题与坑位总结4.1 常见问题速查表我把实际操作中最常撞上的问题整理成了一张速查表后面再挑几个展开细说。现象可能原因处理方式getTables 返回空结果schema 传错大小写或 catalog 传了干扰值将 catalog 置空schema 用大写模式名重试表清单能查到但 getColumns 返回空表名大小写不一致实际存储名称与传入 pattern 不同拉取真实表名做不区分大小写匹配后再查询字段注释始终为空表本身没写 COMMENT或驱动版本问题用系统视图补偿查询确认源头有值联合主键顺序错乱驱动返回顺序不稳定KEY_SEQ 需要手工排序读取 KEY_SEQ 字段在内存中按顺序排序NUMBER 类型映射异常驱动返回可能是 NUMERIC也可能是 DECIMAL两个 case 都要处理以 dataType 优先判断CLOB/BLOB 字段拼 DDL 出错通用模板硬拼长度参数按类型分组判断特殊类型不拼接长度自增标志在元数据中为空驱动未完整实现该列查询系统视图补偿用属性开关控制启用4.2 谈几个实际踩过的坑第一个坑与模式有关。刚开始我按常规写法把数据库名传给 catalog用户名传给 schema结果 getTables 返回 0 行排查了很久。后来看驱动日志才发现达梦 JDBC 驱动内部对 catalog 的理解和 MySQL 不完全一样。最后把 catalog 去掉只保留 schema结果才对。第二个坑是注释。有个表的注释在客户端能看到代码里读到却是 null。一开始以为是驱动解析问题后来才发现是补偿查询时没有加 OWNER 条件结果拿到的是另一个同名表的信息。这个经历让我学会了补偿 SQL 里必须显式限定模式绝不能偷懒。第三个坑是生成 DDL 时表名错乱。原因是原设计里 DDL 没有加引号而原表是带引号的混合大小写表名重建之后全变成大写。后来统一给 DDL 里的表名、字段名加双引号问题才消失。第四个坑不算复杂但很有代表性getColumns 返回的 COLUMN_SIZE对 NUMBER 类型字段返回的是精度还是最大字节数不同版本驱动之间并不完全一致。如果你的生成逻辑对后者的依赖很强建议在接正式库前先建一张包含典型类型的测试表把返回结果和客户端里的定义逐项比对一遍。这一步能提前发现驱动的“特殊解释”省得以后在真实环境里反复折腾。4.3 通用代码的边界与后续扩展在达梦上这套代码能稳定跑起来但我也要泼一点冷水。通用代码的优势是覆盖面广劣势是每个数据库的特性拿不全。可以用补偿机制去补齐但补偿补到一定程度后代码会越来越难维护需要有意识地做取舍。我的经验是如果只做表结构展示、字段清单导出、反向生成基础 DDLJDBC 元数据接口完全够用不需要碰系统表。但如果目标是生成精确到分区策略、表空间、索引结构、存储参数、列加密属性这类深水区内容建议额外写一个达梦专属的方言模块用系统表和系统视图作为数据源。通用代码与方言补偿代码分层存在模块边界清晰未来接更多数据库时只需要替换方言模块。我还见过一种延伸做法把通用元数据读取做成命令行小工具输入连接信息输出 JSON 或 Markdown 格式的表结构文档。这个方向很适合做内部的数据字典导出工具。后续如果要继续扩展可以把索引、约束、触发器也逐步纳入模型但前提是在达梦上把这些信息的元数据行为先验证清楚。最后说一点个人体会。通用代码这种东西“通用”是相对而言的我在达梦上验证通过不代表换一台环境、换一个驱动版本就万事大吉。真正要在项目里落地先把数据库版本、驱动版本、用户模式和表命名规范这四样固定下来在一个稳定的环境基线上跑完整套流程然后才开始接其他库。做过一遍你就能明白所谓通用不是代码本身没有差异分支而是差异设计在了它该待的地方。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →