Streamlit + Prisma 快速构建实时数据应用

📅 2026/7/14 16:29:19 👁️ 阅读次数 📝 编程学习
Streamlit + Prisma 快速构建实时数据应用

1. 项目概述:用 Streamlit + Prisma 搭建实时数据应用,到底省了多少事?

我做数据产品开发快八年了,从最早手写 Flask 后端配 ECharts 前端,到后来用 Dash 做 BI 看板,再到近两年高频接触 Streamlit——不是因为“它火”,而是因为它真正在解决一个长期被低估的痛点:数据工程师和算法同学想快速验证一个想法、跑通一条数据链路、给业务方交付一个可交互的最小闭环,根本不需要拉起一个完整 Web 工程团队。这篇内容讲的,就是怎么用 Streamlit 和 Prisma 组合拳,把“数据库连接 → 数据查询 → 实时刷新 → 表单交互 → 结果可视化”这一整条链路,压缩进不到 200 行 Python 代码里,且全程不碰 SQL 字符串拼接、不写 REST API 路由、不配 CORS、不处理连接池泄漏。关键词里那个 “Towards AI” 其实只是原始发布平台,真正核心是StreamlitPrisma这两个工具的协同逻辑——前者负责让 Python 数据逻辑直接变成网页,后者负责让数据库操作回归“面向对象”的直觉。它适合三类人:一是算法/数据科学家想自己搭个内部看数页,不想等前端排期;二是小团队后端开发者需要快速出 demo 验证需求;三是教学场景下带学生理解“数据流如何从库走到界面”。它不替代成熟的微服务架构,但能帮你砍掉 70% 的样板代码,把精力聚焦在“这个指标为什么异常”“那个筛选条件是否合理”这类真实问题上。我上周刚用这套组合帮风控组上线了一个实时审批流水监控页,从需求确认到部署上线只用了 1 天半,其中 4 小时花在写业务逻辑,剩下全是 Streamlit 自动搞定的 UI 渲染和 Prisma 自动生成的类型安全查询。

2. 整体设计思路与技术选型逻辑

2.1 为什么不是 Flask + SQLAlchemy?也不是 FastAPI + React?

这是每次聊到这个方案,同事必问的问题。我来拆解三层逻辑:

第一层是开发心智负担。Flask 或 FastAPI 写后端,你得先定义路由(@app.get("/users")),再写数据库查询(session.query(User).filter(...)),再手动序列化成 JSON(jsonable_encoder()),再考虑分页参数怎么传、错误怎么统一返回、JWT 怎么校验……这些都不是业务逻辑,但每一步都卡点。而 Streamlit 的范式是:“你写一个 Python 函数,它自动变成网页入口”。比如st.dataframe(get_latest_transactions()),这个函数里直接调 Prisma 查询,Streamlit 负责把结果渲染成可排序、可导出的表格,连分页按钮都是内置的。你不用想“这个接口该叫什么路径”,因为压根没有接口。

第二层是类型安全与开发效率的平衡。SQLAlchemy ORM 确实成熟,但它的模型定义(class User(Base): ...)和查询语法(.filter(User.name == 'xxx'))之间存在语义断层——你得记住filter接的是表达式,order_by接的是列对象,join的写法又不同。Prisma 的设计哲学是“数据库即 Schema”,你用 Prisma Schema 文件(schema.prisma)声明表结构,它自动生成类型严格的客户端。比如prisma.user.findMany(where: { status: 'active' }),IDE 能直接提示status只能是'active' | 'inactive' | 'pending',字段名写错会立刻报红。这在快速迭代时省下的调试时间,远超学习成本。

第三层是实时性实现的简洁度。传统方案要实现实时刷新,得上 WebSocket(复杂)、轮询(浪费资源)、或者 Sse(兼容性差)。Streamlit 原生支持st.experimental_rerun()st_autorefresh组件,配合 Prisma 的高效查询,3 行代码就能实现秒级刷新。我试过用 FastAPI + Vue 做同样功能,光是 WebSocket 连接状态管理、重连逻辑、消息解析就写了 200 多行,而 Streamlit 版本里,刷新逻辑就藏在if st.button("刷新"):里,点击即生效,无感。

