9月7日-8日,相约 2023 腾讯全球数字生态大会!聚焦产业未来发展新趋势! 了解详情
写点什么

Datomic 信息模型

  • 2013-11-13
  • 本文字数:6248 字

    阅读完需:约 20 分钟

Datomic 是一种新型数据库,它被设计为一系列简单服务的组合。它力求在传统的关系型数据库管理系统的能力,以及新一代冗余分布式存储系统的灵活的可伸缩性之间建立一种平衡。

动机

Datomic 力图完成以下几个目标:

  • 提供可靠的信息模型,而避免原地更新(update-in-place)
  • 充分利用冗余的可伸缩存储系统
  • 提供 ACID 事务和一致性
  • 在应用中支持声明性数据编程

Datomic 将数据库视为一个 _ 信息系统 _,每个信息都是一组事实,而事实就是指已经发生的事。由于人类不能够改变过去,因此它意味着数据库将 _ 累积 _ 事实,而不是原地更新结果。虽然过去可以遗忘,但却是不能改变的。因此,如果某些人“修改了”他们的地址,Datomic 会存储他们拥有新地址这个事实,而非替换掉老的事实(它只是在这个时间点被简单的回收了)。这个不变性(immutability)带来了很多重要的架构优势和机会。在上一篇文章中,我介绍了Datomic 的架构。这篇文章将会专注于信息模型与一些编程经验。

传统数据库(也包括许多新来者)专注于“现在”——即当前正确的事实集,但是由于这种做法造成它们丢失了信息。所有的业务都是在历史信息中寻找增长的价值,没有什么理由不去保留这些信息。这不仅仅是保留相关的历史数据,例如采用备份与日志方法的问题,而是保留它将能够支持各种决策流程。对于某个业务实体来说,了解你当前的地址以将某将些东西寄送给你确实是必需的,但是类似于“哪些客户经常性的搬家,他们通常来处哪里?”这样的问题对他们的市场或者产品开发团队来说也非常有趣。类似的还有供应商的产品价格历史等等,他们可不愿意为了查找数据而被迫去恢复备份文件,或者重新将每条日志都扫描一遍。

为什么是否保留活动的历史记录会成为一道是非题呢,考虑一下这个问题会十分有趣。毕竟,在计算机出现之前,人类就学会了保留各种累积数据。并且随着时间推移,进一步产生了“会计绝不能使用橡皮檫”这样的见解。我猜测早期的计算机系统只是单纯的不具有保留历史数据的能力(或者无法承受这种数据量)。但这种猜测在如今需要重新思考一下了,毕竟在过去的25 年间计算机的存储能力得到了百万倍的提升。开发者们还会因为无法将他们的代码库保存在一张软盘上,而拒绝使用类似Git 这样的版本控制系统吗?

数据库之所以称为数据库,主要原因是来自于它所对数据的处理能力。否则的话它就只不过是个存储系统而已。这种能力通常是来自于数据组织(比如通过索引)以及利用该组织的查询系统的一个综合体。开发者们对这样的能力非常感兴趣。随后容量更大的分布式冗余_ 存储系统_ 也开始得到应用,但这些系统的功能性则有所削弱。Datomic 的目标是建立于这些存储系统之上,以利用它们的可伸缩性,将组织化的信息存储其中,并且将那些失去的功能重新交还给开发人员使用。

结构与展现

每个数据库在其模型的底部都有一个基础单元,例如关系、行或者文档。对Datomic 来说,这个单元就是具有原子性的事实,我们将其称为一个 Datom

一个 Datom 包含以下组件:

  • 实体
  • 属性
  • 事务(数据库时间)
  • 新增 / 回收

这种展现形式与 RDF 命题的“主题 / 谓词 / 对象”数据模型非常相似,但由于 RDF 命题缺少了回收这一概念以及适当的展现形式,它的功能并不足以展现历史信息。而 Datomic 是面向商业信息系统的,它采用了封闭世界假定(closed world assumption)理论,以避免了语义网(semantic web)所面临的全局统一命名、开放世界(open world)以及共享语义等方面的挑战。一个Datom 是对一个事实的最小化而充分的展现。

将一个具有原子性的单元放置在模型的最下方确保了对新内容(novelty)(例如事务)的展现的大小和新事实本身的大小一样。否则,每次修改新内容的部分信息都必须重新提交整个文档,或者是为了避免重新提交而导致使用脆弱的增量结构(delta scheme)。

