基于RuoYi-Vue 3.8.9(Spring Boot 3.x / Jakarta EE),零侵入扩展动态数据源管理、元数据自动提取与持久化能力,支持MySQL、Oracle、PostgreSQL、SQL Server四种数据库。

一、为什么需要这个模块?

企业级项目中,经常需要对接多个异构数据库——业务库、报表库、第三方数据源。若依原版只支持单数据源,扩展多数据源能力是刚需。

同时,BI和低代码场景需要自动感知数据库结构(有哪些表、每个字段什么类型),手动维护元数据不现实,必须实现元数据自动提取 + 增量同步

这个模块的核心价值:

  • 动态创建/销毁数据源:运行时通过API添加数据源,无需重启
  • 多数据库适配:MySQL、Oracle、PostgreSQL、SQL Server,统一接口
  • 元数据自动提取:基于JDBC DatabaseMetaData + 各数据库系统表,提取表/列/索引/主键信息
  • 增量同步持久化:三路归并策略(新增INSERT、更新提取器字段、源端删除标记停用),不覆盖用户手动修改
  • Java类型自动映射:数据库原生类型 → Java类型,直接用于代码生成和BI建模

二、整体架构

plaintext

┌────────────────────────────────────────────────────────────────┐
│                        Controller 层                            │
│  SysTableMetaController (元数据CRUD + 同步触发)                  │
└──────────────────────────┬─────────────────────────────────────┘
                           │
┌──────────────────────────▼─────────────────────────────────────┐
│                        Service 层                               │
│  MetaSyncServiceImpl (增量同步核心逻辑)                          │
└───────┬──────────────────────────────────────┬─────────────────┘
        │                                      │
┌───────▼───────────┐              ┌───────────▼──────────────────┐
│  DataSourceManager │              │  persist (持久化)             │
│  (数据源管理核心)   │              │  SysTableMetaMapper          │
│                    │              │  SysColumnMetaMapper          │
│  ┌──────────────┐ │              └──────────────────────────────┘
│  │DataSourceEntry│ │
│  │(车+司机+订单) │ │
│  └──────────────┘ │
└───┬──────┬───────┘
    │      │
    │      │
┌───▼──┐ ┌─▼──────────────────────────────────────────────────┐
│builder│ │ metadata (元数据提取)                               │
│      │ │                                                      │
│Druid │ │ AbstractMetaDataExtractor (模板方法模式)              │
│Hikari│ │   ├─ MySqlMetaDataExtractor     (information_schema) │
│      │ │   ├─ OracleMetaDataExtractor    (ALL_TABLES)         │
│      │ │   ├─ PostgresMetaDataExtractor  (pg_catalog)         │
│      │ │   └─ SqlServerMetaDataExtractor (sys.objects)        │
│      │ │                                                      │
│      │ │ ColumnMeta / TableMeta / IndexMeta / PrimaryKeyMeta  │
└──────┘ └──────────────────────────────────────────────────────┘
    │
┌───▼──────────────────────────────────────────────────────────┐
│ executor (SQL执行)                                            │
│                                                              │
│ AbstractJdbcExecutor (通用JDBC执行)                            │
│   ├─ MySqlExecutor                                           │
│   ├─ OracleExecutor    (testSql=SELECT 1 FROM DUAL)          │
│   ├─ PostgresExecutor                                        │
│   └─ SqlServerExecutor                                       │
└──────────────────────────────────────────────────────────────┘

设计理念:车管所模型

用一个形象的比喻来理解整个架构:

表格

概念 对应代码 说明
车管所 DataSourceManager 管理所有数据源的注册、查找、移除
车牌号 key (String) 数据源唯一标识,如 "order_db"
DataSource (连接池) 实际的数据库连接池
司机 DataSourceExecutor 知道怎么开特定数据库的车
订单 DataSourceConfig 数据源配置信息(URL、用户名、密码等)
条目 DataSourceEntry 把车+司机+订单绑定在一起

调用者只需要知道车牌号,车管所自动找到对应的车和司机完成任务。

三、核心代码详解

3.1 数据源配置 — DataSourceConfig

纯POJO,零框架依赖,所有数据源配置的载体:

public class DataSourceConfig implements Serializable {
    private String key;              // 唯一标识,如 "order_db"
    private String dbType;           // 数据库类型: MySQL / PostgreSQL / Oracle / SQLServer
    private String poolType;         // 连接池类型: Hikari / Druid
    private String url;              // 连接地址
    private String username;         // 用户名
    private String password;         // 密码(AES加密存储)
    private Integer minIdle = 5;     // 最小空闲连接数
    private Integer maxPoolSize = 20; // 最大连接数
    private Long idleTimeout = 600000L;  // 空闲超时(ms)
    private Long maxLifetime = 1800000L; // 最大生命周期(ms)
    private Long connectionTimeout = 30000L; // 连接超时(ms)
    private Map<String, String> extraProps; // 额外连接池参数
}

3.2 数据源管理器 — DataSourceManager

核心类,所有操作的入口:

public class DataSourceConfig implements Serializable {
    private String key;              // 唯一标识,如 "order_db"
    private String dbType;           // 数据库类型: MySQL / PostgreSQL / Oracle / SQLServer
    private String poolType;         // 连接池类型: Hikari / Druid
    private String url;              // 连接地址
    private String username;         // 用户名
    private String password;         // 密码(AES加密存储)
    private Integer minIdle = 5;     // 最小空闲连接数
    private Integer maxPoolSize = 20; // 最大连接数
    private Long idleTimeout = 600000L;  // 空闲超时(ms)
    private Long maxLifetime = 1800000L; // 最大生命周期(ms)
    private Long connectionTimeout = 30000L; // 连接超时(ms)
    private Map<String, String> extraProps; // 额外连接池参数
}

3.3 数据源条目 — DataSourceEntry

把DataSource、Executor、Config绑在一起,对外提供统一操作方法:

public class DataSourceEntry {
    private final DataSource dataSource;       // 车
    private final DataSourceExecutor executor;  // 司机
    private final DataSourceConfig config;      // 订单单
    private MetaDataExtractor metaExtractor;    // 元数据提取器

    public List<Map<String, Object>> executeQuery(String sql) {
        return executor.executeQuery(dataSource, sql);
    }

    public List<ColumnMeta> extractColumns(String tableName) {
        return metaExtractor.extractColumns(dataSource, tableName);
    }

    public DatabaseMetadata extractDatabaseMetadata() {
        return metaExtractor.extractDatabaseMetadata(dataSource);
    }
    // ... listTables, testConnection, extractIndexes, extractPrimaryKey 等
}

3.4 连接池构建器 — Builder模式

工厂 + 策略模式,支持HikariCP和Druid两种连接池:

// 构建器接口
public interface DataSourceBuilder {
    String poolType();
    DataSource build(DataSourceConfig config);
}

// Druid构建器(示例)
public class DruidDataSourceBuilder implements DataSourceBuilder {
    private final String aesKey;

    public DataSource build(DataSourceConfig config) {
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(config.getUrl());
        ds.setUsername(config.getUsername());
        ds.setPassword(AESEncryptUtil.decrypt(config.getPassword(), aesKey)); // AES解密
        ds.setInitialSize(config.getMinIdle());
        ds.setMinIdle(config.getMinIdle());
        ds.setMaxActive(config.getMaxPoolSize());
        ds.setValidationQuery(getValidationQuery(config.getDbType()));
        ds.setTestWhileIdle(true);
        ds.init();
        return ds;
    }
}

// 工厂
public class DataSourceBuilderFactory {
    private final Map<String, DataSourceBuilder> builders = new HashMap<>();

    public void registerBuilder(DataSourceBuilder builder) {
        builders.put(builder.poolType(), builder);
    }

    public DataSourceBuilder getBuilder(String poolType) { ... }
}

3.5 SQL执行器 — 模板方法模式

通用JDBC执行逻辑放在抽象基类,子类只需覆盖差异部分:

// 构建器接口
public interface DataSourceBuilder {
    String poolType();
    DataSource build(DataSourceConfig config);
}

// Druid构建器(示例)
public class DruidDataSourceBuilder implements DataSourceBuilder {
    private final String aesKey;

    public DataSource build(DataSourceConfig config) {
        DruidDataSource ds = new DruidDataSource();
        ds.setUrl(config.getUrl());
        ds.setUsername(config.getUsername());
        ds.setPassword(AESEncryptUtil.decrypt(config.getPassword(), aesKey)); // AES解密
        ds.setInitialSize(config.getMinIdle());
        ds.setMinIdle(config.getMinIdle());
        ds.setMaxActive(config.getMaxPoolSize());
        ds.setValidationQuery(getValidationQuery(config.getDbType()));
        ds.setTestWhileIdle(true);
        ds.init();
        return ds;
    }
}

// 工厂
public class DataSourceBuilderFactory {
    private final Map<String, DataSourceBuilder> builders = new HashMap<>();

    public void registerBuilder(DataSourceBuilder builder) {
        builders.put(builder.poolType(), builder);
    }

    public DataSourceBuilder getBuilder(String poolType) { ... }
}

四、元数据提取 — 核心设计

4.1 接口定义

public interface MetaDataExtractor {
    String dbType();  // 我提取哪种数据库

    DatabaseMetadata extractDatabaseMetadata(DataSource ds);  // 整库信息
    List<TableMeta> extractTablesWithDetails(DataSource ds);  // 所有表详情
    TableMeta extractTableDetail(DataSource ds, String tableName);  // 单表详情
    List<TableMeta> extractTableList(DataSource ds);  // 表列表(轻量)
    List<ColumnMeta> extractColumns(DataSource ds, String tableName);  // 单表列信息
    Map<String, List<ColumnMeta>> extractColumnsForTables(DataSource ds, List<String> tableNames);  // 批量列信息
    List<IndexMeta> extractIndexes(DataSource ds, String tableName);  // 索引信息
    PrimaryKeyMeta extractPrimaryKey(DataSource ds, String tableName);  // 主键信息
}

4.2 模板方法模式 — AbstractMetaDataExtractor

抽象基类用JDBC DatabaseMetaData 提供通用默认实现,子类用各数据库系统表SQL覆盖,获取更丰富的元信息:

public abstract class AbstractMetaDataExtractor implements MetaDataExtractor {

    // ===== 列信息提取(核心) =====
    @Override
    protected List<ColumnMeta> doExtractColumns(Connection conn, String schema, String tableName)
            throws SQLException {
        // 默认实现:JDBC DatabaseMetaData
        DatabaseMetaData meta = conn.getMetaData();

        // 先拿主键列名集合
        Set<String> pkColumns = new HashSet<>();
        try (ResultSet rs = meta.getPrimaryKeys(resolveCatalog(conn), schema, tableName)) {
            while (rs.next()) { pkColumns.add(rs.getString("COLUMN_NAME")); }
        }

        // 拿列信息
        try (ResultSet rs = meta.getColumns(resolveCatalog(conn), schema, tableName, "%")) {
            while (rs.next()) {
                ColumnMeta col = new ColumnMeta();
                col.setColumnName(rs.getString("COLUMN_NAME"));
                col.setTypeName(rs.getString("TYPE_NAME"));
                col.setColumnSize(rs.getLong("COLUMN_SIZE"));
                col.setNullable(rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable);
                col.setPrimaryKey(pkColumns.contains(col.getColumnName()));
                col.setRemarks(rs.getString("REMARKS"));
                // Java类型映射(子类可覆盖)
                col.setJavaType(mapJavaType(col.getTypeName(), col.getColumnSize(), col.getDecimalDigits()));
                columns.add(col);
            }
        }
        return columns;
    }