所以这个组合不是“炫技”,而是精准匹配“中小规模、数据驱动、快速验证”场景的技术杠杆。它把数据库当数据源,把 Streamlit 当渲染引擎,中间 Prisma 是类型安全的翻译官——三者各司其职,没有冗余抽象。

2.2 Prisma 在这里扮演什么角色?它和传统 ORM 有什么本质区别?

很多同学第一次接触 Prisma,容易把它当成另一个 SQLAlchemy。其实它更像一个“数据库代理层 + 代码生成器”的混合体。关键差异有三点:

第一,Schema 驱动,而非代码驱动。
传统 ORM(如 Django ORM、SQLModel)要求你先写 Python 类,再通过makemigrations生成 SQL。Prisma 要求你先写schema.prisma文件,再运行prisma generate生成客户端。这个顺序反转,意味着数据库结构是唯一真相源。比如你改了 MySQL 表的created_at字段为DATETIME(3),只需更新 schema 文件里的@db.DateTime(3),重新生成,所有查询方法的类型签名自动更新。而 SQLAlchemy 里,你得手动改 Python 模型的Column(DateTime),再改迁移脚本,再跑alembic upgrade,稍有遗漏就类型不一致。

第二,查询 API 是声明式的,不是命令式的。
看一个典型对比:

  • SQLAlchemy:

    users = session.query(User).filter(User.status == 'active').order_by(User.created_at.desc()).limit(10).all()

    这里filterorder_bylimit是链式调用,但每个方法返回的对象类型不同,IDE 提示弱,拼错字段名 runtime 才报错。

  • Prisma:

    users = await prisma.user.find_many( where={"status": "active"}, order={"created_at": "desc"}, take=10 )

    参数是纯字典,whereordertake都是明确的关键字,值的类型由 Prisma 自动生成的类型定义约束。status只能填枚举值,ordercreated_at字段名 IDE 会自动补全,写错直接编译报错。

第三,原生支持关系预加载(Preload),避免 N+1 查询。
这是数据应用的隐形杀手。比如查 10 个订单,每个订单要显示用户姓名,传统 ORM 往往写成:

orders = get_orders() # 1 次查询 for order in orders: user = get_user_by_id(order.user_id) # 10 次额外查询!

Prisma 一行解决:

