编程语言研究书库

CRUD 应用 — 数据库、密码哈希,和多页应用

CRUD 应用 — 数据库、密码哈希,和多页应用

这一章讲一件大事: 怎么做一个「真的」Web 应用——数据关浏览器后还在、 用户有账号密码、应用有多个页面。 这三个能力分别对应三个新工具:关系数据库(PostgreSQL)、密码哈希(bcrypt)、 多页应用(st.Page + st.navigation)。读完你掌握了几乎所有 Web 应用的地基—— 作者的开场白是:软件里百分之九十都是 CRUD1,换掉「俳句」这个词, 这套需求就是 Instagram、Asana、Twitter 的骨架2。

1. 这一章讲什么

应用是 Haiku Haven:注册/登录,写俳句、看自己写的俳句、改、删3。 和第 04 章待办清单的关键区别:数据必须活过浏览器关闭——之前所有应用的数据 都在内存或 session_state 里,一刷新就没了。这需要持久存储(persistent storage), 本章的答案是关系数据库4。

架构按三大件拆(书里 figure 8.2):前端(页面和控件)→ 后端(用户服务、俳句服务)→ 数据库(users、haikus 两张表)5。代码组织上新建 backend/ 和 frontend/ 两个目录, 入口 main.py 在根目录。

2. 核心原理(一):数据建模——从名词到表

关系数据库 = 按表(行 + 列)组织的数据,模式(schema)定义每张表的列和类型; 和数据frame 的区别关键在持久:程序退出,数据库还在6。

建模四步(书中流程7):

  1. 找实体:列应用里的核心名词。Haiku Haven 只有两个:user、haiku8。
  2. 定关系:说清两边各能有多少个。一个用户写很多俳句,一首俳句只属于一个用户—— 一对多(1:n)9。
  3. 列属性:user = 用户名 + 密码;haiku = 编号 + 创建时间 + 作者 + 正文10。
  4. 转成表:实体 → 表,属性 → 列;1:n 关系 → 外键(foreign key): haikus 表的 author 列存一个用户名,这个值必须存在于 users 表的 username 列11。

两个术语就此落地:**主键(primary key)**唯一标识一行(users 的主键是 username, haikus 的是 haiku_id);外键指向另一张表的主键(author → username)12。 ER 图(实体关系图)把这四样画在一张纸上,书里那张 figure 8.3 就是范例。

3. 核心原理(二):SQL 速成——五个命令走天下

SQL(结构化查询语言)是数据库的语言。本地装 PostgreSQL 后,用 psql 命令行交互; 生产用它的云版本。建库建用户的命令细节书里给了(建一个权限收窄的 haiku_lord 而不是用管理员 postgres——最小权限原则的早期示范13)。

五个命令,配本章的真实例子14:

-- 建表:VARCHAR(100) 是「最多 100 字符的字符串」,SERIAL 是自增整数
CREATE TABLE users (
  username VARCHAR(100) PRIMARY KEY,
  password_hash VARCHAR(128)
);
CREATE TABLE haikus (
  haiku_id SERIAL PRIMARY KEY,
  created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,  -- 默认填当前时间
  author VARCHAR(100),
  text TEXT,
  FOREIGN KEY (author) REFERENCES users(username)    -- 外键约束
);

INSERT INTO users (username, password_hash) VALUES ('alice', 'Pass_word&34');
SELECT * FROM users;                                   -- 查全部列
SELECT haiku_id, text FROM haikus
  WHERE author = 'alice' AND text LIKE '%fox%';        -- 条件 + 模糊匹配
UPDATE haikus SET text = '[By Alice] ' || text WHERE haiku_id = 1;  -- || 是拼接
DELETE FROM haikus WHERE haiku_id = 2;                 -- 漏掉 WHERE = 删全表!

(%fox% 的 % 是「任意字符」,LIKE 用它做子串匹配15。) 两个血泪点书里特意标红:UPDATE 和 DELETE 不带 WHERE 会作用到全表16; 俳句正文里的换行要写成 E'...\n...'(E 开头表示转义字符串)17。

4. 核心原理(三):连接数据库——st.connection 与事务

Python 侧装两个库:SQLAlchemy(数据库工具包,Streamlit 内部用它)和 psycopg2-binary(PostgreSQL 的适配器,即让 Python 连上 PostgreSQL 的翻译层)。然后写一个 Database 类把一切连接逻辑 收进一个方法18:

class Database:
    def __init__(self):
        self.conn = st.connection("haikuconn", type="sql")

    def execute_query(self, query, params={}, write=False):
        with self.conn.session as session:
            try:
                result = session.execute(text(query), params)
                if write:
                    session.commit()          # 写操作:盖章生效
                return result.fetchall()      # 返回 [ (列1,列2), … ] 的列表
            except Exception:
                if write:
                    session.rollback()        # 出错:撤销这次改动
                raise

三个新概念各一句话:session(会话) 是和数据库的一次「对话」; 事务(transaction) 是临时工作区,commit 让改动永久生效、rollback 全部撤销—— 这保证了「改一半出错不会留下烂摊子」;参数化查询(:username 占位 + params 字典) 把用户输入和 SQL 结构分开,是防 SQL 注入(攻击者用精心构造的输入篡改你的 SQL) 的第一道防线19。

连接参数照旧走 secrets.toml:[connections.haikuconn] 段, url 是 postgresql://用户:密码@地址:5432/库名 形式的连接串20。 还有一个隐蔽但重要的点:st.connection 的返回值被 Streamlit 内部用 st.cache_resource 缓存了——多用户共享一个连接,不会每个标签页开一个21。 这是两种缓存的分工第一次同时出现:cache_data 缓存数据(dataframe、API 结果), cache_resource 缓存资源(数据库连接、模型)——前者要可复制,后者要唯一。

5. 核心原理(四):密码安全——哈希 + 盐

需求:应用要能验证密码,但数据库里绝不能存明文密码——库一泄露,全军覆没。 解法是一条数学性质:单向哈希函数——正着算容易、反着推极难(书里用大质数相乘 打比方:乘起来容易,只给乘积分解出俩质数难)22。

  注册:  存 H(密码),不存密码本身。库里是 $2b$12$hVzjZJN7… 这种看不出原貌的串
  登录:  算 H(用户输入),和库里的 H 比对,相同即通过
  泄露场景:  黑客拿到 H 也登录不了——把 H 输进去,会被再哈希一次,对不上

  图说:哈希是「磨碎」不是「加密」——加密能解密,哈希设计目标是永远不可逆。

bcrypt 库包办两件事:bcrypt.hashpw(密码.encode(), bcrypt.gensalt()) 生成哈希; bcrypt.checkpw(输入, 哈希) 验证。gensalt() 里的「盐」(salt)是每次随机撒的一撮料: 防的是彩虹表(预计算好的「哈希 → 原文」大表)——同样的密码每次哈希结果不同, 查表就失效了23。User 数据类带两个方法:@staticmethod hash_password(静态方法, 属于类不属于实例,不需要 self)和实例方法 authenticate24。

顺带一提,新版 Streamlit 原生支持 st.login/st.logout(走 OpenID Connect, 对接 Google、Auth0 等身份提供商)——不用自己存密码;书里选了手写流程, 理由是教学价值25。我们的判断:学习选手写,生产选 OIDC,这个分工书里没明说,我们补上。

6. 核心原理(五):服务层与 Hub——类的组合

代码组织沿用第 03 章的关注点分离,但升级为类版本(本章是全书第一次写传统类, dataclass 那层糖衣在这里不够用了)26:

  • Database:只管连接和 execute_query(上一节)。
  • UserService / HaikuService:服务类,一组同族操作的集合。 UserService 有 create_user、get_user、get_authenticated_user; HaikuService 有 create_haiku、get_haikus_by_author、update_haiku、delete_haiku。 每个方法的模式完全相同:参数化 SQL → execute_query → 结果元组解包成数据类 (User(*results[0]),星号把元组拆成位置参数)27。
  • Hub:协调器,唯一职责是 new 出 Database、再把同一个 database 传给各服务; 前端只接触 hub,不碰 SQL28。

为什么不写一个巨大的 Hub?书里给了推演:方法会越来越多、越来越难治; 按领域拆成 UserService、HaikuService 后,以后加 LimerickService 都不用动现有的29。 这是「组合优于膨胀」的一次具体示范——也回答了第 03 章留下的悬念: 那章的「过度设计」长什么样、什么时候值得。

建用户那条 INSERT 用了一个顺手的新语法:RETURNING 列… 让 INSERT 像 SELECT 一样 返回刚写入的行,省一次再查30。

Streamlit 原生支持多页:入口文件当路由器,每轮重跑时选出当前页并 .run()31。

# frontend/pages.py
pages = {
  "login":  st.Page("frontend/login.py", title="Log in",  icon=":material/login:"),
  "signup": st.Page("frontend/signup.py", title="Sign up", icon=":material/person_add:"),
  "home":   st.Page("frontend/home.py",   title="Home",    icon=":material/home:"),
  "logout": st.Page("frontend/logout.py", title="Log out", icon=":material/logout:"),
}