    // ===== 批量列信息(复用连接,避免N+1) =====
    @Override
    public Map<String, List<ColumnMeta>> extractColumnsForTables(DataSource ds, List<String> tableNames) {
        try (Connection conn = ds.getConnection()) {
            Map<String, List<ColumnMeta>> result = new LinkedHashMap<>();
            for (String tableName : tableNames) {
                result.put(tableName, doExtractColumns(conn, resolveSchema(conn), tableName));
            }
            return result;
        }
    }

    // 子类必须实现
    protected abstract String resolveSchema(Connection conn) throws SQLException;
    protected abstract String resolveCatalog(Connection conn) throws SQLException;
    protected abstract String mapJavaType(String sqlType, Long size, Integer digits);
}

4.3 MySQL提取器 — 覆盖系统表实现

MySQL用 information_schema 获取最丰富的元信息:

public class MySqlMetaDataExtractor extends AbstractMetaDataExtractor {

    @Override
    protected String resolveSchema(Connection conn) { return null; }  // MySQL不用schema

    @Override
    protected String resolveCatalog(Connection conn) { return conn.getCatalog(); }  // catalog=数据库名

    // 表列表:information_schema.TABLES
    @Override
    protected List<TableMeta> doExtractTableList(Connection conn, String schema) throws SQLException {
        String sql = "SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT, TABLE_SCHEMA, " +
                "ENGINE, TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH, INDEX_LENGTH, " +
                "AUTO_INCREMENT, TABLE_COLLATION, CREATE_TIME, UPDATE_TIME " +
                "FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?";
        // ... PreparedStatement执行并映射到TableMeta
    }

    // 列信息:information_schema.COLUMNS(比JDBC元信息更丰富,含CHARSET/COLLATION)
    @Override
    protected List<ColumnMeta> doExtractColumns(Connection conn, String schema, String tableName)
            throws SQLException {
        String sql = "SELECT COLUMN_NAME, COLUMN_TYPE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, " +
                "NUMERIC_PRECISION, NUMERIC_SCALE, IS_NULLABLE, COLUMN_DEFAULT, " +
                "COLUMN_KEY, EXTRA, COLUMN_COMMENT, ORDINAL_POSITION, " +
                "CHARACTER_SET_NAME, COLLATION_NAME " +
                "FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? " +
                "ORDER BY ORDINAL_POSITION";
        // ... 映射时 col.setCharset / col.setCollation 等MySQL特有字段
    }

    // MySQL Java类型映射
    @Override
    protected String mapJavaType(String sqlType, Long size, Integer digits) {
        return switch (sqlType.toLowerCase()) {
            case "bigint" -> "Long";
            case "int", "integer", "mediumint" -> "Integer";
            case "tinyint" -> size != null && size == 1 ? "Boolean" : "Integer";
            case "decimal", "numeric" -> "BigDecimal";
            case "datetime", "timestamp" -> "LocalDateTime";
            case "date" -> "LocalDate";
            case "float", "real" -> "Float";
            case "double" -> "Double";
            case "blob", "longblob" -> "byte[]";
            default -> "String";  // varchar, char, text, json...
        };
    }
}

4.4 SQL Server提取器

SQL Server用 sys.objects / sys.columns / sys.indexes 系统视图:

public class SqlServerMetaDataExtractor extends AbstractMetaDataExtractor {

    @Override
    protected String resolveSchema(Connection conn) { return "dbo"; }  // 默认schema

    // 列信息:sys.columns + sys.types + sys.extended_properties
    @Override
    protected List<ColumnMeta> doExtractColumns(Connection conn, String schema, String tableName)
            throws SQLException {
        String sql = "SELECT c.name AS column_name, tp.name AS data_type, " +
                "c.max_length, c.precision, c.scale, c.is_nullable, " +
                "dc.definition AS default_value, c.is_identity, " +
                "c.column_id, ep.value AS remarks " +
                "FROM sys.columns c " +
                "INNER JOIN sys.tables t ON c.object_id = t.object_id " +
                "INNER JOIN sys.schemas s ON t.schema_id = s.schema_id " +
                "INNER JOIN sys.types tp ON c.user_type_id = tp.user_type_id " +
                "LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id " +
                "LEFT JOIN sys.extended_properties ep ON c.object_id = ep.major_id " +
                "  AND c.column_id = ep.minor_id AND ep.name = 'MS_Description' " +
                "WHERE s.name = ? AND t.name = ? ORDER BY c.column_id";
        // ...
    }

