数据库设计中的不变性


26

约书亚·布洛赫(Joshua Bloch)的《有效的Java》中的一项内容是,类应允许实例的变异尽可能少,最好根本不允许变异。

通常,对象的数据会保存到某种形式的数据库中。这使我开始思考数据库中的不变性,特别是对于那些代表较大系统中单个实体的表而言。

我最近一直在尝试的一种想法是尝试最小化我对代表这些对象的表行的更新,并尝试尽可能多地执行插入。

我最近正在尝试的一个具体示例。如果我知道以后可以在记录中附加其他数据,我将创建另一个表来表示该表,类似于以下两个表定义:

create table myObj (id integer, ...other_data... not null);
create table myObjSuppliment (id integer, myObjId integer, ...more_data... not null);

希望这些名字不是一字不漏的,只是为了说明这个想法。

这是数据持久性建模的合理方法吗?是否值得尝试限制在表上执行的更新,尤其是对于最初创建记录时可能不存在的数据填充空值?有时候这样的方法以后可能会引起严重的疼痛吗?


7
我觉得这是一个没有问题的解决方案...您应该进行更新,而不是进行详尽的调整以避免更新。
Fosco 2011年

我认为这主要是考虑一个解决方案的直观想法,并希望由尽可能多的人来运行它,并且在此过程中意识到这可能不是解决我所遇到问题的最佳解决方案。如果我在其他地方找不到它,我可能会对此问题提出另一个问题。
Ed Carrel's

1
有充分的理由避免数据库中的更新。但是,当这些原因出现时,更多的是优化问题,因此,在没有证明存在问题的情况下不应这样做。
Dietbuddha 2011年

6
我认为对于数据库内部的不变性有很强的论据。它解决了很多问题。我认为负面评论并非来自思想开明的人。就地更新是造成许多问题的原因。我会说,我们都落后了。就地更新是解决不再存在的问题的传统解决方案。存储很便宜。为什么呢 有多少个数据库系统具有审核日志,版本控制系统,需要分布式复制,众所周知,这需要支持扩展延迟的能力。不变性解决了所有这一切。
卷云

@Fosco绝对需要某些系统永远不要删除数据(包括使用UPDATE)。像医生的病历。
2013年

Answers:


25

不变性的主要目的是确保内存中的数据处于无效状态时不会出现即时事件。(另一个是因为数学记号大部分是静态的,所以不变的事物更易于在数学上进行概念化和建模。)在内存中,如果另一个线程在使用它时尝试读取或写入数据,则可能最终损坏或它本身可能处于损坏状态。如果您对一个对象的字段执行多个分配操作,则在多线程应用程序中,另一个线程可能会在这两者之间的某个时间尝试使用它-这可能很糟糕。

不变性通过首先将所有更改写入内存中的新位置,然后进行最后的分配作为改写对象的指针以指向新对象的下降切换步骤来解决此问题-在所有CPU上都是原子的操作。

数据库使用原子事务执行相同的操作:启动事务时,它将所有新更新写入磁盘上的新位置。当您完成事务时,它将磁盘上的指针更改为新更新的位置-它在很短的时间内完成,在此期间其他进程无法触摸它。

这与创建新表的想法完全一样,只是更加自动化和更加灵活。

因此,要回答您的问题,是的,不变性在数据库中很好,但是,不需要为此而创建单独的表。您可以只使用数据库系统可用的任何原子事务命令。


感谢您的回答。这种观点正是我需要认识到的直觉令人困惑地试图将几个不同的想法组合成一个单一的模式。
Ed Carrel

8
除了氛围之外,还有其他好处。我在OOP上下文中最经常看到的关于不可变性的论点是,不可变对象仅需要您在构造函数中一次验证其状态。如果它们是可变的,则要求每个可以更改其状态的方法还必须验证结果状态仍然有效,这可能会给类增加大量复杂性。此参数也可能适用于数据库,但它的作用要弱得多,因为db验证规则倾向于声明性的而非程序性的,因此不需要为每个查询重复它们。
Dave Sherohman 2011年

24

这取决于您希望从不变性中获得什么好处。宫阪丽的回答解决了一个问题(避免无效的中间国家),但这里又回答了另一个问题。

