第 8 节:集成 CubeJS 数据模型

第 8 节:集成 CubeJS 数据模型

阅读时间 :约 7 分钟
难度级别 :实战
前置知识:FastAPI 基础、Docker 基础、SQL 基础

本节概要

通过本节学习,你将掌握:

  • CubeJS 的核心概念和工作原理
  • 使用 Docker 快速部署 CubeJS 服务
  • 创建和配置 CubeJS 数据模型(Cube)
  • 定义度量(Measures)和维度(Dimensions)
  • 在 Python 中集成 CubeJS 服务
  • 测试和验证 CubeJS 查询功能

引言

CubeJS 是一个强大的数据建模和查询引擎,它能够将复杂的 SQL 查询抽象为简单的 REST API。本节我们将学习如何集成 CubeJS,为 Text-to-BI 系统提供统一的数据访问层。

CubeJS 是一个强大的数据建模和查询引擎。本文将介绍如何集成 CubeJS,定义数据模型,并创建服务层来调用 CubeJS API。

🎯 本章目标

完成后,你将拥有:

  • ✅ 运行中的 CubeJS 服务
  • ✅ 完整的数据模型定义
  • ✅ CubeJS Service 封装
  • ✅ 可以查询的示例数据

🐳 启动 CubeJS 服务

Step 1: 创建 Docker Compose 配置

与 AI 对话:

diff 复制代码
创建 CubeJS 的 Docker Compose 配置:
- 使用 CubeJS 官方镜像
- 配置 SQLite 数据库
- 映射模型目录
- 暴露 4000 端口

backend/cubejs/docker-compose.yml:

yaml 复制代码
version: '3.8'

services:
  cubejs:
    image: cubejs/cube:latest
    ports:
      - "4000:4000"
    environment:
      - CUBEJS_DEV_MODE=true
      - CUBEJS_DB_TYPE=sqlite
      - CUBEJS_DB_NAME=employees.db
      - CUBEJS_API_SECRET=secret
    volumes:
      - ./model:/cube/conf/schema
      - ./data:/cube/conf/data
    restart: unless-stopped

Step 2: 启动服务

bash 复制代码
cd backend/cubejs
docker-compose up -d

# 查看日志
docker-compose logs -f

# 检查服务状态
curl http://localhost:4000/cubejs-api/v1/meta

📊 定义数据模型

CubeJS 数据模型概念

核心概念:

  1. Cubes(数据立方体):数据的逻辑表示
  2. Measures(度量):可聚合的数值指标
  3. Dimensions(维度):用于分组和过滤的字段
  4. Segments(段):预定义的过滤条件
  5. Joins(连接):表之间的关系

Step 1: 创建员工数据模型

与 AI 对话:

diff 复制代码
创建 employees.cube.yml 数据模型:

数据表:employees
字段:
- emp_no: 员工编号(主键)
- first_name, last_name: 姓名
- gender: 性别
- birth_date: 出生日期
- hire_date: 入职日期

需要的度量:
- total_employees: 员工总数
- male_employees: 男性员工数
- female_employees: 女性员工数

需要的维度:
- 所有字段作为维度
- 支持按部门分组(通过 join)

backend/cubejs/model/employees.cube.yml:

yaml 复制代码
cubes:
  - name: employees
    title: Employees
    sql_table: employees

    # ========== Measures(度量/指标) ==========
    measures:
      # 统计员工总数
      - name: total_employees
        type: count
        sql: emp_no  # 唯一主键

      # 按性别统计
      - name: male_employees
        type: count
        sql: emp_no
        filters:
          - sql: "{CUBE}.gender = 'M'"

      - name: female_employees
        type: count
        sql: emp_no
        filters:
          - sql: "{CUBE}.gender = 'F'"

      # 员工平均入职时间(以天数算)
      - name: avg_tenure_days
        type: avg
        sql: "DATEDIFF(CURDATE(), hire_date)"
        title: "Average Tenure (Days)"

    # ========== Dimensions(维度) ==========
    dimensions:
      - name: emp_no
        sql: emp_no
        type: number
        primary_key: true

      - name: first_name
        sql: first_name
        type: string

      - name: last_name
        sql: last_name
        type: string

      - name: gender
        sql: gender
        type: string

      - name: birth_date
        sql: birth_date
        type: time
        title: "Birth Date"

      - name: hire_date
        sql: hire_date
        type: time
        title: "Hire Date"

      # 通过 join 获取部门信息
      - name: dept_no
        sql: "{dept_emp.dept_no}"
        type: string
        title: "Department Number"

      - name: dept_name
        sql: "{dept_emp.departments.dept_name}"
        type: string
        title: "Department Name"

    # ========== Segments(可选) ==========
    segments:
      - name: active_employees
        sql: "{CUBE}.hire_date <= CURDATE()"

    # ========== Joins ==========
    joins:
      - name: dept_emp
        sql: "{CUBE}.emp_no = {dept_emp}.emp_no"
        relationship: one_to_many

Step 2: 理解数据模型

Measures(度量):

yaml 复制代码
# 简单计数
- name: total_employees
  type: count
  sql: emp_no

# 带过滤的计数
- name: male_employees
  type: count
  sql: emp_no
  filters:
    - sql: "{CUBE}.gender = 'M'"

# 平均值
- name: avg_tenure_days
  type: avg
  sql: "DATEDIFF(CURDATE(), hire_date)"

Dimensions(维度):

yaml 复制代码
# 字符串维度
- name: gender
  sql: gender
  type: string

# 时间维度
- name: hire_date
  sql: hire_date
  type: time

# 数值维度
- name: emp_no
  sql: emp_no
  type: number
  primary_key: true

Step 3: 测试数据模型

bash 复制代码
# 获取元数据
curl http://localhost:4000/cubejs-api/v1/meta

# 测试查询
curl -X POST http://localhost:4000/cubejs-api/v1/load \
  -H "Content-Type: application/json" \
  -d '{
    "query": {
      "measures": ["employees.total_employees"]
    }
  }'

🔌 创建 CubeJS Service

Step 1: 设计服务接口

与 AI 对话:

scss 复制代码
创建 CubeJS Service 类,封装 CubeJS REST API:

需要的方法:
1. load(query): 执行查询获取数据
2. sql(query): 获取生成的 SQL
3. get_meta(): 获取数据模型元信息

要求:
- 支持 GET 和 POST 请求
- 完整的错误处理
- 类型提示
- 详细的文档字符串

backend/services/cubejs_service.py:

python 复制代码
"""
Cube.js REST API Service
用于调用 Cube.js 的 REST API 接口
"""

import requests
from typing import Dict, Any, Optional, List, Union
from urllib.parse import urljoin
import json