    // SQL Server类型名构建
    private String buildSqlServerTypeName(String dataType, int maxLength, int precision, int scale) {
        return switch (dataType.toLowerCase()) {
            case "varchar", "nvarchar" -> {
                int len = maxLength == -1 ? -1 : (dataType.startsWith("n") ? maxLength / 2 : maxLength);
                yield len == -1 ? dataType + "(MAX)" : dataType + "(" + len + ")";
            }
            case "decimal", "numeric" -> dataType + "(" + precision + "," + scale + ")";
            default -> dataType;
        };
    }

    // SQL Server Java类型映射
    private String mapSqlServerJavaType(String sqlType, Long size, Integer digits) {
        return switch (sqlType.toLowerCase()) {
            case "bigint" -> "Long";
            case "int" -> "Integer";
            case "bit" -> "Boolean";
            case "decimal", "numeric", "money" -> "BigDecimal";
            case "datetime", "datetime2" -> "LocalDateTime";
            case "datetimeoffset" -> "OffsetDateTime";
            case "uniqueidentifier" -> "String";
            default -> "String";
        };
    }
}

4.5 ColumnMeta — 列元信息模型

public class ColumnMeta implements Serializable {
    private String columnName;       // 列名
    private String typeName;         // 数据库原生类型名(如 varchar(255), int(11))
    private Long columnSize;         // 列大小(精度)
    private Integer decimalDigits;   // 小数位数
    private Boolean nullable;        // 是否允许NULL
    private String defaultValue;     // 默认值
    private Boolean primaryKey;      // 是否主键
    private Boolean autoIncrement;   // 是否自增
    private String remarks;          // 列注释
    private Integer ordinalPosition; // 列序号
    private String charset;          // 字符集(MySQL)
    private String collation;        // 排序规则(MySQL)
    private String javaType;         // Java类型映射建议(核心!)
}

javaType 字段是整个元数据模块的关键输出,它把数据库原生类型映射为Java类型名(如 StringLongBigDecimalLocalDateTime),后续BI建模的维度/度量自动判断、代码生成等场景都依赖它。

4.6 Java类型映射对照表

表格

MySQL SQL Server Java类型
bigint bigint Long
int, integer, mediumint, smallint, tinyint(size>1) int, smallint, tinyint Integer
tinyint(size=1) bit Boolean
decimal, numeric decimal, numeric, money, smallmoney BigDecimal
float, real float, real Float / Double
double, double precision - Double
date date LocalDate
datetime, timestamp datetime, datetime2, smalldatetime LocalDateTime
- datetimeoffset OffsetDateTime
time time LocalTime
year - Integer
blob, binary, varbinary varbinary, binary, image, timestamp byte[]
json xml, uniqueidentifier String
varchar, char, text, longtext, enum, set... varchar, nvarchar, char, nchar, text... String

五、元数据持久化与增量同步

5.1 持久化表结构

元数据提取结果持久化到两张表,与若依 sys_ 命名规范对齐:

sys_table_meta — 表元信息

表格

字段 类型 说明
id BIGINT PK 主键
table_code VARCHAR(100) 物理表名(提取器同步)
table_name VARCHAR(100) 显示名称(优先取注释,用户可改)
parent_id BIGINT 父节点ID(树形结构)
datasource_id BIGINT 关联数据源ID
database_name VARCHAR(100) 库名
table_type VARCHAR(20) TABLE / VIEW
physical_table VARCHAR(200) 物理表名(提取器同步)
description VARCHAR(500) 描述(用户可编辑)
is_export INT 是否可导出(用户可编辑)
is_extract INT 是否可抽取(用户可编辑)
is_temp INT 是否临时表
status INT 状态:0正常 1停用
del_flag CHAR(1) 删除标志

sys_column_meta — 列元信息

表格

字段 类型 说明
id BIGINT PK 主键
table_id BIGINT FK 关联表ID
column_code VARCHAR(100) 物理列名(提取器同步)
column_name VARCHAR(100) 显示名称(优先取注释,用户可改)
column_desc VARCHAR(500) 列描述(用户可编辑)
data_type VARCHAR(100) 数据库原生类型(提取器同步)
column_size BIGINT 列大小
decimal_digits BIGINT 小数位数
is_nullable INT 是否允许NULL
default_value VARCHAR(200) 默认值
is_primary_key INT 是否主键
is_auto_increment INT 是否自增
view_type VARCHAR(50) 展示类型/Java类型(用户可编辑)
view_width VARCHAR(20) 列展示宽度
is_sensitive INT 是否敏感字段(用户可编辑)
is_visible INT 是否可见(用户可编辑)
column_comment VARCHAR(500) 列注释(提取器同步)
sort_order BIGINT 排序号
status INT 状态:0正常 1停用

关键设计:字段分为"提取器同步字段"和"用户可编辑字段"两组,增量同步时只更新提取器字段,不覆盖用户手动修改的内容。

5.2 增量同步 — 三路归并策略

MetaSyncServiceImpl.syncMetadata(datasourceId, datasourceKey)

核心流程:

1. 提取整库元信息(一次连接)
   └─ DataSourceManager.extractDatabaseMetadata(key)
      └─ MySqlMetaDataExtractor.extractDatabaseMetadata(ds)
         └─ information_schema.TABLES → List<TableMeta>

2. 批量提取所有表的列信息(一次连接,避免N+1)
   └─ DataSourceManager.extractColumnsForTables(key, tableNames)
      └─ 循环 doExtractColumns(conn, schema, tableName) 复用同一Connection

3. 查询已持久化记录,按 table_code 建立 Map

4. 三路归并:
   ├─ 新增表 → INSERT(表+列)
   ├─ 已有表 → 只更新提取器字段(不碰用户字段)
   └─ 源端已删除的表 → 标记停用(status=1),不物理删除

5. 列级别同理三路归并:
   ├─ 新增列 → INSERT
   ├─ 已有列 → 只更新提取器字段
   └─ 源端删除的列 → 标记停用

5.3 增量同步关键代码

@Service
public class MetaSyncServiceImpl implements IMetaSyncService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public int syncMetadata(Long datasourceId, String datasourceKey) {
        // 1. 提取整库元信息(一次连接)
        DatabaseMetadata dbMeta = dataSourceManager.extractDatabaseMetadata(datasourceKey);
        List<TableMeta> extractedTables = dbMeta.getTables();

        // 2. 批量提取列信息(一次连接,避免N+1)
        List<String> tableNames = extractedTables.stream()
                .map(TableMeta::getTableName).collect(Collectors.toList());
        Map<String, List<ColumnMeta>> allColumnsMap =
                dataSourceManager.extractColumnsForTables(datasourceKey, tableNames);

        // 3. 查已有记录
        List<SysTableMeta> existingTables = tableMetaMapper.selectByDatasourceId(datasourceId);
        Map<String, SysTableMeta> existingMap = existingTables.stream()
                .collect(Collectors.toMap(SysTableMeta::getTableCode, t -> t));

        Set<String> extractedCodes = new HashSet<>();
        for (TableMeta t : extractedTables) { extractedCodes.add(t.getTableName()); }

        // 4. 三路归并
        for (TableMeta extracted : extractedTables) {
            SysTableMeta existing = existingMap.get(extracted.getTableName());
            if (existing == null) {
                // 情况1:新增 → INSERT
                SysTableMeta newTable = buildNewTable(extracted, datasourceId, databaseName);
                tableMetaMapper.insertTableMeta(newTable);
                // 批量写入列
                columnMetaMapper.batchInsertColumnMeta(buildNewColumns(...));
            } else {
                // 情况2:已有 → 只更新提取器字段
                updateExtractedFields(existing, extracted, databaseName);
                tableMetaMapper.updateTableMetaExtracted(existing);
                // 列级别增量同步
                syncColumns(existing.getId(), allColumnsMap.get(extracted.getTableName()));
            }
        }

