表设计器
表设计器
业务用途
表设计器是低代码引擎的起点。运营人员通过可视化界面定义业务表的结构(表名、表注释、字段名、字段类型、是否必填、字典类型等),平台自动完成三件事:
- 生成 DDL:根据字段配置拼装
CREATE TABLE语句,自动附加id、tenant_id、create_time、update_time、create_by、update_by、deleted等系统字段。 - 执行建表:通过
JdbcTemplate.execute(sql)在数据库中创建物理表。 - 存储元数据:将表结构信息写入
lc_table_meta(表元数据)和lc_column_meta(字段元数据),供后续表单设计、列表设计、动态 CRUD 使用。
修改表结构时,表设计器会做 diff 对比:新字段执行 ALTER TABLE ADD COLUMN,类型/注释变化执行 ALTER TABLE MODIFY COLUMN,删除字段执行 ALTER TABLE DROP COLUMN。
涉及文件
| 层 | 文件路径 | 说明 |
|---|---|---|
| Controller | backend/src/main/java/com/lowcode/controller/DatabaseController.java | /api/database 端点,表管理 + 数据 CRUD + 开放接口配置 |
| Service | backend/src/main/java/com/lowcode/service/SchemaService.java | 核心引擎:DDL 生成、建表、改表、动态 CRUD |
| Entity | backend/src/main/java/com/lowcode/entity/LcTableMeta.java | 表元数据实体(@TableName("lc_table_meta")) |
| Entity | backend/src/main/java/com/lowcode/entity/LcColumnMeta.java | 字段元数据实体(@TableName("lc_column_meta")) |
| Mapper | backend/src/main/java/com/lowcode/mapper/LcTableMetaMapper.java | MyBatis-Plus Mapper |
| Mapper | backend/src/main/java/com/lowcode/mapper/LcColumnMetaMapper.java | MyBatis-Plus Mapper |
| 前端 | frontend/src/views/database/TableDesigner.vue | 表设计器主界面 |
| 前端API | frontend/src/api/tableDesigner.ts | 前端请求封装 |
| SQL | backend/src/main/resources/schema-mysql.sql | 元数据表 DDL(第 236-289 行) |
数据库表
lc_table_meta(数据表元数据表)
CREATE TABLE lc_table_meta (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
tenant_id BIGINT DEFAULT 1 COMMENT '租户ID',
table_name VARCHAR(100) NOT NULL COMMENT '表名',
table_comment VARCHAR(255) COMMENT '表注释',
table_type VARCHAR(50) DEFAULT 'business' COMMENT '表类型(business:业务表 system:系统表)',
class_name VARCHAR(100) COMMENT '实体类名',
module_name VARCHAR(50) COMMENT '模块名',
business_name VARCHAR(50) COMMENT '业务名',
function_author VARCHAR(50) COMMENT '作者',
status CHAR(1) DEFAULT '1' COMMENT '状态(0:未生成 1:已生成)',
remark VARCHAR(500) COMMENT '备注',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
create_by VARCHAR(50) COMMENT '创建人',
update_by VARCHAR(50) COMMENT '更新人',
deleted TINYINT DEFAULT 0 COMMENT '删除标记',
UNIQUE KEY uk_table_name (tenant_id, table_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据表元数据表';lc_column_meta(字段元数据表)
CREATE TABLE lc_column_meta (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
tenant_id BIGINT DEFAULT 1 COMMENT '租户ID',
table_id BIGINT NOT NULL COMMENT '表ID',
column_name VARCHAR(100) NOT NULL COMMENT '字段名',
column_comment VARCHAR(255) COMMENT '字段注释',
column_type VARCHAR(50) NOT NULL COMMENT '字段类型',
java_type VARCHAR(50) NOT NULL COMMENT 'Java类型',
java_field VARCHAR(100) NOT NULL COMMENT 'Java字段名',
is_pk CHAR(1) DEFAULT 'N' COMMENT '是否主键(Y:是 N:否)',
is_increment CHAR(1) DEFAULT 'N' COMMENT '是否自增(Y:是 N:否)',
is_required CHAR(1) DEFAULT 'N' COMMENT '是否必填(Y:是 N:否)',
is_insert CHAR(1) DEFAULT 'Y' COMMENT '是否插入字段(Y:是 N:否)',
is_edit CHAR(1) DEFAULT 'Y' COMMENT '是否编辑字段(Y:是 N:否)',
is_list CHAR(1) DEFAULT 'Y' COMMENT '是否列表字段(Y:是 N:否)',
is_query CHAR(1) DEFAULT 'N' COMMENT '是否查询字段(Y:是 N:否)',
query_type VARCHAR(50) DEFAULT 'EQ' COMMENT '查询方式(EQ/LIKE/GT/LT/GTE/LTE)',
html_type VARCHAR(50) DEFAULT 'input' COMMENT 'HTML类型(input/textarea/select/radio/checkbox/datetime/file)',
dict_type VARCHAR(100) COMMENT '字典类型',
dict_sql VARCHAR(500) COMMENT '字典SQL',
options_api VARCHAR(500) COMMENT '远端选项接口地址',
option_value VARCHAR(64) COMMENT '远端选项value字段名(默认id)',
option_label VARCHAR(64) COMMENT '远端选项label字段名',
display_prop VARCHAR(64) COMMENT '表格展示替代字段名',
sort_order INT DEFAULT 0 COMMENT '排序',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT DEFAULT 0 COMMENT '删除标记',
INDEX idx_table_id (table_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字段元数据表';LcColumnMeta 关键字段说明
columnName:数据库物理列名(蛇形,如order_amount)javaField:Java 字段名(驼峰,如orderAmount),运行时数据交互用驼峰columnType:MySQL 类型字符串,如VARCHAR(255)、BIGINT、DECIMAL(10,2)htmlType:前端渲染控件类型,决定表单设计器中该字段默认渲染成什么组件queryType:列表搜索时的匹配方式dictType/dictSql/optionsApi:三种字段下拉选项来源,优先级为 dictType > dictSql > optionsApi
后端实现
端点列表
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/database/table/list | 获取所有表列表 |
| GET | /api/database/table/list/{appId} | 获取应用下的表列表 |
| GET | /api/database/table/{id} | 获取表详情(含字段列表) |
| GET | /api/database/table/columns/{tableId} | 获取表字段列表 |
| POST | /api/database/table/generateSql | 生成建表 SQL(预览,不执行) |
| POST | /api/database/table/create | 执行建表(DDL + 元数据) |
| PUT | /api/database/table/update | 更新表结构(ALTER diff) |
| DELETE | /api/database/table/{id} | 删除表(DROP + 元数据清理) |
1. 生成建表 SQL -- generateCreateTableSql()
此方法拼装 CREATE TABLE 语句,自动附加系统字段:
public String generateCreateTableSql(LcTableMeta tableMeta) {
StringBuilder sql = new StringBuilder();
sql.append("CREATE TABLE `").append(tableMeta.getTableName()).append("` (\n");
// 自动添加主键
sql.append(" `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',\n");
// 自动添加租户字段
sql.append(" `tenant_id` BIGINT DEFAULT 1 COMMENT '租户ID',\n");
// 系统保留字段,需要过滤(不会重复添加)
java.util.Set<String> systemFields = java.util.Set.of(
"id", "tenant_id", "create_time", "update_time", "create_by", "update_by", "deleted"
);
// 添加用户定义的字段
if (tableMeta.getColumns() != null) {
for (LcColumnMeta column : tableMeta.getColumns()) {
if (column.getColumnName() == null || column.getColumnName().isEmpty()) continue;
if (systemFields.contains(column.getColumnName().toLowerCase())) continue;
sql.append(" `").append(column.getColumnName()).append("` ")
.append(column.getColumnType());
if ("Y".equals(column.getIsRequired())) {
sql.append(" NOT NULL");
}
// 数值类型不加引号,其他类型加单引号
if (column.getDefaultValue() != null && !column.getDefaultValue().isEmpty()) {
sql.append(" DEFAULT ");
String colType = column.getColumnType() != null ? column.getColumnType().toUpperCase() : "";
if (colType.contains("INT") || colType.contains("DECIMAL") ||
colType.contains("FLOAT") || colType.contains("DOUBLE")) {
sql.append(column.getDefaultValue());
} else {
sql.append("'").append(column.getDefaultValue()).append("'");
}
}
if (column.getColumnComment() != null && !column.getColumnComment().isEmpty()) {
sql.append(" COMMENT '").append(column.getColumnComment()).append("'");
}
sql.append(",\n");
}
}
// 自动添加通用审计字段
sql.append(" `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n");
sql.append(" `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',\n");
sql.append(" `create_by` VARCHAR(50) COMMENT '创建人',\n");
sql.append(" `update_by` VARCHAR(50) COMMENT '更新人',\n");
sql.append(" `deleted` TINYINT DEFAULT 0 COMMENT '删除标记',\n");
sql.append(" INDEX idx_tenant_id (`tenant_id`)\n");
sql.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='")
.append(tableMeta.getTableComment()).append("';");
return sql.toString();
}生成的 DDL 示例:
CREATE TABLE `erp_supplier` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',
`tenant_id` BIGINT DEFAULT 1 COMMENT '租户ID',
`supplier_name` VARCHAR(255) NOT NULL COMMENT '供应商名称',
`contact_phone` VARCHAR(50) COMMENT '联系电话',
`status` VARCHAR(20) DEFAULT 'active' COMMENT '状态',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_by` VARCHAR(50) COMMENT '创建人',
`update_by` VARCHAR(50) COMMENT '更新人',
`deleted` TINYINT DEFAULT 0 COMMENT '删除标记',
INDEX idx_tenant_id (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商表';2. 执行建表 -- createTable()
@Transactional(rollbackFor = Exception.class)
public boolean createTable(LcTableMeta tableMeta) {
// 检查表是否已存在
String checkSql = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?";
Integer tableCount = jdbcTemplate.queryForObject(checkSql, Integer.class, tableMeta.getTableName());
if (tableCount != null && tableCount > 0) {
throw new RuntimeException("表 [" + tableMeta.getTableName() + "] 已存在,请勿重复创建");
}
// 生成并执行 DDL
String sql = generateCreateTableSql(tableMeta);
jdbcTemplate.execute(sql);
// 保存表元数据
tableMeta.setStatus("1");
tableMetaMapper.insert(tableMeta);
// 保存字段元数据(自动推导 Java 类型 + 驼峰字段名)
for (LcColumnMeta column : tableMeta.getColumns()) {
column.setId(null);
column.setTableId(tableMeta.getId());
if (column.getJavaType() == null || column.getJavaType().isEmpty()) {
column.setJavaType(deriveJavaType(column.getColumnType()));
}
if (column.getJavaField() == null || column.getJavaField().isEmpty()) {
column.setJavaField(toCamelCase(column.getColumnName()));
}
// 设置默认值
if (column.getIsPk() == null) column.setIsPk("N");
if (column.getQueryType() == null) column.setQueryType("EQ");
if (column.getHtmlType() == null) column.setHtmlType("input");
columnMetaMapper.insert(column);
}
return true;
}类型自动推导
deriveJavaType() 方法根据 MySQL 列类型推导 Java 类型:VARCHAR/TEXT/CHAR -> String,INT -> Integer,BIGINT -> Long,DECIMAL/DOUBLE/FLOAT -> BigDecimal,DATETIME/TIMESTAMP/DATE -> LocalDateTime。toCamelCase() 将蛇形命名转为驼峰,如 order_amount -> orderAmount。
3. 更新表结构 -- updateTable()(ALTER diff)
@Transactional(rollbackFor = Exception.class)
public boolean updateTable(LcTableMeta tableMeta) {
LcTableMeta existingTable = tableMetaMapper.selectById(tableMeta.getId());
// 获取现有字段,构建 Map<columnName, LcColumnMeta>
Map<String, LcColumnMeta> existingColumnMap = ...;
for (LcColumnMeta column : tableMeta.getColumns()) {
String colName = column.getColumnName();
LcColumnMeta existing = existingColumnMap.get(colName);
if (existing == null) {
// 新增字段:ALTER TABLE ADD COLUMN
String alterSql = "ALTER TABLE `" + existingTable.getTableName()
+ "` ADD COLUMN `" + colName + "` " + column.getColumnType();
if ("Y".equals(column.getIsRequired())) alterSql += " NOT NULL";
jdbcTemplate.execute(alterSql);
columnMetaMapper.insert(column);
} else {
// 修改字段:比较类型/必填/默认值/注释
boolean needsModify = false;
if (!existing.getColumnType().equals(column.getColumnType())) needsModify = true;
if (!Objects.equals(existing.getIsRequired(), column.getIsRequired())) needsModify = true;
// ... 其他比较
if (needsModify) {
String modifySql = "ALTER TABLE `" + existingTable.getTableName()
+ "` MODIFY COLUMN `" + colName + "` " + column.getColumnType();
jdbcTemplate.execute(modifySql);
}
columnMetaMapper.updateById(column);
existingColumnMap.remove(colName); // 从待删除列表中移除
}
}
// 剩下的 existingColumnMap 中的字段 = 被删除的字段
for (String colName : existingColumnMap.keySet()) {
String alterSql = "ALTER TABLE `" + existingTable.getTableName()
+ "` DROP COLUMN `" + colName + "`";
jdbcTemplate.execute(alterSql);
columnMetaMapper.deleteById(col.getId());
}
return true;
}前端实现
前端组件
表设计器主界面为 frontend/src/views/database/TableDesigner.vue,它提供:
- 表名、表注释输入
- 字段列表表格(可增删行、拖拽排序)
- 每个字段可配置:字段名、注释、类型、是否必填、HTML 类型、字典类型、查询方式等
- 「生成 SQL 预览」按钮(调用
/database/table/generateSql) - 「保存建表」按钮(调用
/database/table/create) - 「保存修改」按钮(调用
/database/table/update)

前端 API 封装
// frontend/src/api/tableDesigner.ts
export function getTableList(appId?: number | null) {
const url = appId ? `/database/table/list/${appId}` : '/database/table/list'
return request.get(url).then((res: any) => res.data)
}
export function getTableDetail(id: number) {
return request.get(`/database/table/${id}`).then((res: any) => res.data)
}
export function createTable(data: any) {
return request.post('/database/table/create', data).then((res: any) => res.data)
}
export function updateTable(data: any) {
return request.put('/database/table/update', data).then((res: any) => res.data)
}
export function generateCreateTableSql(data: any) {
return request.post('/database/table/generateSql', data).then((res: any) => res.data)
}三处白名单同步
根据项目实践记录,TableDesigner.vue 中有三处字段属性白名单(字段配置面板、表格列定义、保存时的字段提取),新增字段属性时必须同步修改三处,否则该属性不会被保存或显示。详见项目记忆文件 lowcode-table-designer-field-whitelist。

操作步骤
- 进入「数据库管理 -> 表设计器」页面
- 输入表名(蛇形,如
erp_supplier)和表注释(如「供应商表」) - 添加字段:填写字段名、注释、类型(如
VARCHAR(255))、是否必填、HTML 类型等 - 点击「生成 SQL」预览 DDL,确认无误
- 选择所属应用(appId),点击「保存建表」
- 建表成功后,表元数据写入
lc_table_meta,字段元数据写入lc_column_meta - 后续修改字段时,进入表详情页编辑,保存时自动 ALTER diff

常见问题
建表时报「表已存在」
createTable() 会先查 information_schema.tables 检查表名是否已存在。如果之前建表失败但物理表已创建,需手动 DROP 后重试。
修改字段类型后数据丢失
ALTER TABLE MODIFY COLUMN 在缩小类型精度时(如 VARCHAR(255) 改 VARCHAR(50))可能截断数据。修改前请确认数据兼容性。
删除字段后元数据未清理
updateTable() 会在 diff 后删除 existingColumnMap 中剩余的字段。如果元数据和物理表不一致,可能是之前操作中断导致,需手动检查 lc_column_meta 表。