# main.py
if 'hub' not in st.session_state:
    st.session_state.hub = Hub()
if st.session_state.get('logged_in'):
    page = st.navigation([pages['home'], pages['logout']])
else:
    page = st.navigation([pages['login'], pages['signup']])
page.run()

四件套各就各位:st.Page 定义一页(脚本路径 + 标题 + 图标;:material/xxx: 是 Google Material 图标库的引用语法)32;st.navigation 渲染侧边导航、返回被选中的页; page.run() 执行该页脚本;st.page_link 在页面之间放链接 (登录页底部「没有账号?去注册」)33。

跨页状态靠 session_state:hub 实例存进去,任何页都能 st.session_state.hub 取—— 多页应用共享同一份会话存储34。登录/登出流就是它的实战:

  登录页:验证通过 → st.session_state.logged_in = True;user = 用户对象 → st.rerun()
  下一次重跑:main.py 看到 logged_in → 导航换成 [home, logout],默认加载 home
  登出页:logged_in = False;user = None → st.rerun() → 导航换回 [login, signup]

  图说:页面切换不靠 URL 路由,靠「导航列表随状态变化 + 全页 rerun」。
  这是 Streamlit 多页模型和传统 Web 路由的根本差异。

注意 st.rerun 的位置:登录按钮的处理里必须调用,否则要等下一次交互才切换导航35。 CRUD 三件套随后落位:创建/编辑共用一个 @st.dialog 的 haiku_editor(编辑时传入 haiku、文本框预填旧文,保存后 st.rerun 关掉对话框刷新列表);列表用 get_haikus_by_author 查出后逐个 display_haiku(边框容器 + 四列:时间、正文、 编辑、删除;按钮 key 带 haiku_id 防重复)36;删除成功 rerun 刷新37。

8. 核心原理(七):部署——数据库也要搬上云

Community Cloud 不替你跑 PostgreSQL,所以数据库单独找托管服务:书里选 Neon (免费额度可观的托管 PostgreSQL)38。流程:注册 Neon → 拿到它生成的连接串 → 在 Neon 的 SQL Editor 里把两张 CREATE TABLE 重跑一遍 → secrets.toml 的 url 换成 Neon 的连接串(云端 Secrets 同步粘贴)→ requirements.txt 钉上 psycopg2-binary、 sqlalchemy、bcrypt → push39。

应用代码一行没改。 和第 07 章 BigQuery 迁移同一个道理,这次复用的是 「连接细节全在 secrets、代码只认 Database 类」这条边界。

9. 作者的判断与证据

判断:服务类 + Hub 优于单一巨型后端类。 依据是可扩展性推演(加 LimerickService 不动旧代码)40。证据是结构性的,在本章范围内成立;更大规模的应用会需要依赖注入、接口抽象等更重的手段,书里没走到那一步。

判断:手写认证有教学价值,尽管 st.login 已存在。 明说的取舍41。我们认可教学安排,但强调生产红线:自研认证很容易在细节上出错(密码重置、暴力破解防护、会话管理,本章一概未涉),生产应用应当用 OIDC 或成熟库,别自己攒。

判断:最小权限从建库开始。 不用管理员账号跑应用,专门建 haiku_lord42。这条没有得到展开,但它和第 05 章的密钥纪律是同一族的安全直觉,值得单独记住。

10. 边界与局限

  • 本章的认证是教学版:不区分「用户不存在」和「密码错误」(返回都是 None, 书中明说从简43),无防爆破、无密码重置、无邮箱验证。任何真实用户系统需要的 它都没有——这是范围声明,不是疏漏。
  • SQL 只教了五个命令:聚合(GROUP BY)、联表(JOIN)、索引一概未提; CRUD 够用,报表和性能问题会立刻撞墙。
  • Neon/点击流是时点快照;托管数据库的免费政策变化很快。
  • 对话框里 st.rerun 关弹窗是个顺手但脆弱的机制(依赖「按钮已点击状态被清」); Streamlit 版本演进中对话框生命周期语义可能变化。
  • 多页模型无 URL 路由(除非自己叠 query params):刷新后总在默认页, 深链到某页需要第 07 章的手艺。这是框架级约束,不是本章的错。