Datom 构成了一种单一的、平滑的、全局的关系,除此之外 Datomic 就没有别的结构化组件了。这一点非常重要,因为你的模型中结构化组件越多,你的应用程序就会显得越僵硬。比方说,在一个典型的关系型数据库中,每个关系都必须被命名,并且为了查找数据必须了解这些名称。更糟的是,为了创建类似于多对多这样的关系模型,必须人为地建立联合表(join table),而这些联合表的名称也必须为应用程序所了解。为了将应用程序与物理结构的决策进行分离,必须投入极大的精力去创建一系列的逻辑视图,但这些视图不仅数量众多并且用途单一。而文档存储的结构就更加僵硬,因为它已经硬编码在你的应用程序中的每个角落里了,只有极少数(如果有的话)类似于视图的工具可以为该结构稍稍提供一些间接抽象。

Schema

所有的数据库都有 schema,唯一的区别就是它们在多大程度上支持(或者要求)schema 的明确性。在 Datomic 中,必须在使用 schema 前预先定义属性。

属性本身就是实体,只是包含了以下一些特定的属性:

  • 名称
  • 值的数据类型
  • 基数(属性可以为多值)
  • 一致性
  • 索引的特性
  • 自然组件(你的双足可以认为是你的组件,但你的母亲就不是了)
  • 文档

应用到实体上的属性并没有什么限制,因此实体是开放且分散的。属性可以在多个实体间共享,并且可以使用命名空间以避免冲突。以下是一个属性的例子:

复制代码
:person/name
{:db/ident :person/name,
:db/valueType :db.type/string,
:db/cardinality :db.cardinality/one,
:db/doc "A person's name"}

与 Datomic 其它的各种交互形式一样,schema 也是由数据展现的,以上的 schema 是以 edn 格式展现的某个 map 对象。在 Datomic 中不存在数据定义语言(DDL)。

通过 Datom 这种简单的基元形式,以及分散的、(有可能)支持多值的属性,使用者就可以展现类似行(row)的元组(tuple)、类似结构化文档的实体、以及类似列存储的多列与图形数据等等了。

事务

Datomic 中的事务以其最基本的形式存在,它仅仅是原子性地提交至数据库并被接受的一系列断言与撤销。一个基本的事务就是一个 Datom 的列表:

复制代码
[[:db/add entity-id attribute value]
[:db/add entity-id attribute value]...]

再声明一次,Datomic 中所有的交互形式都是由数据展现的,以上是以 _edn_ 格式展现的一个嵌套列表,每个内部列表都按顺序表现一个 Datom。

复制代码
[op entity attribute value]

如果你打算提交同一个实体的多个事实,你可以用一个 map 对象来代替:

复制代码
[{:db/id entity-id,
attribute value,
attribute value}
...]

虽然在这篇文章中必须以文本的方式展现这些 Datom,但对于 Datomic 的设计来说有一点非常重要,即事务其实就是你的编程语言中创建的一些基本数据结构(例如 j.u.Lists、j.u.Maps 以及 array 等等)。Datomic 的主要表现形式既不是字符串也不是数据操作语言(DML),而是数据。

注意到在这里并没有在 Datom 中指定事务的部分,而是会由解释器提供该信息。也就是说,事务本身就是实体,并且事务能够断言它本身的事实,例如关于事务源头、外部执行时间、发起的进程等元数据信息。

当然,并非每个事务都可以表现为断言或撤消那么简单,它的实现需要由某种“后来者获胜”的竞态冲突方案进行处理。Datomic 也支持数据库函数的概念,这些函数是由普通的编程语言(例如 Java 或 Clojure)所写,并安装至数据库中的(当然,是以数据形式通过事务提交的)。一旦安装后,数据库函数调用就可以成为某个事务的一部分了:

复制代码
[[:db/add entity-id attribute value]
[:my/giveRaise sally-id 100]
...]