变异有时称为破坏性更新:当您变异对象时,旧状态会丢失(除非您采取其他步骤以某种方式明确地保留它)。相反,对于不可变数据,在某个操作之前和之后同时表示状态,或表示多个后继状态很简单。想象一下,通过突变单个状态对象来尝试实现广度优先搜索。

这可能最常在数据库世界中显示为时间数据。假设上个月您使用的是基本计划,但是16日您切换到了高级计划。如果我们只是改写了表明您正在执行的计划的字段,则可能难以正确设置帐单。我们也可能会错过分析趋势的能力。(嘿,看看这个本地广告系列做了什么!)

无论如何,当我说“数据库设计的不变性”时,这就是我的想法。


2
我不同意你的第三段。如果您想拥有历史记录(审核日志,计划变更日志等),则必须为此创建一个单独的表。复制Customer表的所有50个字段只是为了记住用户更改了计划,除了带来巨大的性能缺陷,随着时间的推移选择速度变慢,数据挖掘更加复杂(与日志相比)以及浪费的空间之外,什么都没有。
阿森尼·穆尔琴科

6
@MainMa:也许我应该只是说“去阅读有关时态数据库”。我的示例旨在作为时态数据的草图。我并不是说这总是代表变化的数据的最佳方法。另一方面,尽管当前对时态数据的支持还很差,但我希望趋势是将时态数据容纳在数据库本身中,而不是将其委托给诸如更改日志之类的“第二类”表示形式。
Ryan Culpepper

如果我们在审核表中保留更改历史记录(例如,具有此功能的Spring Boot和Hibernate),该怎么办?
Mohammad Najar

14

如果您对从数据库中的不变性或至少从提供不变性幻觉的数据库中获得的收益感兴趣,请选中Datomic。

Datomic是Rich Hickey与Think Relevance联合开发的数据库,有很多视频可以解释体系结构,目标和数据模型。搜索infoq,尤其是标题为Datomic,数据库作为值。在共鸣中,您可以找到Rich Hickey在2012年euroclojure会议上发表的主题演讲。confreaks.com/videos/2077-euroclojure2012-day-2-keynote-the-datomic-architecture-and-data-model

在vimeo.com/53162418中有一个演讲是面向开发的。

这是斯图尔特·哈洛韦(stuart halloway)的另一个作品,网址为:.pscdn.net / 008/00102 / videoplatform / kv / 121105techconf_close.html

  • Datomic是5个元组[E,A,V,T,O]中的时间事实数据库,称为基准。
    • E实体编号
    • 一个在实体属性的名称(可以有命名空间)
    • V属性值
    • T交易编号,有了这个您就有时间的观念。
    • O断言(当前值或当前值),拒绝(过去值)的一种操作;
  • 使用它自己的数据格式,称为EDN(可扩展数据表示法)
  • 交易为ACID
  • 使用数据日志作为查询语言,它是声明性的SQL +递归查询。查询用数据结构表示,并用您的jvm语言扩展,您不需要使用clojure。
  • 该数据库在3个单独的服务(流程,机器)中解耦:
    • 交易
    • 存储
    • 查询引擎。
  • 您可以分别扩展每个服务。
  • 它不是开源的,但是有免费的(如啤酒中的)Datomic版本。
  • 您可以声明一个灵活的模式。
    • 属性集是开放的
    • 随时添加新属性
    • 定义或查询没有僵化

现在,由于该信息被及时存储为事实:

  • 您要做的就是将事实添加到数据库中,您永远不会删除它们(法律要求时除外)
  • 您可以永久缓存所有内容。查询引擎作为内存数据库存储在应用程序服务器中(对于jvm语言,非jvm语言可以通过REST API进行访问。)
  • 您可以查询过去的时间。

数据库是查询引擎的值和参数,QE管理连接和缓存。由于您可以将db视为一个值,并且在内存中具有不变的数据结构,因此可以将其与由“将来”的值构成的另一个数据结构合并,并将其传递给带有将来值的QE和查询,而无需更改实际数据库。

Rich Hickey有一个名为codeq的开源项目,您可以在github Datomic / codeq中找到它,该项目扩展了git模型,并将对git对象的引用存储在一个无原子的数据库中,并对您的代码进行查询,可以看到一个例子,如何使用datomic。