class CubeJSService:
    """Cube.js REST API 服务类"""
    
    def __init__(
        self, 
        base_url: str, 
        api_token: Optional[str] = None, 
        base_path: str = "/cubejs-api"
    ):
        """
        初始化 Cube.js 服务
        
        Args:
            base_url: Cube.js 服务的基础 URL
            api_token: API 认证令牌(可选)
            base_path: API 基础路径
        """
        self.base_url = base_url.rstrip('/')
        self.api_token = api_token
        self.base_path = base_path.rstrip('/')
        self.headers = {"Content-Type": "application/json"}
        
        if api_token:
            self.headers["Authorization"] = api_token
    
    def _build_url(self, endpoint: str) -> str:
        """构建完整的 API URL"""
        return urljoin(self.base_url, f"{self.base_path}{endpoint}")
    
    def load(
        self, 
        query: Union[Dict[str, Any], List[Dict[str, Any]]],
        method: str = "POST"
    ) -> Dict[str, Any]:
        """
        执行查询并获取结果
        
        Args:
            query: 查询对象或查询数组
            method: HTTP 方法(GET 或 POST)
            
        Returns:
            包含查询结果的字典
            
        Example:
            >>> service = CubeJSService("http://localhost:4000")
            >>> result = service.load({
            ...     "measures": ["employees.total_employees"],
            ...     "dimensions": ["employees.gender"]
            ... })
        """
        url = self._build_url("/v1/load")
        
        if method.upper() == "GET":
            params = {"query": json.dumps(query)}
            response = requests.get(url, params=params, headers=self.headers)
        else:
            payload = {"query": query}
            response = requests.post(url, json=payload, headers=self.headers)
        
        response.raise_for_status()
        return response.json()
    
    def sql(
        self,
        query: Union[Dict[str, Any], str],
        format: str = "rest",
        method: str = "POST"
    ) -> Dict[str, Any]:
        """
        获取生成的 SQL 查询
        
        Args:
            query: 查询对象或 SQL 字符串
            format: 查询格式(rest 或 sql)
            method: HTTP 方法
            
        Returns:
            包含 SQL 信息的字典
            
        Example:
            >>> sql_result = service.sql({
            ...     "measures": ["employees.total_employees"]
            ... })
            >>> print(sql_result["sql"]["sql"][0])
        """
        url = self._build_url("/v1/sql")
        
        if method.upper() == "GET":
            query_value = json.dumps(query) if isinstance(query, dict) else query
            params = {"query": query_value, "format": format}
            response = requests.get(url, params=params, headers=self.headers)
        else:
            payload = {"query": query, "format": format}
            response = requests.post(url, json=payload, headers=self.headers)
        
        response.raise_for_status()
        return response.json()
    
    def get_meta(self) -> Dict[str, Any]:
        """
        获取数据模型的元信息
        
        Returns:
            包含所有 cubes 定义的字典
        """
        url = self._build_url("/v1/meta")
        response = requests.get(url, headers=self.headers)
        response.raise_for_status()
        return response.json()

Step 2: 测试服务

创建测试脚本:

python 复制代码
# backend/services/test_cubejs.py
from cubejs_service import CubeJSService

def test_cubejs_service():
    # 初始化服务
    service = CubeJSService(base_url="http://localhost:4000")
    
    # 测试 1: 获取元数据
    print("=== 测试元数据 ===")
    meta = service.get_meta()
    print(f"可用的 Cubes: {[cube['name'] for cube in meta['cubes']]}")
    
    # 测试 2: 简单查询
    print("\n=== 测试简单查询 ===")
    result = service.load({
        "measures": ["employees.total_employees"]
    })
    print(f"员工总数: {result['data'][0]['employees.total_employees']}")
    
    # 测试 3: 分组查询
    print("\n=== 测试分组查询 ===")
    result = service.load({
        "measures": ["employees.total_employees"],
        "dimensions": ["employees.gender"]
    })
    for row in result['data']:
        print(f"{row['employees.gender']}: {row['employees.total_employees']}")
    
    # 测试 4: 获取 SQL
    print("\n=== 测试 SQL 生成 ===")
    sql_result = service.sql({
        "measures": ["employees.total_employees"],
        "dimensions": ["employees.gender"]
    })
    print(f"生成的 SQL:\n{sql_result['sql']['sql'][0]}")

if __name__ == "__main__":
    test_cubejs_service()

运行测试:

bash 复制代码
cd backend/services
python test_cubejs.py

🎯 CubeJS 查询示例

基础查询

json 复制代码
{
  "measures": ["employees.total_employees"]
}

分组查询

json 复制代码
{
  "measures": ["employees.total_employees"],
  "dimensions": ["employees.gender"]
}

过滤查询

json 复制代码
{
  "measures": ["employees.total_employees"],
  "dimensions": ["employees.dept_name"],
  "filters": [{
    "member": "employees.gender",
    "operator": "equals",
    "values": ["F"]
  }]
}

排序查询

json 复制代码
{
  "measures": ["employees.total_employees"],
  "dimensions": ["employees.dept_name"],
  "order": {
    "employees.total_employees": "desc"
  }
}