判断(我们的,不是书里的): 这一章是全书对「编程语言与系统研究」主题 贡献最直接的一章:它把三种经典系统构件(关系模型、密码学基础工具、会话状态) 装进了一个 60 页的教学单元,而且每一处都给了「为什么」——为什么 1:n 变外键、 为什么哈希不可逆、为什么 commit/rollback 成对出现。这些「为什么」正是 系统课该讲的东西。唯一要警惕的是它给的舒适感:跟着做,两天就能上线一个 「全栈应用」,这会让人低估真实系统的深度(认证、并发、注入、备份全是深渊)。 如果错,会错在: 服务层 + Hub 的模式在 Streamlit 小团队里其实很少见—— 真实 Streamlit 代码大量是「页面脚本直接调 st.connection」的平铺结构; 本章的架构是「面向未来的写法」,其收益要到多人维护和多次变更时才兑现, 单人一次性项目里它是负担。这个分界,本章没说。

11. 可带走的

  1. CRUD 四操作(Create/Read/Update/Delete)是九成应用的骨架;换名词不换结构。
  2. 数据建模四步:找实体 → 定关系(含基数)→ 列属性 → 转表(1:n → 外键)。
  3. 主键唯一标识行;外键指向别表主键,数据库替你强制这个约束。
  4. SQL 五命令:CREATE TABLE / INSERT INTO / SELECT…WHERE / UPDATE…SET / DELETE FROM;WHERE 一漏,全表遭殃。
  5. 连接串进 secrets.toml 的 [connections.xxx] 段;st.connection("xxx", type="sql") 按名取用。
  6. 写操作配 commit,异常配 rollback;参数化查询(:占位符 + params)防 SQL 注入。
  7. cache_data 缓存数据,cache_resource 缓存资源(连接);st.connection 已内置后者。
  8. 密码用 bcrypt 哈希 + 盐存储;验证 = 哈希输入后比对;哈希是磨碎,不是加密。
  9. 类 = 属性 + 方法;self 是实例自己;@staticmethod 属于类不属于实例。
  10. 服务类按领域拆(UserService/HaikuService),Hub 只做组装与转发;SQL 不许越过服务层进页面。
  11. 多页应用 = pages 字典 + st.navigation + page.run();跨页状态全放 session_state。
  12. 登录态 = session_state 里的布尔位 + 全页 rerun 驱动的导航切换;登出就是反向操作。
  13. 部署:数据库用托管服务(Neon),连接串换进云端 Secrets,代码零改动。

12. 原文地图

主题原书章原文位置
CRUD 无处不在的论点8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:12搜「Ninety percent」)
需求与范围外8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:61搜「Requirements」)
三大件架构图8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:128搜「three components」)
关系数据库与 schema 定义8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:169搜「persistent storage」)
建模四步与 ER 图8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:198搜「Identifying the entities」)
外键定义与 schema 图8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:246搜「foreign key」)
PostgreSQL 安装与最小权限建用户8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:328搜「security nightmare」)
SQL 速成(CREATE/INSERT/SELECT/UPDATE/DELETE)8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:369搜「crash course」)
Database 类与 execute_query8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:584搜「Database class」)
事务 commit/rollback 解释8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:726搜「temporary workspace」)
SQL 注入与参数化8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:710搜「SQL injection」)
connections.haikuconn 连接串8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:777搜「postgresql://」)
cache_resource 共享连接8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:872搜「Sharing the database」)
哈希 + 盐的原理8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:960搜「one-way cryptographic」)
User/UserService/Hub 三层8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:900搜「Splitting the app into services」)
RETURNING 子句8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1087搜「RETURNING username」)
st.Page / st.navigation / 图标语法8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1187搜「multi-page」)
session_state 跨页共享8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1249搜「shared between the pages」)
登录/登出流与 st.rerun8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1360搜「Navigating between pages」)
st.login / OIDC 补充8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1527搜「native support for authentication」)
HaikuService 与编辑器对话框8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1554搜「HaikuService」)
俳句列表显示与图标文件8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1719搜「display_haiku」)
更新/删除与 st.rerun 刷新8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1884搜「update and delete functionality」)
Neon 托管与部署8 Building a CRUD apptext/17-ch08-8-building-a-crud-app-with-streamlit.txt:1958搜「managed PostgreSQL」)

Footnotes

  1. 出处:「8 Building a CRUD app」第 35–48 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:16,搜「Ninety percent」)。原文引了 1957 年 Theodore Sturgeon 的「Sturgeon's law」。 ↩

  2. 出处:「8 Building a CRUD app」第 69–79 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:74,搜「universal pattern」)。 ↩

  3. 出处:「8 Building a CRUD app」第 53–68 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:53,搜「Requirements」)。 ↩

  4. 出处:「8 Building a CRUD app」第 169–176 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:172,搜「lose their data and progress」)。 ↩

  5. 出处:「8 Building a CRUD app」第 125–167 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:129,搜「frontend, a backend, and a database」)。 ↩

  6. 出处:「8 Building a CRUD app」第 178–192 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:178,搜「Relational database concepts」)。 ↩

  7. 出处:「8 Building a CRUD app」第 194–202 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:196,搜「modeling data」)。 ↩

  8. 出处:「8 Building a CRUD app」第 204–210 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:205,搜「identify the entities」)。 ↩

  9. 出处:「8 Building a CRUD app」第 211–219 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:219,搜「one-to-many」)。 ↩

  10. 出处:「8 Building a CRUD app」第 220–227 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:221,搜「attributes of an entity」)。 ↩

  11. 出处:「8 Building a CRUD app」第 242–276 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:246,搜「foreign keys」)。author 列必须存在于 users 表的说明见第 273–276 段。 ↩

  12. 出处:「8 Building a CRUD app」第 248–276 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:250,搜「primary key」)。 ↩

  13. 出处:「8 Building a CRUD app」第 326–341 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:328,搜「broad administrative privileges」)。 ↩

  14. 出处:「8 Building a CRUD app」第 369–566 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:369,搜「crash course in SQL」)。E'…' 转义串见第 455–463 段。 ↩

  15. 出处:「8 Building a CRUD app」第 505–522 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:508,搜「LIKE」)。 ↩

  16. 出处:「8 Building a CRUD app」第 547–558 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:548,搜「every single row」)。 ↩

  17. 出处:「8 Building a CRUD app」第 455–463 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:462,搜「escape string」)。 ↩

  18. 出处:「8 Building a CRUD app」第 568–763 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:584,搜「Database class」)。listing 8.1 在 598–617 段。 ↩

  19. 出处:「8 Building a CRUD app」第 684–729 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:685,搜「conversation」)与第 710–716 段(SQL 注入注记)。 ↩

  20. 出处:「8 Building a CRUD app」第 764–804 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:776,搜「connections.haikuconn」)。 ↩

  21. 出处:「8 Building a CRUD app」第 872–892 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:883,搜「cache_resource」)。 ↩

  22. 出处:「8 Building a CRUD app」第 949–982 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:960,搜「one-way cryptographic」)。 ↩

  23. 出处:「8 Building a CRUD app」第 1013–1019 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1019,搜「rainbow table」)。 ↩

  24. 出处:「8 Building a CRUD app」第 986–1022 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:997,搜「hash_password」)。@staticmethod 的解释见第 1003–1006 段。 ↩

  25. 出处:「8 Building a CRUD app」第 1526–1549 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1530,搜「OpenID Connect」)。 ↩

  26. 出处:「8 Building a CRUD app」第 619–625 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:621,搜「peel back that layer」)。 ↩

  27. 出处:「8 Building a CRUD app」第 1023–1093 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1023,搜「UserService class」)。 ↩

  28. 出处:「8 Building a CRUD app」第 1094–1115 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1114,搜「coordinator」)。 ↩

  29. 出处:「8 Building a CRUD app」第 911–925 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:925,搜「LimerickService」)。 ↩

  30. 出处:「8 Building a CRUD app」第 1083–1091 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1089,搜「RETURNING clause」)。 ↩

  31. 出处:「8 Building a CRUD app」第 1186–1216 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1189,搜「router」)。 ↩

  32. 出处:「8 Building a CRUD app」第 1217–1235 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1231,搜「Material Symbols」)。 ↩

  33. 出处:「8 Building a CRUD app」第 1411–1442 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1413,搜「st.page_link」)。 ↩

  34. 出处:「8 Building a CRUD app」第 1248–1252 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1249,搜「shared between the pages」)。 ↩

  35. 出处:「8 Building a CRUD app」第 1486–1514 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1486,搜「Automatic redirection」)。 ↩

  36. 出处:「8 Building a CRUD app」第 1616–1665 与 1717–1833 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1620,搜「haiku_editor」)。 ↩

  37. 出处:「8 Building a CRUD app」第 1930–1943 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1932,搜「delete_button」)。 ↩

  38. 出处:「8 Building a CRUD app」第 1958–1984 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1964,搜「neon.tech」)。 ↩

  39. 出处:「8 Building a CRUD app」第 1986–2003 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1988,搜「requirements.txt」)。 ↩

  40. 出处:「8 Building a CRUD app」第 911–925 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:922,搜「more modular」)。 ↩

  41. 出处:「8 Building a CRUD app」第 1546–1549 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1549,搜「educational value」)。 ↩

  42. 出处:「8 Building a CRUD app」第 326–341 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:329,搜「narrowly-scoped user」)。 ↩

  43. 出处:「8 Building a CRUD app」第 1302–1316 段(text/17-ch08-8-building-a-crud-app-with-streamlit.txt:1314,搜「two ways」)。 ↩

On this page