当数据库函数作为某个事务的一部分出现的时候,它就会被视为一个事务函数,在调用时会传递给它一个额外的参数作为它的第一个参数,该参数是 _ 数据库本身的内部事务值 _。函数还可以发起查询等操作。事务函数必需返回事务数据,它本身返回的任何数据都会替换成事务中的数据。这一过程将不断重复,直到所有的事务函数都返回简单的新增 / 撤消为至。在之前描述的事务中,giveRaise 函数会查找 Sally 的当前工资,它找到了具体的数字 45000,随后返回一个新值的断言,用以下形式返回事务数据:

复制代码
[[:db/add entity-id attribute value]
[:db/add sally-id :employee/salary 45100]
...]

由于 _:employee/salary_ 的基数为 1,新增了 Sally 的工资的这一事实会显式地撤消之前的相关事实。由于事务函数在事务内是以原子方式顺序进行的,可以用这些函数来进行人为的、无冲突的转换。你可以在这篇文档中了解数据库函数的更多信息。

连接与数据库值

在写操作方面的过程显得很平常。你使用一个URI 获取一个数据库连接,该URI 包含了如何连接该数据库,以及如果通过该数据库连接到当前使用的转换器的信息。可以通过调用该连接上的_transact_ 方法发起一个事务,并按以上描述的方式传递事务数据。

在读操作方面的过程就完全不同了。在传统的数据库中,读与查询都是连接对象的某个方法。你通过连接传递某个查询,它会到达当前数据库状态上下文所在的服务器,取决于该服务器上所使用的查询语言的限制,它可能会与其它操作竞争资源与同步性,包括写操作在内。

与传统数据库的方式相反,在Datomic 中,在连接对象上唯一的读操作就是db() 方法,而且它实际上不会到达服务器。相反,连接对象会持续地从数据库中获取信息,等到获取到了足够的信息时,它就能够以一个不可变对象的形式立即返回数据库中的值信息,并让你的应用程序对该对象进行处理。因此,对数据的所有调用和查询都发生在本地(底层的引擎在需要时会自行到数据库端获取数据,无需人为干预)。请注意,并非每个应用服务器结点都会保存整个数据库的信息,它们只会保存最近发生的新内容以及指向之前的数据在数据库中的指针。另外也不会发生任何“快照”操作。虽然对应用程序与查询引擎来说,数据库似乎始终可用,但实际的实现是非常轻量级的,它们仅仅引用了一些持久化数据结构的指针,包括内存中和数据库中的数据。大量的缓存功能都由底层负责实现。

查询

在Datomic 中,查询并不是连接对象上的某个方法,甚至不是数据库中的某个方法。相反,查询是一个独立的方法,它接受一个或多个数据源作为参数。这些数据源可以是数据库的值或者普通的数据集合,或者是这两者任意的组合形式。这种方式的一大好处就是查询操作的运行可以从数据库上下文中解脱出来。

Datomic 的客户端类库中包含了一个基于 Datalog 的查询引擎,Datalog 是一个基于逻辑的声明式查询语言,它使用了模式匹配的方式,这一点非常适合于查询 Datom 以及内存中的集合数据。

该查询的基本形式如下:

复制代码
{:find [variables...] :where [clauses...]}

或者也可以为列表使用这种替代(更容易键入)的形式:

复制代码
[:find variables... :where clauses...]

再次声明,以上仅仅只是以文本形式展现了你可以以编程方式创建的数据结构而已,查询是数据而不是字符串,当然如果你传入了字符串,也能够接受它并随后转换为数据。

如果你的数据库中包含了以下 Datom(在这里 sally、fred 和 ethel 代表了他们各自的实体的 Id):

复制代码
[[sally :age 21]
[fred :age 42]
[ethel :age 42]
[fred :likes pizza]
[sally :likes opera]
[ethel :likes sushi]]

我们就会以这种方式创建一个查询:

复制代码
;;who is 42?
[:find ?e :where [?e :age 42]]

并且获得以下结果:

复制代码
[[fred], [ethel]]

_:where_ 语句是部分匹配的。对于数据库源来说,每个匹配的 Datom 会展现为元组的方式

复制代码
[entity attribute value transaction].

你可以从返回值的右方去除任意一部分(在本例中就是 transaction)。以?开头的符号代表变量,返回结果会包含每个匹配该变量的源元组所对应的值元组。

查询的联合是隐式的,并且只会在你使用某个变量超过一次时才会发生:

复制代码
;;which 42-year-olds like what?
[:find ?e ?x
:where [?e :age 42]
[?e :likes ?x]

它将返回:

复制代码
[[fred pizza], [ethel sushi]]

查询的 API 是一个名为 p 的方法:

复制代码
Peer.q(query, inputs...);

这里的 inputs 可以是数据库、集合或者是标量值等等。查询还可以(递归式地)应用各种规则,并且调用你的应用程序中的代码。你可以在这篇文档中了解关于查询的更多内容。

最终代码如下:

复制代码
//connect
Connection conn = Peer.connect("a-db-URI");
//grab the current value of the database
Database db = conn.db();
//a string for now, because Java doesn't have collection literals
String query = "[:find ?e :where [?e :likes pizza]]";
//who likes pizza?
Collection result = Peer.q(query, db);

相同的查询,不同的基准时间

当我们利用了 db 方法能够包含所有历史信息的特性之后,事实就开始变得有趣了:

复制代码
//who liked pizza last week?
Peer.q(query, db.asOf(lastTuesday));

对数据库执行 _asOf_ 方法,并指定一个时间值或是事务,会返回一个该数据库在过去某个时间点上的视图。请注意,在这里我们既没有调用连接对象的方法,也没有改变查询本身。如果你曾经自己实现过时间戳,你就会了解对某段暂时保留数据的查询与对当前数据的查询往往是完全不同的。与之类似功能的相关方法还有 _since_。

复制代码
//what if we added everyone from Brooklyn?
Peer.q(query, db.with(everyoneFromBrooklyn));

_with_ 方法会接受事务数据,并返回数据库的一个本地值,其中包含了新增的数据。这里不会将事务传递给连接对象,因此你可以在提交事务之前进行推测查询、如果查询以及检查事务数据。另外还有一个 _filter_ 方法可以根据某些谓词返回过滤后的数据。在这里我们依然没有用到连接对象、db 或者 query。

如果我们想测试查询,但又不想搭建一个数据库该怎么办呢?我们可以提供一些简单的数据,而沿用同样的方式进行查询:

复制代码
//test the query without a database
Peer.q(query, aCollectionOfListsWithTestData);

query 还是保持不变,但确实已经运行了, 不同之处就是模拟了一个数据库的连接对象。

目前为止,所有的查询技术都是针对过去或未来的某个特定时间点进行的,但许多有趣的分析都需要查询跨时间的数据:

复制代码
//who has ever liked pizza?
Peer.q(query, db.history());

_history_ 方法会返回所有时间里的全部 Datom,并且可以和 _asOf_ 等方法结合使用。你可以直接使用这个方法,不过跨时间的查询经常会有些不同之处,比如经常需要聚合数据等等。

查询可以接受多个数据源,因此可以轻易地跨数据库执行,或者使用某个数据库的不同视图。能够将集合传递给查询的功能就类似于 Steroids 上的参数化语句

不同的查询(或者关系),相同的基准时间

数据库值是不可变的,因此你可以随意进行非事务性的多步骤计算,而不必担心数据被更改了。与之类似,数据库的基础点也可以被获取并传递给另一个进程,该进程就可以在相同的状态下获取某个数据库值了。因此分布在不同进程上或不同时间上的查询也能够在相同的基础上工作了。

直接索引访问

最后,数据库值为从(不可变的)索引中迭代式地访问底层的已排序 Datom 提供了一个高性能的 API。可以根据这个基础功能创建各种不同的查询方式。比方说,使用这个 API,你就可以通过 Clojure 中类似 Prolog 功能的 _core.log_ 类库来查询 Datomic 数据库了。

结论

我希望本文使你对 Datomic 的信息模型的本质以及它的部分细节有了一个直观的感受。将数据库看作一个值是一种非常不同但又强大的方式,我想我们都在不断探索着各种可能性的存在!你可以从 Datomic 的文档中了解更多的知识。

关于作者

Richy Hickey是 Clojure 的作者以及 Datomic 的设计者,他作为一名软件开发者在各个领域已经有着超过 25 年的工作经验了。Rich 曾经参与过调度系统、广播自动化、音频分析与识别、数据库设计、收益管理、民意调查系统以及机器学习等多方面的内容,并基于多种语言进行开发。

查看英文原文: The Datomic Information Model

活动推荐:

2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。

2013-11-13 09:052066
用户头像

发布了 428 篇内容, 共 168.0 次阅读, 收获喜欢 35 次。

关注

评论

发布
暂无评论
发现更多内容

SpringBoot3集成Kafka

知了一笑

Java kafka 架构 springboot SpringBoot3

直播系统源码协议探索篇(二):网络套接字协议WebSocket

山东布谷科技

软件开发 websocket 源码搭建 直播系统源码 网络套接字协议

奖金丰厚 等你来拿!第六届开源创新大赛飞桨赛道下半场来啦

飞桨PaddlePaddle

人工智能 百度飞桨

Oracle-TiDB迁移-生僻字乱码问题

TiDB 社区干货传送门

迁移 实践案例 应用适配

我和 TiDB 的故事 | 远近高低各不同

TiDB 社区干货传送门

人物访谈 社区活动

PoseiSwap 更新质押系统,并将在 8 月18 日开启“Trident ”快照

鳄鱼视界

开放原子开源基金会TOC(技术监督委员会)第八十次全体会议

开放原子开源基金会

开源

SimpleDateFormat 线程安全问题修复方案 | 京东物流技术团队

京东科技开发者

jdk8 线程安全 SimpleDateFormat类 SimpleDateFormat 企业号 8 月 PK 榜

7种创建方式,带你理解Java的单例模式

华为云开发者联盟

Java 开发 华为云 华为云开发者联盟 企业号 8 月 PK 榜

API 自动化测试的佳实践

Apifox

软件测试 自动化测试 API测试 API开发 测试自动化工具

使用tidb-toolkit批量删除/更新数据

TiDB 社区干货传送门

性能调优 管理与运维 应用适配

永久激活版 Parallels Desktop 18 最新激活可用 附 pd 18激活工具

Geek_f9e1f3

Parallels Desktop 18 pd 18 Parallels Desktop 虚拟机

这,就是大模型时代的生产力!

飞桨PaddlePaddle

人工智能 paddle 百度飞桨 文心大模型 WAVE SUMMIT

Java如何生成随机数?要不要了解一下!

java易二三

Java 程序员 random 计算机

ThreadLocal不过如此

java易二三

Java 程序员 计算机

三生ONE物,无限可能|博睿数据上市三周年!

博睿数据

可观测性 智能运维 One 上市3周年

基于迁移学习的基础设施成本优化框架,火山引擎数智平台与北京大学联合论文被KDD收录

字节跳动数据平台

大数据 A/B测试 企业号 8 月 PK 榜

Vue 框架提升加载速度的经验分享

FinClip

java——反射与注解

java易二三

Java 程序员 计算机 API 科技

基于 Vercel & TiDB Serverless 的 chatbot

TiDB 社区干货传送门

社区活动

React请求机制优化思路 | 京东云技术团队

京东科技开发者

React 前端性能 企业号 8 月 PK 榜 react18 请求机制

ps ai beta 25.0全新上线!全新AI智能填充,支持中文关键词

Geek_2bc454

ps ai beta Photoshop beta爱国版

WIFI7 M.2 moudle-QCN9274+QCN6274-Pinnacle of WiFi field-support-MU-MIMO-OFDMA-TWT technology

wifi6-yiyi

6G WiFi 7

SpringBoot 太强了,这些优势你需要了解

java易二三

Java 程序员 Spring Boot 后端 计算机

支持M1、Parallels Desktop 18 for Mac激活可用 附pd 18激活密钥

Geek_2bc454

Parallels Desktop 18 pd 18 Parallels Desktop 虚拟机

校源行丨开放原子开源基金会赴福州走访交流

开放原子开源基金会

开源

一文带你读懂设计模式之责任链模式 | 京东云技术团队

京东科技开发者

源码分析 设计模式 责任链模式 企业号 8 月 PK 榜

PoseiSwap 更新质押系统,并将在 8 月18 日开启“Trident ”快照

威廉META

简单理解 TiDB Serverless branching

TiDB 社区干货传送门

数据库前沿趋势

PCTA 认证考试高分通过经验分享

TiDB 社区干货传送门

社区活动 6.x 实践

TiDB 源码编译之 TiFlash 篇

TiDB 社区干货传送门

新版本/特性解读 HTAP 场景实践 7.x 实践

  • 扫码添加小助手
    领取最新资料包
Datomic信息模型_语言 & 开发_Rich Hickey_InfoQ精选文章