第 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 数据模型概念
核心概念:
- Cubes(数据立方体):数据的逻辑表示
- Measures(度量):可聚合的数值指标
- Dimensions(维度):用于分组和过滤的字段
- Segments(段):预定义的过滤条件
- 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 的集成:
- CubeJS 概念:理解了 Cube、Measures、Dimensions 等核心概念
- Docker 部署:使用 docker-compose 快速部署 CubeJS 服务
- 数据模型:创建了 employees、departments、dept_emp 三个 Cube
- 度量定义:定义了计数、平均值等多种度量类型
- 维度定义:定义了字符串、时间、数值等多种维度类型
- Python 集成:创建了 CubeJSService 封装 CubeJS API
- 测试验证:通过多种方式验证 CubeJS 功能正常
现在我们有了统一的数据访问层,可以在此基础上构建 AI Agent。
思考与练习
思考题
- CubeJS 相比直接写 SQL 有什么优势?在什么场景下应该使用 CubeJS?
- Measures 和 Dimensions 的本质区别是什么?如何决定一个字段应该定义为哪种类型?
- 如果要支持实时数据更新,CubeJS 的配置需要如何调整?
- 如何在 CubeJS 中实现数据权限控制?
实践练习
-
扩展数据模型:
- 添加一个新的 Cube(如 salaries)
- 定义相关的 Measures 和 Dimensions
- 测试新 Cube 的查询功能
-
复杂查询:
- 实现多表关联查询
- 实现带过滤条件的查询
- 实现分组和排序查询
-
性能优化:
- 配置 CubeJS 的预聚合(Pre-aggregations)
- 测试查询性能提升
- 分析查询执行计划
-
服务封装优化:
- 添加查询缓存功能
- 添加查询重试机制
- 添加查询超时控制