若依框架集成动态数据源与元数据管理模块(全栈实战)
基于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类型名(如 String、Long、BigDecimal、LocalDateTime),后续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/LONGBLOB 列 column_size=4294967295,超出 INT 最大值。建表时 column_size 和 decimal_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() 方法做特殊处理。
八、总结
这个模块的核心设计思路:
- 策略模式 + 工厂模式:连接池(Hikari/Druid)、执行器(MySQL/Oracle/PG/SQLServer)、元数据提取器各走各的工厂,扩展新数据库只需加一个实现类
- 模板方法模式:
AbstractJdbcExecutor和AbstractMetaDataExtractor封装通用逻辑,子类覆盖差异部分 - 三路归并增量同步:新增INSERT、已有只更新提取器字段、删除标记停用,不丢失用户手动修改
- Java类型映射:从数据库原生类型自动推导Java类型,是BI建模维度/度量自动判断的基础
- 零框架依赖:core层不依赖Spring,纯JDBC实现,方便移植和测试
模块已经在线上稳定运行,支撑了后续BI数据建模和报表功能的元数据基础。
更多推荐




所有评论(0)