orders = await prisma.order.find_many( include={"user": True}, # 自动 JOIN,1 次查询拿到全部数据 )

include参数告诉 Prisma:“把关联的 user 表字段一起查出来”,生成的 SQL 就是SELECT ... FROM order JOIN user ON ...,没有 N+1。我在一个日活 5k 的内部看板里,把 SQLAlchemy 的 N+1 改成 Prisma 的include,首屏加载时间从 3.2s 降到 0.8s。

所以 Prisma 不是“另一个 ORM”,它是用现代类型系统(TypeScript/Python)重构数据库交互体验的尝试。它牺牲了一点“完全动态 SQL”的自由度,换来了开发阶段的确定性、运行时的性能保障和团队协作的清晰契约。

2.3 Streamlit 的“实时性”到底指什么?它和真正的实时推送有何区别?

这里必须划清界限:Streamlit 的“实时”不是指 WebSocket 推送,而是“按需重执行”。它的底层机制是:每次用户交互(点按钮、拖滑块、选下拉框),Streamlit 服务端会重新运行整个脚本文件,从头到尾执行一遍,然后把新结果渲染到前端。这听起来像“刷新页面”,但它比页面刷新聪明得多——它只更新变化的组件,保留输入框的值、图表的缩放状态,甚至能维持 session state 中的临时数据。

这种模式对数据应用恰恰够用,且更可控。比如一个销售漏斗看板,业务方想看“今天 vs 昨天”的对比,你写:

date_range = st.date_input("选择日期范围", [today - timedelta(days=1), today]) if st.button("加载数据"): data = fetch_funnel_data(date_range[0], date_range[1]) # Prisma 查询 st.line_chart(data)

点按钮,脚本重跑,fetch_funnel_data重新查库,图表更新。没有长连接,没有心跳包,没有服务器内存泄漏风险。而真正的 WebSocket 推送(如用 Socket.IO),需要维护连接状态、处理断线重连、广播消息给指定用户组——这些在内部工具里纯属过度设计。

当然,Streamlit 也提供了st_autorefresh这样的社区组件,可以设置interval=5000(5 秒刷新一次),底层还是定时触发st.experimental_rerun()。我在线上环境用它监控 Kafka 消费延迟,每 5 秒查一次prisma.kafka_metric.find_first(),延迟超过阈值就标红,效果稳定。它不像 WebSocket 那样毫秒级响应,但对“分钟级业务指标监控”这个场景,5 秒刷新足够,且运维简单——不需要额外部署消息中间件。

总结一句话:Streamlit 的实时,是“用户驱动的确定性重计算”,不是“服务端驱动的异步事件推送”。前者简单、可靠、易调试;后者强大、灵活、需更多基础设施。选哪个,取决于你的 SLA 要求。对于 90% 的内部数据应用,前者是更优解。

3. 核心细节解析与实操要点

3.1 环境准备:Python 版本、依赖安装与 Prisma 初始化

别跳过这一步。我踩过最大的坑,就是在一个旧项目里用 Python 3.8 装了prisma,结果运行时报ModuleNotFoundError: No module named 'prisma',折腾两小时才发现是 Prisma Python 客户端要求Python 3.9+。官方文档没强调这点,但实际生成的客户端代码用了typing.Union的新语法。所以第一步,确认 Python 版本:

python --version # 必须 >= 3.9

接着创建虚拟环境(强烈建议,避免全局污染):

python -m venv .venv source .venv/bin/activate # macOS/Linux # 或 .venv\Scripts\activate.bat # Windows

安装核心依赖:

pip install streamlit prisma python-dotenv

注意prisma包本身只是 Python 客户端,它需要 Prisma CLI 来生成客户端。CLI 是 Node.js 工具,所以还得装 Node.js(>=16.13)。如果你没装 Node,去官网下载 LTS 版本即可。装好后,全局安装 CLI:

npm install -g prisma

现在初始化 Prisma 项目:

prisma init

这条命令会做三件事:

  1. 创建prisma/目录;
  2. 生成prisma/schema.prisma文件,里面是数据库连接配置和默认模型;
  3. 创建.env文件,让你填数据库 URL。

打开prisma/schema.prisma,你会看到类似这样的内容:

generator client { provider = "prisma-client-py" } datasource db { provider = "postgresql" url = env("DATABASE_URL") }

这里provider = "prisma-client-py"是关键,它告诉 Prisma 生成 Python 客户端,而不是默认的 TypeScript 客户端。如果这行没写,后面prisma generate会生成 JS 代码,Python 里就 import 不了。

再看.env文件,它默认是:

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb"

你需要替换成自己的数据库地址。PostgreSQL 最常用,MySQL 和 SQLite 也支持。如果是本地 PostgreSQL,用psql创建数据库:

CREATE DATABASE streamlit_demo;

然后把 URL 改成:

DATABASE_URL="postgresql://your_user:your_password@localhost:5432/streamlit_demo"

密码里如果有特殊字符(如@/),必须 URL 编码。比如密码是p@ss/w0rd,要写成p%40ss%2Fw0rd。这个细节很多人忽略,导致连接失败,报错信息还很模糊。

提示:Prisma CLI 会读取.env文件,但 Streamlit 脚本默认不读。所以你在 Streamlit 里不能直接os.getenv("DATABASE_URL"),而要用 Prisma 客户端内置的加载逻辑。这是 Prisma 的设计,确保环境变量只在数据库层生效。

3.2 Prisma Schema 设计:从数据库表到 Python 类型的映射

Schema 文件是整个数据层的基石。我们以一个真实的电商监控场景为例:需要展示“今日订单量”、“各渠道转化率”、“高价值用户分布”。对应三张表:orderschannelsusers。在prisma/schema.prisma里这样定义:

generator client { provider = "prisma-client-py" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique tier String @map("user_tier") // 映射数据库字段名 createdAt DateTime @map("created_at") @default(now()) orders Order[] @@map("users") // 映射表名 } model Channel { id Int @id @default(autoincrement()) name String conversionRate Float @map("conversion_rate") createdAt DateTime @map("created_at") @default(now()) @@map("channels") } model Order { id Int @id @default(autoincrement()) userId Int channelId Int amount Float status String createdAt DateTime @map("created_at") @default(now()) user User @relation(fields: [userId], references: [id]) channel Channel @relation(fields: [channelId], references: [id]) @@map("orders") }

几个关键点解释:

  • @map("xxx"):数据库字段名和 Python 属性名不一致时的映射。比如数据库是user_tier,Python 里想叫tier,就用@map("user_tier")。这避免了 Python 命名规范(snake_case)和数据库习惯(snake_case)的冲突。
  • @default(now()):设置默认值为当前时间,Prisma 会在 INSERT 时自动填充,不用在 Python 里写datetime.now()
  • 关系定义user: User @relation(...):Prisma 会自动生成反向关系。比如user.orders就能拿到该用户所有订单,无需手动 JOIN。
  • @@map("xxx"):映射表名。如果数据库表叫user_profiles,但你想在 Python 里叫UserProfile,就用这个。

定义完,运行生成命令:

prisma generate

成功后,prisma/client目录下会出现 Python 客户端代码。此时你可以启动 Python 交互环境测试:

from prisma import Prisma prisma = Prisma() await prisma.connect() # 必须先 connect users = await prisma.user.find_many(take=5) print(users) await prisma.disconnect() # 记得断开

如果这步报错,90% 是.env里的DATABASE_URL不对,或者数据库没启动。我建议先用psql或 DBeaver 连一下数据库,确认账号密码、端口、库名都正确。

注意:prisma generate只生成客户端,不修改数据库。要创建表,得用prisma db push(开发环境)或prisma migrate dev(生产环境)。db push直接同步 schema 到数据库,适合本地快速试错;migrate dev会生成迁移文件,适合团队协作。新手推荐db push,一行命令搞定:prisma db push --preview-feature(加 preview-feature 是因为 Python 客户端还在预览期)。

3.3 Streamlit 应用骨架:从空白脚本到可运行页面

新建一个app.py文件,这是 Streamlit 的入口。不要试图一上来就写复杂逻辑,先搭骨架:

import streamlit as st from prisma import Prisma # 1. 页面配置 st.set_page_config( page_title="实时电商监控", page_icon="📊", layout="wide" # 宽屏布局,适合数据看板 ) # 2. 标题与简介 st.title("🚀 实时电商监控看板") st.markdown("基于 Streamlit + Prisma 构建,数据每 30 秒自动刷新") # 3. 初始化 Prisma 客户端(关键!) # 使用 st.cache_resource 装饰器,确保客户端只初始化一次 @st.cache_resource def get_prisma_client(): prisma = Prisma() prisma.connect() return prisma prisma = get_prisma_client() # 4. 主内容区占位 st.subheader("数据概览") st.write("这里将显示实时指标...")

保存后,在终端运行:

streamlit run app.py

浏览器会自动打开http://localhost:8501,看到标题和子标题。这就是最简可用版本。

现在解释几个关键点:

  • st.set_page_config:设置页面标题、图标、布局。layout="wide"很重要,Streamlit 默认窄屏,数据表格会被挤成一列,wide模式能让多列图表并排。
  • @st.cache_resource:这是 Streamlit 2023 年新增的装饰器,专为“昂贵的、全局共享的资源”设计,比如数据库连接、大模型加载。它保证get_prisma_client()只执行一次,后续所有用户访问都复用同一个连接实例。如果不加,每次用户刷新页面,都会新建一个 Prisma 连接,很快耗尽数据库连接池。我在线上环境见过没加这个装饰器,10 个并发就把 PostgreSQL 的 100 连接上限占满。
  • prisma.connect():必须显式调用,Prisma 不会自动连接。connect()是异步的,所以get_prisma_client函数要标记为async吗?不用。@st.cache_resource会处理异步初始化,你直接await prisma.connect()即可,Streamlit 内部会等待。

接下来,把“数据概览”部分替换为真实查询:

# 在 st.subheader("数据概览") 下面添加: col1, col2, col3 = st.columns(3) # 创建三列布局 # 今日订单量 with col1: st.metric("今日订单量", "1,248", "+12% vs 昨天") # 今日GMV with col2: st.metric("今日GMV", "¥248,932", "+8.3% vs 昨天") # 转化率 with col3: st.metric("平均转化率", "3.2%", "-0.1% vs 昨天")

st.metric是 Streamlit 的数据卡片组件,自带箭头图标和颜色区分涨跌。数值是静态的,下一步我们要让它动起来。

3.4 实现数据实时加载:Prisma 查询 + Streamlit 刷新控制

现在让指标变成动态的。我们写一个函数,用 Prisma 查询今日订单数:

from datetime import datetime, timedelta @st.cache_data(ttl=30) # 缓存 30 秒,避免频繁查库 def get_today_orders_count(): """获取今日订单总数""" today = datetime.now().date() count = prisma.order.count( where={ "createdAt": { "gte": datetime.combine(today, datetime.min.time()), "lt": datetime.combine(today + timedelta(days=1), datetime.min.time()) } } ) return count

@st.cache_data是另一个关键装饰器,用于缓存函数返回值。ttl=30表示 30 秒后自动失效,下次调用会重新执行函数。这样既保证了数据新鲜度(30 秒内不重复查库),又避免了每次用户操作都触发查询(比如用户点了个按钮,你不希望它立刻查库,而是用缓存值)。

然后在st.metric里调用:

# 替换之前的静态数字 with col1: count = get_today_orders_count() st.metric("今日订单量", f"{count:,}", "+12% vs 昨天") # f"{count:,}" 自动加千分位

但这样还是静态的。要实现“自动刷新”,有两种方式:

方式一:手动刷新按钮(推荐给生产环境)

if st.button("🔄 刷新数据"): st.cache_data.clear() # 清空所有 @st.cache_data 缓存 st.experimental_rerun() # 重跑整个脚本

点按钮,所有@st.cache_data失效,get_today_orders_count()重新执行,页面更新。好处是可控,不会因网络抖动导致意外刷新。

方式二:自动定时刷新(适合监控大屏)
安装社区组件:

pip install streamlit-autorefresh

然后在代码里:

from streamlit_autorefresh import st_autorefresh # 每 30 秒刷新一次 count = st_autorefresh(interval=30000, limit=100, key="fizzbuzzcounter") # 重新获取数据(注意:这里不能用 @st.cache_data,否则缓存不更新) def get_today_orders_count_live(): today = datetime.now().date() return prisma.order.count( where={ "createdAt": { "gte": datetime.combine(today, datetime.min.time()), "lt": datetime.combine(today + timedelta(days=1), datetime.min.time()) } } ) count = get_today_orders_count_live() st.metric("今日订单量", f"{count:,}")

st_autorefresh返回一个递增计数器,你用它作为key触发重渲染。这种方式简单,但要注意:@st.cache_data在自动刷新时可能不生效,所以查询函数最好不加缓存,或者用ttl短于刷新间隔。

实操心得:我在线上用方式一(手动刷新),因为业务方有时会说“等等,我还没看完这个数据”,自动刷新会打断他们。而方式二更适合放在会议室大屏上,没人操作,纯展示。

4. 实操过程与核心环节实现

4.1 构建交互式数据表格:支持筛选、排序、导出

数据看板的核心是表格。Streamlit 的st.dataframe功能强大,但需要和 Prisma 查询配合才能发挥最大价值。我们做一个“订单明细表”,支持按状态、渠道、时间范围筛选。

首先,写一个带参数的查询函数:

@st.cache_data(ttl=60) def get_orders( status: str = "all", channel_id: int = None, start_date: datetime = None, end_date: datetime = None, limit: int = 100 ): """获取订单列表,支持多条件筛选""" where_clause = {} if status != "all": where_clause["status"] = status if channel_id is not None: where_clause["channelId"] = channel_id if start_date or end_date: date_clause = {} if start_date: date_clause["gte"] = start_date if end_date: date_clause["lte"] = end_date + timedelta(days=1) # 包含 end_date 当天 where_clause["createdAt"] = date_clause return prisma.order.find_many( where=where_clause, include={ "user": True, "channel": True }, take=limit, order={"createdAt": "desc"} )

这个函数用了 Prisma 的嵌套where语法,createdAt的范围查询写成{"gte": start, "lte": end},非常直观。include加载了关联的userchannel,这样表格里能直接显示用户名和渠道名,不用额外查。

然后在 Streamlit 页面里构建筛选控件:

st.subheader("订单明细") # 创建筛选区域 col1, col2, col3 = st.columns(3) with col1: status_filter = st.selectbox("订单状态", ["all", "paid", "shipped", "cancelled"]) with col2: # 获取所有渠道,用于下拉选择 channels = prisma.channel.find_many() channel_options = {c.name: c.id for c in channels} channel_name = st.selectbox("选择渠道", ["全部"] + list(channel_options.keys())) channel_id = channel_options.get(channel_name) if channel_name != "全部" else None with col3: date_range = st.date_input("时间范围", [datetime.now().date() - timedelta(days=7), datetime.now().date()]) # 执行查询 if len(date_range) == 2: start_dt = datetime.combine(date_range[0], datetime.min.time()) end_dt = datetime.combine(date_range[1], datetime.max.time()) orders = get_orders( status=status_filter, channel_id=channel_id, start_date=start_dt, end_date=end_dt ) else: orders = get_orders(status=status_filter, channel_id=channel_id) # 渲染表格 if orders: # 转换为 Pandas DataFrame,方便 Streamlit 渲染 import pandas as pd df = pd.DataFrame([ { "订单ID": o.id, "用户邮箱": o.user.email if o.user else "N/A", "渠道": o.channel.name if o.channel else "N/A", "金额": f"¥{o.amount:.2f}", "状态": o.status, "时间": o.createdAt.strftime("%Y-%m-%d %H:%M") } for o in orders ]) st.dataframe( df, use_container_width=True, # 填满容器宽度 hide_index=True, # 隐藏默认索引列 column_config={ # 配置列样式 "金额": st.column_config.NumberColumn("金额", format="¥%d"), "时间": st.column_config.DatetimeColumn("时间", format="MM/DD/YY HH:mm") } ) # 添加导出按钮 csv = df.to_csv(index=False).encode('utf-8') st.download_button( label="📥 导出为 CSV", data=csv, file_name=f"orders_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv", mime="text/csv" ) else: st.info("暂无符合条件的订单")

这段代码展示了 Streamlit 的核心优势:

  • st.selectboxst.date_input等控件,天然绑定 Python 变量;
  • st.dataframe自动支持列排序(点列头)、列宽拖拽、横向滚动;
  • column_config可以格式化数字、日期,提升可读性;
  • st.download_button一行实现导出,不用写后端接口。

注意:st.dataframe渲染的是 Pandas DataFrame,不是 Prisma 的 model 对象。所以必须用列表推导式把orders转成字典列表,再转pd.DataFrame。这是 Streamlit 的约定,无法绕过。

4.2 构建动态图表:用 Prisma 数据驱动 Altair 可视化

Streamlit 原生支持多种图表库,我推荐 Altair,因为它的声明式语法和 Prisma 的查询风格高度一致——都是“描述我要什么,而不是怎么画”。

我们做一个“各渠道转化率趋势图”。先写查询函数:

@st.cache_data(ttl=300) def get_channel_conversion_trend(days: int = 30): """获取最近 N 天各渠道转化率""" from datetime import datetime, timedelta end_date = datetime.now() start_date = end_date - timedelta(days=days) # Prisma 不支持直接 GROUP BY + AVG,所以用 raw SQL # 但我们可以用 Prisma 的 $queryRaw 方法 sql = """ SELECT c.name as channel_name, DATE(o.created_at) as date, COUNT(*) as orders_count, COUNT(DISTINCT o.user_id) as unique_users, ROUND(COUNT(*)::decimal / NULLIF(COUNT(DISTINCT o.user_id), 0) * 100, 2) as conversion_rate FROM orders o JOIN channels c ON o.channel_id = c.id WHERE o.created_at >= $1 AND o.created_at < $2 GROUP BY c.name, DATE(o.created_at) ORDER BY c.name, date """ # Prisma Python 客户端支持 raw query results = prisma.query_raw( sql, start_date, end_date ) # 转成 Pandas DataFrame import pandas as pd return pd.DataFrame(results)

prisma.query_raw是 Prisma 的“逃生舱口”,当你需要复杂聚合、窗口函数时,就用它。这里我们用原生 SQL 计算转化率(订单数 / 独立用户数),并按渠道和日期分组。

然后用 Altair 画图:

import altair as alt st.subheader("渠道转化率趋势") # 获取数据 df = get_channel_conversion_trend(days=14) if not df.empty: # Altair 图表 chart = alt.Chart(df).mark_line(point=True).encode( x=alt.X('date:T', title='日期'), y=alt.Y('conversion_rate:Q', title='转化率 (%)'), color=alt.Color('channel_name:N', title='渠道'), tooltip=['date', 'channel_name', 'conversion_rate'] ).properties( width=700, height=400 ).interactive() # 支持缩放、平移 st.altair_chart(chart, use_container_width=True) else: st.warning("暂无转化率数据")

mark_line(point=True)画折线图并标出数据点;encode定义坐标轴和颜色;interactive()让图表可缩放,这对看趋势很重要。use_container_width=True确保图表填满列宽。

实操心得:Altair 的学习曲线比 Matplotlib 平缓,因为它的 API 更接近“说人话”。比如x='date:T'表示日期类型,y='conversion_rate:Q'表示定量数值,color='channel_name:N'表示分类变量。Prisma 的find_many返回对象,Altair 的Chart接受 DataFrame,两者通过 Pandas 无缝衔接。

4.3 实现表单提交:从 Streamlit 输入到数据库写入

前面都是读数据,现在看怎么写。我们加一个“新增渠道”功能,让用户在页面上填渠道名和转化率,点提交就存到数据库。

Streamlit 表单用st.form

st.subheader("新增渠道") # 创建表单 with st.form("add_channel_form"): col1, col2 = st.columns(2) with col1: new_channel_name = st.text_input("渠道名称", placeholder="例如:微信公众号") with col2: new_conversion_rate = st.number_input("转化率 (%)", min_value=0.0, max_value=100.0, value=2.5, step=0.1) submitted = st.form_submit_button("✅ 提交") # 处理提交 if submitted: if not new_channel_name.strip(): st.error("渠道名称不能为空!") else: try: # Prisma 写入 channel = prisma.channel.create( data={ "name": new_channel_name.strip(), "conversionRate": new_conversion_rate } ) st.success(f"✅ 渠道 '{channel.name}' 添加成功!ID: {channel.id}") # 刷新页面,显示新数据 st.cache_data.clear() st.experimental_rerun() except Exception as e: st.error(f"❌ 添加失败:{str(e)}")

st.form保证表单内所有输入在点击提交前不触发重运行,避免误操作。prisma.channel.create(data={...})是标准的创建语法,data字典的键必须和 Schema 里定义的字段名一致(nameconversionRate)。

这里有个隐藏技巧:st.experimental_rerun()后,页面重载,但st.text_input的值会清空,用户体验好。如果你用st.button而不是st.form_submit_button,输入框的值可能残留,造成混淆。

注意:生产环境一定要加权限控制。这段代码没有鉴权,任何访问者都能添加渠道。实际项目中,应该在if submitted:前加if st.session_state.get("is_admin"):,并通过登录流程设置st.session_state.is_admin = True。Streamlit 的st.session_state是页面级状态,适合存用户身份。

4.4 错误处理与连接池管理:避免线上事故

最后,也是最重要的,是健壮性。我见过太多 Streamlit 应用,本地跑得好好的,一上生产就报Connection refusedToo many connections。根源都在连接管理。

Prisma 连接池配置:
prisma/schema.prismadatasource块里,加连接池参数: