Asami实体系统完全解析:对象、三元组与:db/ident的转换奥秘
发布时间:2026/8/20 19:36:08 作者:尧图编辑部 阅读量:1,286

Asami实体系统完全解析对象、三元组与:db/ident的转换奥秘【免费下载链接】asamiA graph store for Clojure and ClojureScript项目地址: https://gitcode.com/gh_mirrors/asa/asami在 Clojure/ClojureScript 生态中Asami 是一款无模式schemaless的图数据库而它的**实体系统Entity System**正是连接程序员友好的对象与数据库底层的三元组之间的那座桥梁。理解这套系统你就能彻底弄懂一个普通的 Clojure Map 是如何被拆解成图数据存储的读取时又是如何被拼装回原样的神秘的:db/ident在转换过程中究竟扮演了什么角色本文将从源码层面为你逐一揭秘 Asami 实体系统的完整工作原理。什么是 Asami 实体系统Asami 的定位是A graph store for Clojure and ClojureScript它既支持 JVM 上的持久化存储也支持浏览器端的纯内存存储。与关系型数据库不同Asami 不需要预先定义 Schema你可以直接向图数据库写入任意结构的 Clojure Map这就是实体Entity。实体系统的核心职责只有一个完成「对象Map⇄ 三元组Triple」的双向转换。三元组由(主体 subject, 属性 predicate, 客体 object)构成这也是所有 RDF 风格图数据库的通用语言。而 Asami 的实体模块就是让用户几乎感知不到三元组存在的翻译官。┌─────────────────────┐ writer.cljc ┌─────────────────────┐ │ Clojure 对象/Map │ ────────────────────────▶ │ 三元组 Triples │ │ {:name Toy Story}│ │ [:n :name Toy] │ └─────────────────────┘ └─────────────────────┘ ▲ │ └─────────────── reader.cljc ◀───────────────────┘ ref-entity 还原对象图数据库的三元组基础一句话看懂存储模型在进入实体系统之前先建立最小共识。Asami 中每个节点Node都可以拥有若干条[主体 属性 值]三元组。例如一部电影实体[:tg/node-10327 :movie/title Toy Story] [:tg/node-10327 :movie/genre animation/adventure] [:tg/node-10327 :movie/release-year 1995]其中:tg/node-10327是 Asami 自动生成的内存图节点 ID:movie/title等是属性字符串和数字是字面值。实体系统要做的就是把用户手写的 Map 自动翻译成这样的三元组集合并在查询后把节点 ID 重新还原成 Map。对象到三元组实体如何写入图数据库写入的核心逻辑位于 writer.cljc入口是entities-tripleswriter.cljc与ident-map-tripleswriter.cljc。转换过程分三步第一步为实体分配节点引用。map-triples会调用get-refwriter.cljc决定实体对应哪个内部节点 ID。规则如下输入方式行为无:db/id系统自动分配一个新节点 ID:db/id为负数视为临时 ID映射到系统分配的 ID事务返回的tempids可查到映射:db/id为正数直接使用该 ID 作为节点只有:db/ident先查库中是否已存在该 ident存在则复用节点实现 upsert 效果第二步递归展开属性值。value-tripleswriter.cljc根据值的类型分派Map→ 递归调用map-triples生成嵌套子实体List/Vector→ 通过list-triples生成链表结构每个元素用:tg/first指向值、:tg/rest指向下一个节点集合的:tg/contains提供快捷索引Set→ 按顺序转成链表nil→ 特殊节点:tg/nil标量数字、字符串等→ 直接作为字面值。第三步标记顶层实体。这是关键一步。在 ident-map-triples 中如果 Map 带有:db/ident会追加三元组[node-ref :tg/entity true]如果没有 ident则自动生成一个:tg/id-NNN形式的 ident 并同样标记。这个:tg/entity true标记正是读取时识别顶层实体的依据。三元组到对象实体如何读回读取逻辑集中在 reader.cljc。顶层 API 有三个ref-entityreader.cljc根据节点 ID 还原实体ident-entityreader.cljc根据:db/ident还原实体graph-entitiesreader.cljc取出库中全部顶层实体。读取流程的灵魂是pairs-structreader.cljc它负责把属性-值对组装回 Map同时剥离内部属性。看这段核心代码(into-multimap (comp (remove (comp #{:db/id :db/ident :tg/entity} first)) ;; 内部属性在此被剥离 (map (fn [[a v :as av]] (if ( :tg/nil v) [a nil] av))) (map (partial recurse-node graph seen)) ...) prop-vals)也就是说你在d/entity拿到的 Map 中永远看不到:db/ident、:db/id和:tg/entity——它们是系统内部使用的元数据。:db/ident 的转换奥秘三种身份的切换现在来到本文的核心谜题:db/ident到底是如何工作的1. 写入时ident 是唯一的身份证在 writer.cljc 中identity-prop?定义了#{:id :db/ident}两个识别属性。当写入一个带:db/ident的实体时系统会用 ident 查找是否已有同名节点[?n :db/ident ident]已存在 → 复用该节点这就是无 Schema 下的变相 upsert不存在 → 创建新节点并建立 ident 关联。2. 引用时ident 是精简的指针当 A 实体中的嵌套值引用 B 实体的:db/ident时系统不会把 B 的完整内容复制进去而是存入一个引用三元组。读取时recurse-nodereader.cljc检测到子节点带有:tg/entity标记或已被访问过就会短路返回简化的引用形式;; 原始数据 {:task/name Task 1 :task/requires [{:db/ident :t3}]} ;; 读回时得到的不是完整实体而是引用标记 {:task/name Task 1 :task/requires [{:db/ident :t3}]}这样既避免了无限递归A→B→A 的循环引用又保留了指针语义。3. 查找时三种方式按顺序匹配ident-entityreader.cljc的查找顺序非常有讲究(or (and (seq (node/find-triple graph [ident ?a ?v])) ident) ;; ① ident 本身就是节点 (ffirst (node/find-triple graph [?eid :db/ident ident])) ;; ② 通过 :db/ident 找 (ffirst (node/find-triple graph [?eid :id ident]))) ;; ③ 通过 :id 找优先把 ident 本身当作节点 ID 尝试找不到再反向通过:db/ident或:id属性查询。这也解释了为什么在内存图中没有显式 ident 的实体会被自动分配:tg/node-NNN这样的关键词作为默认 ident。:db/id 与临时 ID事务中的占位符在多实体相互引用时你不可能预先知道系统分配的节点 ID。Asami 的解决方案是负数临时 ID[{:db/id -1 :name Alice :friend {:db/id -2 :name Bob}} {:db/id -2 :friend {:db/id -1}}]:db/id -1、-2都是临时占位符事务过程中writer.cljc的get-ref会把负数 ID 映射到真实节点writer.cljc事务返回的:tempids字段记录{-1 → 真实ID, -2 → 真实ID}的完整映射引用方通过resolve-ref和:db/id查找解析回真实节点实现环状引用。隐藏的内部属性实体系统的暗物质整个转换过程会用到一批:tg/*前缀的内部属性见 writer.cljc 的注释。认识它们你就能读懂图数据库的原始存储内部属性含义使用者:tg/entity标记顶层实体值为 truewriter / reader:tg/owns子实体归属关系writer:tg/rest链表下一个节点列表存储:tg/first链表节点的值列表存储:tg/contains集合成员的快捷索引列表存储:tg/nil表示 nil 值值编码:tg/empty-list空列表值编码这些属性在pairs-struct中被系统性地剥离用户几乎永远接触不到——这正是对象与三元组转换透明性的体现。实战演练从写入到读回的一次完整旅程结合 README.md 的经典电影示例让我们看看完整流程(require [asami.core :as d]) (def db-uri asami:mem://dbname) (d/create-database db-uri) (def conn (d/connect db-uri)) ;; ① 写入Map → 三元组 (d/transact conn {:tx-data [{:movie/title Toy Story :movie/genre animation/adventure :movie/release-year 1995}]}) ;; ② 查询得到节点 ID (def db (d/db conn)) (d/q [:find [?m ...] :where [?m :movie/release-year 1995]] db) ;; (:tg/node-10327 :tg/node-10326) ;; ③ 读回节点 ID → 原始 Map (d/entity db :tg/node-10327) ;; #:movie{:title Toy Story, :genre animation/adventure, :release-year 1995}整个过程中transact内部调用entities/build-triplesentities.cljc把 Map 变成断言三元组d/entity则走storage/entity→ref-entity的还原链路core.cljc。对象的增删改查全部由实体系统在幕后自动完成。总结一把钥匙打开 Asami 的大门Asami 的实体系统可以用三句话概括写入是拆解Map 经writer.cljc递归拆成带内部标记的三元组:db/ident与:db/id决定节点的身份与复用读取是拼装三元组经reader.cljc的pairs-struct递归拼回 Map同时剥离内部属性、解析引用:db/ident是万能的钥匙它既是唯一标识又是引用指针还是查找索引一条属性贯穿了实体系统的全部转换环节。掌握了这套机制你不仅能自如地使用 Asami 存取复杂嵌套数据还能理解它无模式设计的底气所在。下次再遇到:db/ident相关的疑惑时不妨直接翻阅 reader.cljc 和 writer.cljc答案都写在源码里。【免费下载链接】asamiA graph store for Clojure and ClojureScript项目地址: https://gitcode.com/gh_mirrors/asa/asami创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考