06 | 把 meta_config 同步进 MySQL(生成阶段)

06 | 把 meta_config 同步进 MySQL(生成阶段)

项目地址:github.com/frontzhm/n2...

每一步对应的完整代码都在仓库里,跟着文档卡住了就去翻源码。

这是一篇系列文,请按顺序阅读。

本文目标

NL2SQL 召回前,要先把业务元数据灌进各个存储:

  1. MySQL meta 库:结构化元数据(表 / 字段 / 指标 / 关联)
  2. Qdrant:字段语义向量(后续)
  3. Elasticsearch:维度值全文检索(后续)

本文完成第 1 步:把 conf/meta_config.yaml 同步到 MySQL meta 库的四张表。

数据源头是 YAML,不是直接扫业务库;业务库(dw)只用来补字段类型和样例值。


1. 整体流程

text 复制代码
meta_config.yaml
       │
       ▼
  conf/sync_db.py
       │
       ├─ sync_table_info()
       ├─ sync_column_info()   ← 会读 dw 库补 type / examples
       ├─ sync_metric_info()
       └─ sync_column_metric()
              │
              ▼
     Service → Repository → Mapper → Model
              │
              ▼
         MySQL meta 库

调用链一句话:

脚本解析 YAML → 组装 Entity → Service 调 Repository → Mapper 转 Model → SQLAlchemy 写入 MySQL。


2. meta 库要存什么

表名 作用 关键字段 状态
table_info 业务表信息 id / name / role / description
column_info 业务字段信息 id / name / type / role / description / alias / examples / table_id
metric_info 指标定义 id / name / description / alias / relevant_columns
column_metric 字段 ↔ 指标关联 column_id / metric_id

关系:

text 复制代码
table_info 1 ── N column_info
column_info N ── N metric_info   (通过 column_metric)

配置上做了拆分:

  • tables:只放业务表(dim / fact)及其 columns
  • metric_info:单独放在顶级 key,不再塞进 tables

这样同步逻辑更清晰,也避免把指标误写进 table_info


3. 为什么要分层

YAML 格式和数据库表结构不一样,不能「读完 YAML 直接 INSERT」。分层是为了把职责拆开:

层级 路径 职责 不关心什么
Entity app/entities/ 纯业务对象(dataclass) 数据库类型、SQLAlchemy
Model app/models/ ORM 映射,对应物理表 YAML 怎么解析
Mapper app/mappers/ Entity ↔ Model 转换 业务流程
Repository app/repositories/ CRUD / 事务 业务规则
Service app/services/ 编排业务 SQL 细节
Script conf/sync_db.py 读配置、装配依赖、执行同步 领域细节

相关文件:

text 复制代码
app/
├── entities/          # table_info / column_info / metric_info / column_metric
├── models/
├── mappers/
├── repositories/      # 含 dw_db_repository(读业务库)
├── services/
└── dbs/               # meta_db.py / dw_db.py
conf/
├── meta_config.yaml
├── app_config.yaml
├── get_config.py
├── sync_db.py
└── ../.env

4. 配置:密码放 .env,YAML 只放占位符

4.1 .env

env 复制代码
MYSQL_USER=yan
MYSQL_PASSWORD=你的密码
LLM_API_KEY=...
LLM_BASE_URL=...

4.2 app_config.yaml(片段)

yaml 复制代码
meta_db:
  host: localhost
  port: 3306
  user: "${MYSQL_USER}"
  password: "${MYSQL_PASSWORD}"
  database: meta

dw_db:
  host: localhost
  port: 3306
  user: "${MYSQL_USER}"
  password: "${MYSQL_PASSWORD}"
  database: dw

4.3 get_config.py 做什么

  1. load_dotenv(.env) 把环境变量读进进程
  2. 读 YAML 时,把 ${MYSQL_PASSWORD} 替换成真实值
  3. 缺少环境变量时直接报错,避免连库时才发现密码为空

依赖:

bash 复制代码
uv add pyyaml python-dotenv sqlalchemy asyncmy

5. 先实现 table_info:按层搭起来

后面三张表的 Entity / Model / Mapper / Repository / Service 都照这个套路搭,本文只详细展开 table_info

5.1 Entity

python 复制代码
# app/entities/table_info.py
from dataclasses import dataclass

@dataclass
class TableInfo:
    id: str
    name: str
    role: str
    description: str

要点:

  • idstr,对应数据库 varchar(64) 主键
  • YAML 没有单独 id,同步时用物理表名(如 dim_region)当稳定主键

5.2 Model

python 复制代码
# app/models/base.py
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass
python 复制代码
# app/models/table_info_model.py
class TableInfoModel(Base):
    __tablename__ = "table_info"

    id: Mapped[str] = mapped_column(String(64), primary_key=True)
    name: Mapped[str | None] = mapped_column(String(128))
    role: Mapped[str | None] = mapped_column(String(32))
    description: Mapped[str | None] = mapped_column(Text)

Base 统一持有 metadata,后面的 create_all / drop_all 都靠它。

5.3 Mapper

python 复制代码
class TableInfoMapper:
    def to_entity(self, model: TableInfoModel) -> TableInfo:
        # ORM → Entity:字段铺开写(ORM.__dict__ 含内部状态)
        return TableInfo(
            id=model.id,
            name=model.name,
            role=model.role,
            description=model.description,
        )

    def to_model(self, entity: TableInfo) -> TableInfoModel:
        # Entity → ORM:dataclass.__dict__ 干净,可解包
        return TableInfoModel(**entity.__dict__)

5.4 数据库连接

app/dbs/meta_db.py 负责 meta 库;app/dbs/dw_db.py 同理,只是读 app_config["dw_db"]

要点:

  • 引擎模块级创建一次
  • 驱动用 mysql+asyncmy
  • 密码含特殊字符时要 quote_plus

5.5 Repository / Service

Repository 只做持久化;Service 目前是薄封装,YAML → Entity 的解析先放在 sync_db.py

add_all 是真正的批量:一次 add_all + 一次 commit。不要在 Service 里循环调单条 add


6. 同步脚本骨架

conf/sync_db.py 的职责:

  1. 开发阶段:drop_all + create_all,方便反复测试
  2. 依次同步四张表
  3. 打开 Session,装配 Repository → Service,批量写入
python 复制代码
async def reset_meta_tables() -> None:
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
        await conn.run_sync(Base.metadata.create_all)

async def sync_table_info(config: dict) -> list[TableInfo]:
    table_infos = []
    for table in config.get("tables", []):
        name = table["name"]
        table_infos.append(
            TableInfo(
                id=name,
                name=name,
                role=table["role"],
                description=table["description"],
            )
        )

    async with MetaAsyncSessionLocal() as session:
        service = TableInfoService(TableInfoRepository(session))
        return await service.add_all(table_infos)

开发期用 drop_all,是为了避免重复执行时报主键冲突。代价是每次都会清空已有元数据;上线后应改成 upsert。

指标配置单独放在顶级:

yaml 复制代码
metric_info:
  - name: GMV
    description: 全称 Gross Merchandise Value,表示所有订单的成交金额总和。
    alias: [成交总额, 订单总额]
    relevant_columns:
      - fact_order.order_amount

7. 执行与验证

7.1 确认 MySQL 已启动

bash 复制代码
docker ps --filter name=mysql
# 期望看到 Up ... (healthy) 且 0.0.0.0:3306->3306/tcp

7.2 执行同步

bash 复制代码
uv run python conf/sync_db.py

四张表都写完后,输出大致如下:

text 复制代码
已写入 table_info 5 条
已写入 column_info 24 条
已写入 metric_info 2 条
已写入 column_metric 2 条

7.3 查库确认

bash 复制代码
set -a && source .env && set +a
docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" mysql \
  mysql --default-character-set=utf8mb4 -u"$MYSQL_USER" -D meta \
  -e "SELECT * FROM table_info;"

--default-character-set=utf8mb4 要加,否则中文会变成 ?????


8. 常见问题

现象 原因 处理
No module named 'app' 工作目录 / sys.path 不对 在项目根执行;脚本已自动把根目录加入 path
No module named 'asyncmy' 缺异步驱动 uv add asyncmy
Access denied ... using password: NO 密码没从 .env 展开 检查 .env${MYSQL_PASSWORD}get_config.py
Duplicate entry '...PRIMARY' 重复插入相同主键 开发期用 drop_all;或改 upsert
中文显示 ????? 客户端字符集不对 --default-character-set=utf8mb4

9. 同步 column_info

YAML 里字段长这样:

yaml 复制代码
columns:
  - name: province
    role: dimension
    description: 订单所属的省份名称。
    alias: [省份, 省, 所在省份]
    sync: true

落库字段:

text 复制代码
id / name / type / role / description / alias / examples / table_id

映射规则:

  • id{table_name}.{column_name},例如 dim_region.province
  • name / role / description / alias:直接来自 YAML
  • table_id:所属业务表名
  • type:从 dw 库的 information_schema 读取
  • examples:当 sync: true 时,从 dw 表 SELECT DISTINCT 取值;否则为空列表

所以要先补业务库只读能力:

python 复制代码
# app/repositories/dw_db_repository.py
class DwDBRepository:
    async def get_column_type(self, table_name: str, column_name: str) -> str | None:
        # SELECT COLUMN_TYPE FROM information_schema.COLUMNS ...

    async def get_column_all_values(self, table_name: str, column_name: str) -> list[str]:
        # SELECT DISTINCT `column` FROM `table` WHERE `column` IS NOT NULL

Service 只是薄封装。同步核心逻辑:

python 复制代码
async def sync_column_info(config: dict) -> list[ColumnInfo]:
    column_infos: list[ColumnInfo] = []

    async with DwAsyncSessionLocal() as dw_session:
        dw_service = DwDBService(DwDBRepository(dw_session))
        for table in config.get("tables", []):
            table_name = table["name"]
            for column in table.get("columns", []):
                column_name = column["name"]
                column_type = await dw_service.get_column_type(table_name, column_name)
                examples = (
                    await dw_service.get_column_all_values(table_name, column_name)
                    if column.get("sync", False)
                    else []
                )
                column_infos.append(
                    ColumnInfo(
                        id=f"{table_name}.{column_name}",
                        name=column_name,
                        type=column_type,
                        role=column["role"],
                        examples=examples,
                        description=column["description"],
                        alias=column.get("alias", []),
                        table_id=table_name,
                    )
                )

    async with MetaAsyncSessionLocal() as meta_session:
        service = ColumnInfoService(ColumnInfoRepository(meta_session))
        return await service.add_all(column_infos)

Entity / Model / Mapper / Repository / Service 的搭法与 table_info 相同。


10. 同步 metric_info

YAML:

yaml 复制代码
metric_info:
  - name: GMV
    description: 全称 Gross Merchandise Value,表示所有订单的成交金额总和。
    alias: [成交总额, 订单总额]
    relevant_columns:
      - fact_order.order_amount

落库字段:

text 复制代码
id / name / description / alias / relevant_columns

其中 id 直接用 name,其余字段原样写入。

python 复制代码
async def sync_metric_info(config: dict) -> list[MetricInfo]:
    metric_infos: list[MetricInfo] = []
    for record in config.get("metric_info", []):
        metric_infos.append(
            MetricInfo(
                id=record["name"],
                name=record["name"],
                description=record["description"],
                alias=record.get("alias", []),
                relevant_columns=record.get("relevant_columns", []),
            )
        )

    async with MetaAsyncSessionLocal() as session:
        service = MetricInfoService(MetricInfoRepository(session))
        return await service.add_all(metric_infos)

执行后应看到:

text 复制代码
已写入 metric_info 2 条:
  - GMV
  - AOV

11. 同步 column_metric

column_metric 是字段与指标的多对多关联表,方便后续按字段反查可参与的指标。

它不单独出现在 YAML 里,而是从 metric_info[].relevant_columns 展开:

text 复制代码
column_id = relevant_column   # 如 fact_order.order_amount
metric_id = record["name"]    # 如 GMV

因为 column_info.id 也用「表名.字段名」,所以这里可以直接对齐。

python 复制代码
async def sync_column_metric(config: dict) -> list[ColumnMetric]:
    column_metrics: list[ColumnMetric] = []
    for record in config.get("metric_info", []):
        for relevant_column in record.get("relevant_columns", []):
            column_metrics.append(
                ColumnMetric(
                    column_id=relevant_column,
                    metric_id=record["name"],
                )
            )

    async with MetaAsyncSessionLocal() as session:
        service = ColumnMetricService(ColumnMetricRepository(session))
        return await service.add_all(column_metrics)

执行后应看到:

text 复制代码
已写入 column_metric 2 条:
  - fact_order.order_amount -> GMV
  - fact_order.order_quantity -> AOV

联表验证:

bash 复制代码
docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" mysql \
  mysql --default-character-set=utf8mb4 -u"$MYSQL_USER" -D meta \
  -e "
  SELECT cm.column_id, cm.metric_id
  FROM column_metric cm;
  "

12. 下一步

MySQL meta 四张表已经齐了。后续继续在 sync_db.py 里补:

  1. Qdrant:把字段 / 指标的描述与别名向量化,供语义召回
  2. Elasticsearch :把 sync: true 的维度值写入全文索引,供值匹配

入口里已经预留:

python 复制代码
sync_to_qdrant(meta_config)          # TODO
sync_to_elasticsearch(meta_config)   # TODO
相关推荐
stereohomology1 小时前
实战微调排版细节:豆包Seed 2.1 Turbo 不弱于GLM5.2?
人工智能·大模型·对比·why不coding
renhongxia12 小时前
世界模型的四个商业场景:自动驾驶、机器人、内容产业与工业仿真
人工智能·机器人·自动驾驶
OpenTiny社区2 小时前
Loop Engineering:让 AI Agent 自己跑起来的工程方法
前端·github
stormzhangV2 小时前
AI 的玩法,该做减法了
人工智能·ai编程·claude
IT_陈寒2 小时前
SpringBoot自动配置坑了我一周,原来问题这么蠢!
前端·人工智能·后端
糯米导航2 小时前
实践教程|搭建电商 AI 无限画布,实现百款商品主图自动化批量生成
运维·人工智能·自动化
kyriewen2 小时前
我review了一份Vibe Coding写的前端代码——能跑,但5个地方迟早要命
前端·javascript·ai编程
朴马丁3 小时前
从制造到智造:PLM如何赋能企业研发创新
大数据·运维·人工智能·食品行业·流程行业plm·化工新材料行业·新能源材料行业
武子康3 小时前
GAIA、Cosmos、Genie 等视频模型纷纷自称"世界模型",为什么说视觉逼真度不构成决策证据?
人工智能·llm·agent