From 768e890e2cd3425cd1c332d06d975881e8dcff5b Mon Sep 17 00:00:00 2001 From: Looly Date: Mon, 29 Apr 2024 12:55:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DOracle=E4=B8=8B=E7=89=B9?= =?UTF-8?q?=E6=AE=8A=E8=A1=A8=E5=90=8D=E5=AF=BC=E8=87=B4meta=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E8=8E=B7=E5=8F=96=E4=B8=8D=E5=88=B0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +- .../main/java/cn/hutool/db/meta/MetaUtil.java | 298 +++++++++++++----- .../java/cn/hutool/db/IssueI9BANETest.java | 5 +- 3 files changed, 228 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05070afcd..27af7174e 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ # 🚀Changelog ------------------------------------------------------------------------------------------------------------- -# 5.8.28(2024-04-28) +# 5.8.28(2024-04-29) ### 🐣新特性 * 【core 】 修正XmlUtil的omitXmlDeclaration描述注释(issue#I9CPC7@Gitee) @@ -28,6 +28,7 @@ * 【core 】 修复FileUtil.copyFile没有创建父目录导致的问题(issue#3557@Github) * 【http 】 修复HttpDownloader全局超时无效问题(issue#3556@Github) * 【core 】 修复ZipReader.checkZipBomb遇到空目录报错问题(issue#I9K494@Gitee) +* 【db 】 修复Oracle下特殊表名导致meta信息获取不到问题(issue#I9BANE@Gitee) ------------------------------------------------------------------------------------------------------------- # 5.8.27(2024-03-29) diff --git a/hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java b/hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java index f1ecafa83..1212fbb3b 100755 --- a/hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java +++ b/hutool-db/src/main/java/cn/hutool/db/meta/MetaUtil.java @@ -2,21 +2,15 @@ package cn.hutool.db.meta; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.db.DbRuntimeException; import cn.hutool.db.DbUtil; import cn.hutool.db.Entity; import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.sql.*; +import java.util.*; /** * 数据库元数据信息工具类 @@ -207,78 +201,57 @@ public class MetaUtil { * @since 5.7.22 */ public static Table getTableMeta(DataSource ds, String catalog, String schema, String tableName) { - final Table table = Table.create(tableName); Connection conn = null; try { conn = ds.getConnection(); - - // catalog和schema获取失败默认使用null代替 - if (null == catalog) { - catalog = getCatalog(conn); - } - table.setCatalog(catalog); - if (null == schema) { - schema = getSchema(conn); - } - table.setSchema(schema); - - final DatabaseMetaData metaData = conn.getMetaData(); - - // 获得表元数据(表注释) - try (final ResultSet rs = metaData.getTables(catalog, schema, tableName, new String[]{TableType.TABLE.value()})) { - if (null != rs) { - if (rs.next()) { - table.setComment(rs.getString("REMARKS")); - } - } - } - - // 获得主键 - try (final ResultSet rs = metaData.getPrimaryKeys(catalog, schema, tableName)) { - if (null != rs) { - while (rs.next()) { - table.addPk(rs.getString("COLUMN_NAME")); - } - } - } - - // 获得列 - try (final ResultSet rs = metaData.getColumns(catalog, schema, tableName, null)) { - if (null != rs) { - while (rs.next()) { - table.setColumn(Column.create(table, rs)); - } - } - } - - // 获得索引信息(since 5.7.23) - try (final ResultSet rs = metaData.getIndexInfo(catalog, schema, tableName, false, false)) { - final Map indexInfoMap = new LinkedHashMap<>(); - if (null != rs) { - while (rs.next()) { - //排除tableIndexStatistic类型索引 - if (0 == rs.getShort("TYPE")) { - continue; - } - - final String indexName = rs.getString("INDEX_NAME"); - final String key = StrUtil.join("&", tableName, indexName); - // 联合索引情况下一个索引会有多个列,此处须组合索引列到一个索引信息对象下 - IndexInfo indexInfo = indexInfoMap.get(key); - if (null == indexInfo) { - indexInfo = new IndexInfo(rs.getBoolean("NON_UNIQUE"), indexName, tableName, schema, catalog); - indexInfoMap.put(key, indexInfo); - } - indexInfo.getColumnIndexInfoList().add(ColumnIndexInfo.create(rs)); - } - } - table.setIndexInfoList(ListUtil.toList(indexInfoMap.values())); - } - } catch (SQLException e) { - throw new DbRuntimeException("Get columns error!", e); + return getTableMeta(conn, catalog, schema, tableName); + } catch (final SQLException e){ + throw new DbRuntimeException(e); } finally { - DbUtil.close(conn); + IoUtil.close(conn); } + } + + /** + * 获得表的元信息
+ * 注意如果需要获取注释,某些数据库如MySQL,需要在配置中添加: + *
+	 *     remarks = true
+	 *     useInformationSchema = true
+	 * 
+ * + * @param conn 数据库连接对象,使用结束后不会关闭。 + * @param tableName 表名 + * @param catalog catalog name,{@code null}表示自动获取,见:{@link #getCatalog(Connection)} + * @param schema a schema name pattern,{@code null}表示自动获取,见:{@link #getSchema(Connection)} + * @return Table对象 + * @since 5.8.28 + */ + public static Table getTableMeta(final Connection conn, String catalog, String schema, final String tableName) { + final Table table = Table.create(tableName); + + // catalog和schema获取失败默认使用null代替 + if (null == catalog) { + catalog = getCatalog(conn); + } + table.setCatalog(catalog); + if (null == schema) { + schema = getSchema(conn); + } + table.setSchema(schema); + + final DatabaseMetaData metaData = getMetaData(conn); + // 获取原始表名 + final String pureTableName = unWrapIfOracle(metaData, tableName); + // 获得表元数据(表注释) + table.setComment(getRemarks(metaData, catalog, schema, pureTableName)); + // 获得主键 + table.setPkNames(getPrimaryKeys(metaData, catalog, schema, pureTableName)); + // 获得列 + fetchColumns(metaData, catalog, schema, table); + // 获得索引信息(since 5.7.23) + final Map indexInfoMap = getIndexInfo(metaData, catalog, schema, tableName); + table.setIndexInfoList(ListUtil.toList(indexInfoMap.values())); return table; } @@ -335,4 +308,177 @@ public class MetaUtil { return null; } + + /** + * 获取数据库连接的元数据信息。 + * + * @param conn 数据库连接对象。如果连接为null,则返回null。 + * @return DatabaseMetaData 数据库元数据对象,如果获取失败或连接为null,则返回null。 + * @since 5.8.28 + */ + public static DatabaseMetaData getMetaData(final Connection conn) { + if (null == conn) { + return null; + } + try { + return conn.getMetaData(); + } catch (final SQLException e) { + // ignore + } + + return null; + } + + /** + * 获取指定表的备注信息。 + * + * @param metaData 数据库元数据,用于查询表信息。 + * @param catalog 目录名称,用于指定查询的数据库(可为{@code null},表示任意目录)。 + * @param schema 方案名称,用于指定表所属的schema(可为{@code null},表示任意schema)。 + * @param tableName 表名称,指定要查询备注信息的表。 + * @return 表的备注信息。未找到指定的表或查询成功但无结果,则返回null。 + * @since 5.8.28 + */ + public static String getRemarks(final DatabaseMetaData metaData, final String catalog, final String schema, String tableName) { + // issue#I9BANE Oracle中特殊表名需要解包 + tableName = unWrapIfOracle(metaData, tableName); + + try (final ResultSet rs = metaData.getTables(catalog, schema, tableName, new String[]{TableType.TABLE.value()})) { + if (null != rs) { + if (rs.next()) { + return rs.getString("REMARKS"); + } + } + } catch (final SQLException e) { + throw new DbRuntimeException(e); + } + // 未找到指定的表或查询成功但无结果 + return null; + } + + /** + * 获取指定表的主键列名列表。 + * + * @param metaData 数据库元数据,用于查询主键信息。 + * @param catalog 数据库目录,用于限定查询范围。 + * @param schema 数据库模式,用于限定查询范围。 + * @param tableName 表名,指定要查询主键的表。 + * @return 主键列名的列表。如果表没有主键,则返回空列表。 + * @throws DbRuntimeException 如果查询过程中发生SQLException,将抛出DbRuntimeException。 + * @since 5.8.28 + */ + public static Set getPrimaryKeys(final DatabaseMetaData metaData, final String catalog, final String schema, String tableName) { + // issue#I9BANE Oracle中特殊表名需要解包 + tableName = unWrapIfOracle(metaData, tableName); + + // 初始化主键列表 + Set primaryKeys = null; + try (final ResultSet rs = metaData.getPrimaryKeys(catalog, schema, tableName)) { + // 如果结果集不为空,遍历结果集获取主键列名 + if (null != rs) { + primaryKeys = new LinkedHashSet<>(rs.getFetchSize(), 1); + while (rs.next()) { + primaryKeys.add(rs.getString("COLUMN_NAME")); + } + } + } catch (final SQLException e) { + // 将SQLException转换为自定义的DbException抛出 + throw new DbRuntimeException(e); + } + return primaryKeys; + } + + /** + * 获取指定表的索引信息。 + * + * @param metaData 数据库元数据,用于查询索引信息。 + * @param catalog 数据库目录,用于限定查询范围。 + * @param schema 数据库模式,用于限定查询范围。 + * @param tableName 需要查询索引信息的表名。 + * @return 返回一个映射,其中包含表的索引信息。键是表名和索引名的组合,值是索引信息对象。 + * @since 5.8.28 + */ + public static Map getIndexInfo(final DatabaseMetaData metaData, final String catalog, final String schema, final String tableName) { + final Map indexInfoMap = new LinkedHashMap<>(); + + try (final ResultSet rs = metaData.getIndexInfo(catalog, schema, tableName, false, false)) { + if (null != rs) { + while (rs.next()) { + //排除统计(tableIndexStatistic)类型索引 + if (0 == rs.getShort("TYPE")) { + continue; + } + + final String indexName = rs.getString("INDEX_NAME"); + final String key = StrUtil.join("&", tableName, indexName); + // 联合索引情况下一个索引会有多个列,此处须组合索引列到一个索引信息对象下 + IndexInfo indexInfo = indexInfoMap.get(key); + if (null == indexInfo) { + indexInfo = new IndexInfo(rs.getBoolean("NON_UNIQUE"), indexName, tableName, schema, catalog); + indexInfoMap.put(key, indexInfo); + } + indexInfo.getColumnIndexInfoList().add(ColumnIndexInfo.create(rs)); + } + } + } catch (final SQLException e) { + throw new DbRuntimeException(e); + } + return indexInfoMap; + } + + /** + * 判断当前数据库是否为Oracle。 + * + * @param metaData 数据库元数据,用于获取数据库产品名称。 + * @return 返回true表示当前数据库是Oracle,否则返回false。 + * @throws DbRuntimeException 如果获取数据库产品名称时发生SQLException,将抛出DbRuntimeException。 + * @since 5.8.28 + */ + public static boolean isOracle(final DatabaseMetaData metaData) throws DbRuntimeException { + try { + return StrUtil.equalsIgnoreCase("Oracle", metaData.getDatabaseProductName()); + } catch (final SQLException e) { + throw new DbRuntimeException(e); + } + } + + /** + * 如果是在Oracle数据库中并且表名被双引号包裹,则移除这些引号。 + * + * @param metaData 数据库元数据,用于判断是否为Oracle数据库。 + * @param tableName 待处理的表名,可能被双引号包裹。 + * @return 处理后的表名,如果原表名被双引号包裹且是Oracle数据库,则返回去除了双引号的表名;否则返回原表名。 + */ + private static String unWrapIfOracle(final DatabaseMetaData metaData, String tableName) { + final char wrapChar = '"'; + // 判断表名是否被双引号包裹且当前数据库为Oracle,如果是,则移除双引号 + if (StrUtil.isWrap(tableName, wrapChar) && isOracle(metaData)) { + tableName = StrUtil.unWrap(tableName, wrapChar); + } + return tableName; + } + + /** + * 从数据库元数据中获取指定表的列信息。 + * + * @param metaData 数据库元数据,用于查询列信息。 + * @param catalog 数据库目录,用于过滤列信息。 + * @param schema 数据库模式,用于过滤列信息。 + * @param table 表对象,用于存储获取到的列信息。 + */ + private static void fetchColumns(final DatabaseMetaData metaData, final String catalog, final String schema, final Table table) { + // issue#I9BANE Oracle中特殊表名需要解包 + final String tableName = unWrapIfOracle(metaData, table.getTableName()); + + // 获得列 + try (final ResultSet rs = metaData.getColumns(catalog, schema, tableName, null)) { + if (null != rs) { + while (rs.next()) { + table.setColumn(Column.create(table, rs)); + } + } + } catch (final SQLException e) { + throw new DbRuntimeException(e); + } + } } diff --git a/hutool-db/src/test/java/cn/hutool/db/IssueI9BANETest.java b/hutool-db/src/test/java/cn/hutool/db/IssueI9BANETest.java index 4a73ddb65..f2189ece8 100644 --- a/hutool-db/src/test/java/cn/hutool/db/IssueI9BANETest.java +++ b/hutool-db/src/test/java/cn/hutool/db/IssueI9BANETest.java @@ -19,6 +19,9 @@ public class IssueI9BANETest { final DataSource ds = DSFactory.get("orcl"); final Table tableMeta = MetaUtil.getTableMeta(ds, null, null, "\"1234\""); - Console.log(tableMeta.getIndexInfoList()); + Console.log("remarks: " + tableMeta.getComment()); + Console.log("pks: " + tableMeta.getPkNames()); + Console.log("columns: " + tableMeta.getColumns()); + Console.log("index: " + tableMeta.getIndexInfoList()); } }