news 2026/10/12 1:51:31

re-frame 中 app-db 的数据规范化:在内存 Map 中镜像服务端数据库结构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
re-frame 中 app-db 的数据规范化:在内存 Map 中镜像服务端数据库结构
  • 前端
  • Web框架

【免费下载链接】re-frame

A ClojureScript framework for building user interfaces, leveraging React

项目地址:https://gitcode.com/gh_mirrors/re/re-frame
点击查看免费下载

导读

re-frame 将所有应用状态集中存放在一个名为app-db的ratom(Reagent atom)中,其内部值是一个 Clojuremap。当你的服务端采用关系型数据库、并返回范式化(normalised)数据时,如何让这个内存map同样以规范化形态组织数据,从而与数据库结构一一对应?本文围绕官方 FAQ《DB Normalisation》展开,先讲清app-db的"内存数据库"本质,再介绍 compound、SubGraph、pull、reflet 等可复用库的取舍,最后讨论激进方案——用 DataScript 替换app-db(re-posh)。读完后,你将能在实践中依据数据规模与查询模式,选择正确的规范化落地方式。

app-db为什么是一个"map"?——先理解它的本质

在深入规范化之前,需要明确 re-frame 官方文档对app-db的定位。在 application-state.md 中,re-frame 作者明确指出:

re-frame puts all application state into one place, which is calledapp-db.

它实际就是一个 Reagent atom,初始值为一个空 map。仓库源码 src/re_frame/db.cljc 中的定义非常简洁:

(ns re-frame.db (:require [re-frame.interop :refer [ratom]])) (def app-db (ratom {}))

而ratom在 CLJS 端是 Reagent 提供的响应式 atom(src/re_frame/interop.cljs):

(defn ratom [x] (reagent.core/atom x))

在 JVM 端(用于跑测试或 JVM REPL),则退化为普通的 Clojureatom(src/re_frame/interop.clj):

(defn ratom [x] (atom x))

官方文档特意强调:与其把app-db想成"atom 里的一个 map",不如把它想成一个"内存数据库"(in-memory database)。既然是数据库,你就需要查询它、对它做 CRUD 和各种变换、还经常要原子地"事务式"更新它。这正是理解"规范化"这一话题的前提——规范化是数据库领域的概念,把它引入app-db,就是让内存中的这一份数据,结构上尽可能贴近你服务端数据库中的形态。

几点值得注意的官方约定:

  • 文档与代码中区分app-db(ratom 本身)与db(ratom 内部当前存放的 map 值),阅读代码时注意这个命名差异。
  • re-frame 会自动创建并管理app-db,你通常无需自己声明(src/re_frame/db.cljc)。
  • app-db并非必须是一个"装着 map 的 ratom"——理论上它可以是任何能在变化时通知你的数据库(详见后文 DataScript 部分)。

FAQ 原文的问题描述如下:

app-dbcontains amap. How do I store normalised data in amap, bettering mirroring the structure in my server-side database?

翻译过来就是:app-db里存的是一个 map,如何在这个 map 里存储规范化数据,以更好地镜像服务端数据库的结构?答案的落点,是推荐一批专门解决该问题的 Clojure/ClojureScript 库。

推荐方案一:引入专用规范化库

FAQ 原文首先推荐了四个值得关注的库,它们在"规范化与反规范化"这个主题上各有侧重:

库定位侧重点
compound规范化/反规范化工具库通用数据转换,适合自建数据层
SubGraph订阅层规范化与 re-framesubscribe/reg-sub深度结合
pullDatomic 风格 pull API用类似 Datomic 的语法从规范化数据中取图
reflet一套完整的 re-frame 封装在同一个app-db中混合使用规范化和非规范化数据

这些库的共同思路是:以 ID 而非嵌套结构来组织数据。比如一个包含作者信息的文章列表,非规范化的写法是:

;; 反规范化:文章里直接嵌套作者对象 {:articles [{:id 1 :title "A" :author {:id 7 :name "X"}} {:id 2 :title "B" :author {:id 7 :name "X"}}]}

规范化的写法则是把作者单独存一张"表",文章只引用 ID:

;; 规范化:作者独立存放,文章通过 ID 引用 {:articles [{:id 1 :title "A" :author-id 7} {:id 2 :title "B" :author-id 7}] :authors {7 {:id 7 :name "X"}}}

当服务端数据库返回的就是这种"ID 引用"形态的数据时,直接把它放进app-db,两者结构即可严格对应。随后用 pull 或 SubGraph 这类工具,在订阅时把规范化数据重新"拼装"成组件需要的嵌套视图。

FAQ 原文还特别提到:reflet 允许你在同一个app-db中同时使用规范化和非规范化数据。这是非常实用的现实场景——一个应用里,有些数据天然嵌套(例如表单草稿),有些数据来自服务端需要按 ID 引用(例如用户、文章),强制全部规范化反而增加负担。reflet 的价值正是解除这种"非此即彼"的约束。

FAQ 原文同时引用了一个 GitHub issue 中的讨论(re-frame issue #304 的评论),对"如何在 map 中存储规范化数据"做了更深入的背景阐述,建议对数据建模有疑问的读者查阅该讨论。

推荐方案二:整体转向 DataScript——"全情投入"路线

如果规范化需求已经到了极致,FAQ 原文给出了另一个更彻底的选择:

If you want to go whole hog and ditchapp-dband embrace DataScript, then you might find re-posh interesting.

即:彻底放弃app-db,拥抱 DataScript 数据库,并用 re-posh 把它接入 re-frame。

DataScript 是一个运行在内存中的 Datalog 数据库,天然以规范化(EAVT 三元组)方式存储数据,并提供强大的查询能力。application-state.md 中作者对这个方案的态度非常鲜明——"we'd love! to be using datascript - so damn cool - but we had too much data in our apps",即 re-frame 团队非常欣赏 DataScript,只是他们的应用数据量太大,才没有全面采用。

需要明确的是:DataScript 方案并非开箱即用。app-db在 re-frame 中承担着订阅、事件处理、trace 调试等核心流程的输入信号角色,从源码结构看:

  • 事件处理默认把app-db的当前值放进:coeffects的:db键(src/re_frame/cofx.cljc);
  • 订阅的默认signal-fn返回app-db(src/re_frame/subs.cljc);
  • :dbeffect 通过reset!用新 map 替换app-db(src/re_frame/fx.cljc)。

因此要用 DataScript,就必须"tweak re-frame a bit"(做一点改造),而 re-posh 正是帮你完成这一层对接的桥梁。FAQ 原文对这条路的定性是"whole hog"(全情投入)——它收益最大,代价也最大,适合数据关联关系复杂、查询模式多变、且内存数据量可控的应用。

规范化与app-db的天然契合点

规范化数据形态之所以与app-db契合,底层原因可以从 re-frame 的架构中看出来:

1. 单点数据源 + 订阅查询。app-db是唯一真源,任何视图只能通过订阅读取。规范化数据配合reg-sub层做拼装查询,恰好形成"存储规范化、视图反规范化"的分层:底层存 ID 引用,订阅时用pull/get-in/select-keys组合出组件要的嵌套结构。官方文档在 subscriptions.md 中把订阅视为对app-db的查询层,这正与数据库的"视图"概念同构。

2. 单一reset!即"事务提交"。官方文档指出,全部状态收拢进一个 atom 后,一次reset!就能让应用从一种状态原子地过渡到另一种状态,不存在中间不一致态。规范化数据天然要求"引用完整性"——比如删除作者时必须同步清理引用它的文章——而单点原子更新让这类多表联动可以在一个事务性更新中完成。re-frame 的:dbeffect 也实现了"新旧值相同则跳过重置"的优化(src/re_frame/fx.cljc)。

3. 树形结构直觉 vs 关系型结构现实。刚接触 re-frame 时,很多人习惯在app-db里维护一棵与 UI 组件树对应的嵌套树。但当数据主要来自服务端关系型数据库时,嵌套树会带来严重的重复存储与同步问题——同一份用户数据散落在多个地方,任何一处更新都要记得同步其余副本。规范化把"一份数据只存一份"这个原则落到内存中,是消除这类 bug 的根本手段。

如何验证数据形态:配合 schema 与调试工具

选择了规范化方案后,app-db的数据形态会变得更像"多个表",此时给数据加上 schema 约束的收益会成倍放大。官方文档在 application-state.md 中建议为app-db编写 clojure.spec,并且可以在每次事件处理完之后对全量数据做校验,从而尽早且精准地捕获错误。

调试方面,FAQ《Inspecting app-db》提供了实用的查看手段(docs/FAQs/Inspecting-app-db.md):

  • REPL 中直接检查re-frame.db/app-db;
  • JS console 中查看window.re_frame.db.app_db.state;
  • 在视图 hiccup 里嵌入打印,观察某个子路径:
[:pre (with-out-str (pprint (:part @re-frame.db/app-db)))] ;; see a part of it!

规范化之后数据层级变深、引用变多,这类"只看一部分"的调试手段会比整体打印实用得多。

小结与选型建议

回到 FAQ 的核心问题——"如何在 map 中存储规范化数据以镜像服务端数据库结构"——官方给出的答案路径是清晰的:

  1. 多数场景:保持app-db为普通 map,把服务端返回的规范化数据原样存入(按 ID 组织、引用而非嵌套),再借助 compound、SubGraph、pull 等库在订阅层做查询与拼装;reflet 让你可以在同一个app-db中按需混合规范化和非规范化数据。
  2. 重度关联场景:考虑"全情投入"——放弃app-db,用 DataScript 作为响应式数据源,通过 re-posh 桥接 re-frame。这条路线需要改造 re-frame 默认的数据接入方式(事件:coeffects、订阅默认signal-fn、:dbeffect 的reset!),从源码结构看(src/re_frame/cofx.cljc、src/re_frame/subs.cljc、src/re_frame/fx.cljc)是可行但需要额外工作的。
  3. 无论哪条路:都应配合 spec 校验与订阅层查询设计,把"存储规范化、视图反规范化"的分层坚持到底。

这条 FAQ 于 2017 年加入 re-frame 官方文档(见 docs/releases/2017.md 与 mkdocs.yml 中的导航条目),至今仍是 re-frame 数据建模问题的高频入口。对数据组织有疑问时,不妨先从"这份数据是一棵与组件树对应的树,还是一张与数据库对应的表"这个问题开始思考。

  • 前端
  • Web框架

【免费下载链接】re-frame

A ClojureScript framework for building user interfaces, leveraging React

项目地址:https://gitcode.com/gh_mirrors/re/re-frame
点击查看免费下载
上一篇:Olympus智能合约部署指南:从本地测试网到主网的无缝迁移
下一篇:如何将aria2-static-builds集成到自动化脚本:批量下载解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 1:50:16

数据库设计实战:在线学习系统库表设计与SQL优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/12 1:49:13

Natron Roto 节点 Python 脚本指南:ItemBase 抽象类 API 全面解析

音视频视频处理图形学桌面应用 【免费下载链接】Natron Open-source video compositing software. Node-graph based. Similar in functionalities to Adobe After Effects and Nuke by The Foundry. 项目地址: https://gitcode.com/gh_mirrors/na/Natron 点击查看 …

作者头像 李华
网站建设 2026/10/12 1:47:02

“我我我我我我”背后:网络重复表达的情绪与传播逻辑

前几天在某个群里看到一个挺有意思的片段:有人发了句“我我我我我我”,紧接着自己又补了一句“不好意思,激动了”。就这六个“我”,居然炸出了七八条回复,有人跟着复读,有人发“你结巴了?”&…

作者头像 李华
网站建设 2026/10/12 1:46:26

关于Original Research Article(二)写作

【写在前面的废话】大概因为我还不够强大还是处于照猫画虎的阶段,我常常想我应该先选刊再写还是写完再选刊。各个期刊出版社每一部分的表达安排文章结构布局似乎都略有不同。。。 图与表display items 图figure 图和表有了,论文的骨骼就有了,图表有了基本也就算实验告与段…

作者头像 李华