限制结果

json 复制代码
{
  "measures": ["employees.total_employees"],
  "dimensions": ["employees.dept_name"],
  "limit": 10
}

🧪 Vibe Coding 要点

1. 迭代定义模型

复制代码
第1版:基础字段
第2版:添加度量
第3版:添加维度
第4版:添加 Joins
第5版:优化和测试

2. 测试驱动

每添加一个字段,立即测试:

bash 复制代码
# 测试新添加的度量
curl -X POST http://localhost:4000/cubejs-api/v1/load \
  -d '{"query": {"measures": ["employees.new_measure"]}}'

3. 参考文档

在与 AI 对话时提供 CubeJS 文档链接:

arduino 复制代码
"参考 CubeJS 文档 https://cube.dev/docs/schema/reference/cube
创建一个包含 measures 和 dimensions 的数据模型"

本节小结

本节我们完成了 CubeJS 的集成:

  1. CubeJS 概念:理解了 Cube、Measures、Dimensions 等核心概念
  2. Docker 部署:使用 docker-compose 快速部署 CubeJS 服务
  3. 数据模型:创建了 employees、departments、dept_emp 三个 Cube
  4. 度量定义:定义了计数、平均值等多种度量类型
  5. 维度定义:定义了字符串、时间、数值等多种维度类型
  6. Python 集成:创建了 CubeJSService 封装 CubeJS API
  7. 测试验证:通过多种方式验证 CubeJS 功能正常

现在我们有了统一的数据访问层,可以在此基础上构建 AI Agent。

思考与练习

思考题

  1. CubeJS 相比直接写 SQL 有什么优势?在什么场景下应该使用 CubeJS?
  2. Measures 和 Dimensions 的本质区别是什么?如何决定一个字段应该定义为哪种类型?
  3. 如果要支持实时数据更新,CubeJS 的配置需要如何调整?
  4. 如何在 CubeJS 中实现数据权限控制?

实践练习

  1. 扩展数据模型

    • 添加一个新的 Cube(如 salaries)
    • 定义相关的 Measures 和 Dimensions
    • 测试新 Cube 的查询功能
  2. 复杂查询

    • 实现多表关联查询
    • 实现带过滤条件的查询
    • 实现分组和排序查询
  3. 性能优化

    • 配置 CubeJS 的预聚合(Pre-aggregations)
    • 测试查询性能提升
    • 分析查询执行计划
  4. 服务封装优化

    • 添加查询缓存功能
    • 添加查询重试机制
    • 添加查询超时控制

上一节第 7 节:搭建 FastAPI 基础框架
下一节第 9 节:使用 Agno 构建 AI Agent

相关推荐
一块小方糖2 小时前
OA工时填报Skill,打通gitlab与禅道,实现每日工时自动提交
ai编程
QX_hao2 小时前
Codex_AGENTS_设置教学文档
ai编程
YimWu3 小时前
面试官:能聊聊 oh-my-opencode 这个插件都有啥内容吗?
前端·agent·ai编程
小小小小小鹿3 小时前
Claude Code Agent Skills 入门指南(下):理解Skill的工作机制
llm·ai编程·claude
crossoverJie3 小时前
DeepWiki 优化实战:代码行号与确定性目录生成
后端·ai编程
小虎AI生活5 小时前
全网爆火的 OpenClaw 迎来最强对手?腾讯“龙虾战略”的杀招在这
ai编程
智算菩萨6 小时前
ChatGPT 5.4 API深度解析:从Transformer架构到企业级应用实践
人工智能·深度学习·ai·chatgpt·ai编程
星辰引路-Lefan6 小时前
全平台 Docker 部署 CPA(CLIProxyAPI Plus) 灵活定制指南 (Linux/Windows)——接入Codex
linux·windows·docker·ai·ai编程
进击的野人6 小时前
新手入门:如何接入AI大模型?从零开始的实用指南
人工智能·agent·ai编程