您可以将datomic视为ACID NoSQL,使用基准可以对表或文档或Kv存储或图形进行建模。


7

避免更新而喜欢插入的想法是将数据存储构建为事件源的想法之一,这种想法通常会与CQRS一起使用。在事件源模型中,没有更新:聚合表示为其“转换”(事件)的顺序,因此存储是仅追加的。如果您对此感到好奇,
此站点包含有关CQRS和事件源的有趣讨论!


如今,CQRS和事件采购成为了亮点。
Gulshan

6

这与数据仓库世界中所谓的“缓慢变化的维度”以及其他域中的“时态”或“双时态”表有着非常密切的关系。

基本构造为:

  1. 始终使用生成的代理密钥作为主密钥。
  2. 您所描述的任何内容的唯一标识符都将成为“逻辑键”。
  3. 每行至少应具有一个“ ValidFrom”时间戳,并可选地具有“ ValidTo”时间戳,甚至还应具有“最新版本”标志。
  4. 在逻辑实体的“创建”上,插入一个带有当前时间戳“有效期自”的新行。可选的ValidTo设置为“ forever”(永久)(9999-12-31 23:59:59),Last Version设置为“ True”。
  5. 在逻辑实体的后续更新上。您至少如上所述插入新行。您可能还需要将先前版本的ValidTo调整为“ now()-1秒”,最新版本将其调整为“ False”
    1. 进行逻辑删除时(这仅适用于ValidTo时间戳!),请将当前行中的ValidTo标志设置为“ now()-1秒”。

这种方案的优点是,您可以在任何时间点重新创建逻辑实体的“状态”,拥有一段时间的历史记录,并且如果频繁使用“逻辑实体”,则可以最大程度地减少争用。

缺点是您存储了更多的数据,并且需要维护更多的索引(至少在逻辑键+ ValidFrom + ValidTo上)。逻辑键+最新版本上的索引大大加快了大多数查询的速度。这也使您的SQL复杂化!

除非您真的需要维护历史记录并有在特定时间点重新创建实体状态的要求,否则是否值得这样做取决于您。


1

具有不可变数据库的另一个可能原因是支持更好的并行处理。乱序进行的更新会永久破坏数据,因此必须进行锁定以防止这种情况,从而破坏并行性能。许多事件的插入可以以任何顺序进行,并且只要最终处理所有事件,该状态至少最终将是正确的。但是,与进行数据库更新相比,在实践中这很难进行,以至于您真的需要很多并行性才能考虑以这种方式进行处理-我建议这样做。


0

免责声明:我在DB:p中几乎是个新手。

话虽这么说,但这种对数据进行分类的方法对性能产生了直接影响:

  • 对主表流量少
  • 主表上较小的
  • 需要卫星数据意味着另一种查找是必要的
  • 如果所有对象都存在于两个表中,则会浪费更多的空间

根据您的要求,您可以欢迎或不欢迎,但这当然是要考虑的一点。


-1

我不知道如何将您的方案称为“不可变的”。

当存储在补充表中的值更改时会发生什么?看来您需要对该表执行更新。

对于真正不可变的数据库,仅需通过“ INSERTS”进行维护。为此,您需要一些识别“当前”行的方法。这几乎总是导致效率极低。您要么必须复制所有先前未更改的值,要么在查询时将多个记录的当前状态拼凑在一起。当前行的选择通常需要一些令人讨厌的SQL,例如(where updTime = (SELECT max(updTime) from myTab where id = ?)。

在DataWarehousing中会出现很多问题,您需要保留一段时间内的数据历史记录,并且能够选择任何给定时间点的状态。解决方案通常是“维”表。但是,当他们解决DW“谁是去年1月的销售代表”的问题时。它们没有提供Java不变类所具有的任何优势。

更具哲学意义;存在数据库来存储“状态”(您的银行结余,用电量,StackOverflow上的布朗尼点等),试图建立“无状态”数据库似乎是没有意义的练习。


对于单个记录,WHERE id = {} ORDER BY updTime DESC LIMIT 1通常效率不是太低。
2013年

@Izkata-尝试在三张桌子的中间插入:-)
James Anderson
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.