        // 情况3:源端已删除 → 标记停用
        for (SysTableMeta existing : existingTables) {
            if (!extractedCodes.contains(existing.getTableCode())) {
                existing.setStatus(1);  // 停用
                tableMetaMapper.updateTableMetaStatus(existing);
                columnMetaMapper.updateStatusByTableId(existing.getId(), 1);
            }
        }
    }

    // 更新提取器字段(不碰用户字段)
    private void updateExtractedFields(SysTableMeta existing, TableMeta extracted, String dbName) {
        existing.setTableName(extracted.getRemarks() != null && !extracted.getRemarks().isEmpty()
                ? extracted.getRemarks() : extracted.getTableName());
        existing.setTableType(extracted.getTableType());
        existing.setPhysicalTable(extracted.getTableName());
        existing.setDatabaseName(dbName);
        // 不更新:description / is_export / is_extract / status / parent_id / remark
    }

    // 列更新同理:只更新提取器字段
    private void updateColumnExtractedFields(SysColumnMeta existing, ColumnMeta extracted) {
        existing.setColumnName(extracted.getRemarks() != null && !extracted.getRemarks().isEmpty()
                ? extracted.getRemarks() : extracted.getColumnName());
        existing.setDataType(extracted.getTypeName());
        existing.setColumnSize(extracted.getColumnSize());
        existing.setIsNullable(extracted.getNullable() ? 1 : 0);
        existing.setIsPrimaryKey(extracted.getPrimaryKey() ? 1 : 0);
        // 不更新:column_desc / view_type / view_width / is_sensitive / is_visible / status
    }
}

5.4 同步接口

@RestController
@RequestMapping("/system/tablemeta")
public class SysTableMetaController extends BaseController {

    // 触发同步(异步执行,立即返回)
    @PostMapping("/sync/{datasourceId}")
    public AjaxResult syncMetadata(@PathVariable Long datasourceId,
                                    @RequestParam String datasourceKey) {
        metaSyncService.syncMetadataAsync(datasourceId, datasourceKey);
        return success("同步任务已提交,请稍后刷新页面查看结果");
    }

    // 表列表(分页)
    @GetMapping("/list")
    public TableDataInfo list(SysTableMeta tableMeta) { ... }

    // 表详情(含列信息)
    @GetMapping("/detail/{id}")
    public AjaxResult detail(@PathVariable Long id) { ... }

    // 列元信息修改
    @PutMapping("/column/edit")
    public AjaxResult editColumn(@RequestBody SysColumnMeta columnMeta) { ... }

    // 批量修改列元信息
    @PutMapping("/column/batchEdit")
    public AjaxResult batchEditColumn(@RequestBody List<SysColumnMeta> columnList) { ... }
}

六、包结构总览

com.company.datasource
├── builder/                          # 连接池构建器
│   ├── DataSourceBuilder.java            # 构建器接口
│   ├── DataSourceBuilderFactory.java     # 构建器工厂
│   ├── DruidDataSourceBuilder.java       # Druid实现
│   └── HikariDataSourceBuilder.java      # HikariCP实现
├── config/
│   └── DataSourceConfig.java             # 数据源配置POJO
├── crypto/
│   └── AESEncryptUtil.java               # AES加解密工具
├── exception/
│   └── DataSourceException.java          # 统一异常
├── executor/                         # SQL执行器
│   ├── DataSourceExecutor.java           # 执行器接口
│   ├── DataSourceExecutorFactory.java    # 执行器工厂
│   ├── AbstractJdbcExecutor.java         # JDBC通用实现
│   ├── MySqlExecutor.java
│   ├── OracleExecutor.java
│   ├── PostgresExecutor.java
│   └── SqlServerExecutor.java
├── manager/                          # 核心管理
│   ├── DataSourceManager.java            # 数据源管理器(车管所)
│   └── DataSourceEntry.java              # 数据源条目(车+司机+订单)
├── metadata/                         # 元数据提取
│   ├── MetaDataExtractor.java           # 提取器接口
│   ├── MetaDataExtractorFactory.java    # 提取器工厂
│   ├── AbstractMetaDataExtractor.java   # 抽象基类(模板方法)
│   ├── MySqlMetaDataExtractor.java       # MySQL(information_schema)
│   ├── OracleMetaDataExtractor.java      # Oracle(ALL_TABLES)
│   ├── PostgresMetaDataExtractor.java    # PostgreSQL(pg_catalog)
│   ├── SqlServerMetaDataExtractor.java   # SQL Server(sys.objects)
│   ├── ColumnMeta.java                   # 列元信息
│   ├── TableMeta.java                    # 表元信息
│   ├── IndexMeta.java                    # 索引元信息
│   ├── PrimaryKeyMeta.java               # 主键元信息
│   └── DatabaseMetadata.java             # 整库元信息
├── persist/                          # 元数据持久化
│   ├── domain/
│   │   ├── SysTableMeta.java             # 表元信息DO
│   │   └── SysColumnMeta.java            # 列元信息DO
│   ├── mapper/
│   │   ├── SysTableMetaMapper.java
│   │   └── SysColumnMetaMapper.java
│   ├── service/
│   │   ├── IMetaSyncService.java
│   │   └── impl/MetaSyncServiceImpl.java # 增量同步核心
│   └── controller/
│       └── SysTableMetaController.java   # 元数据CRUD + 同步API
├── model/                            # BI数据建模(基于元数据)
│   ├── domain/ ...
│   ├── mapper/ ...
│   ├── service/ ...
│   └── controller/ ...
└── report/                           # BI数据报表
    ├── domain/ ...
    ├── service/ ...
    └── controller/ ...

七、踩坑记录

7.1 MySQL UNSIGNED INT JDBC读取异常

MySQL UNSIGNED INT 最大值为 4294967295,超出 Java Integer.MAX_VALUE 范围。使用 rs.getInt() 读取直接抛 NumberOutOfRange 异常。

修复:必须改用 rs.getLong() 读取,对应字段类型调整为 Long

7.2 MySQL元数据column_size溢出

DatabaseMetaData.getColumns() 获取的 LONGTEXT/LONGBLOBcolumn_size=4294967295,超出 INT 最大值。建表时 column_sizedecimal_digits 必须用 BIGINT 而非 INT

7.3 TINYINT返回类型注意

TINYINT字段若实体类用String类型映射,序列化返回前端为字符串 "0"/"1",前端状态判断必须用 === '0' 不能用 === 0,否则恒为false导致状态显示异常。

7.4 批量提取避免N+1连接问题

最初实现每张表的列信息单独开一个Connection,50张表就开50次连接。改为 extractColumnsForTables() 复用同一Connection循环处理,显著提升同步速度。

7.5 SQL Server类型名构建

SQL Server的 sys.columns 返回的 max_length 对 nvarchar 类型是字节数,需要除以2才是字符数。max_length=-1 表示 MAX 类型。需要 buildSqlServerTypeName() 方法做特殊处理。

八、总结

这个模块的核心设计思路:

  1. 策略模式 + 工厂模式:连接池(Hikari/Druid)、执行器(MySQL/Oracle/PG/SQLServer)、元数据提取器各走各的工厂,扩展新数据库只需加一个实现类
  2. 模板方法模式AbstractJdbcExecutorAbstractMetaDataExtractor 封装通用逻辑,子类覆盖差异部分
  3. 三路归并增量同步:新增INSERT、已有只更新提取器字段、删除标记停用,不丢失用户手动修改
  4. Java类型映射:从数据库原生类型自动推导Java类型,是BI建模维度/度量自动判断的基础
  5. 零框架依赖:core层不依赖Spring,纯JDBC实现,方便移植和测试

模块已经在线上稳定运行,支撑了后续BI数据建模和报表功能的元数据基础。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