首先,我将概述使用数据库时的一些问题和功能,我将展示抽象方面的漏洞。 接下来,我们将分析基于免疫的更简单抽象。
读者应该对Active Record , Data Maper , Identity Map和Unit of Work模式有点熟悉。
在足够大的项目中考虑问题和解决方案,这些项目不能扔掉并迅速重写。
身份图
第一个问题是保持身份的问题。 身份是唯一标识实体的事物。 在数据库中,这是主键,在内存中,是链接(指针)。 链接仅指向一个对象是很好的选择。
对于Ruby ActiveRecord库,情况并非如此:
post_a = Post.find 1 post_b = Post.find 1 post_a.object_id != post_b.object_id
即 我们获得2个对内存中2个不同对象的引用。
因此,如果我们不经意地开始使用相同的实体,但是由不同的对象表示,则可能会丢失更改。
Hibernate有一个会话,实际上是一个第一级缓存,用于将实体标识符到对象的映射存储在内存中。 如果我们重新请求相同的实体,我们将获得指向现有对象的链接。 即 Hibernate实现了Identity Map模式。
多头交易
但是,如果我们不按标识符选择怎么办? 为了防止对象状态和数据库状态不同步,请在请求选择之前先休眠Hibernate。
即 将脏对象转储到数据库中,以便请求读取约定的数据。
这种方法迫使您在业务事务进行时保持数据库事务处于打开状态。
如果业务交易很长,则负责数据库本身中连接的进程也将处于空闲状态。 例如,如果业务交易通过网络请求数据或执行复杂的计算,则会发生这种情况。
N + 1
ORM抽象中最大的“漏洞”也许是N + 1查询问题。
关于ActiveRecord库的ruby示例:
posts = Post.all
ORM使程序员想到他只使用内存中的对象的想法。 但是它可以与网络上可用的服务一起使用,并可以建立连接和进行数据传输
需要时间。 即使该请求被执行了50ms,那么一秒钟也会执行20个请求。
附加数据
为了避免上述N + 1问题,您可以这样写
要求 :
SELECT * FROM posts JOIN LATERAL ( SELECT * FROM likes WHERE post_id = posts.id ORDER BY likes.id DESC LIMIT 1 ) as last_like ON true;
即 除了帖子的属性外,还将选择last like的所有属性。 该数据映射到哪个实体? 在这种情况下,您可以从帖子中退还几对,因为 结果包含所有必要的属性。
但是,如果我们仅选择部分字段或未在模型中选择的字段(例如,喜欢的出版物数量)怎么办? 是否需要将它们映射到实体上? 也许只留下它们数据?
国家和身份
考虑一下js代码:
const alice = { id: 0, name: 'Alice' };
在这里,对象引用的名称为alice
。 因为 它是一个常量,则无法调用Alice另一个对象。 同时,对象本身仍然可变。
例如,我们可以分配一个现有的标识符:
const bob = { id: 1, name: 'Bob' }; alice.id = bob.id;
让我提醒您,一个实体有2个身份:数据库中的链接和主键。 并且即使保存后,常量也无法停止生成Alice Bob。
该对象(我们称为alice
的链接)执行2个任务:它同时对身份和状态进行建模。 状态是描述给定时间点的实体的值。
但是,如果我们将这两个职责分开并为国家使用不变结构怎么办?
function Ref(initialState, validator) { let state = initialState; this.deref = () => state; this.swap = (updater) => { const newState = updater(state); if (! validator(state, newState) ) throw "Invalid state"; state = newState; return newState; }; } const UserState = Immutable.Record({ id: null, name: '' }); const aliceState = new UserState({id: 0, name: 'Alice'}); const alice = new Ref( aliceState, (oldS, newS) => oldS.id === newS.id ); alice.swap( oldS => oldS.set('name', 'Queen Alice') ); alice.swap( oldS => oldS.set('id', 1) ); // BOOM!
Ref
保持不变状态的容器,允许对其进行受控替换。 Ref
模型的身份就像我们为对象命名一样。 我们称伏尔加河,但每时每刻都有不同的不变状态。
贮藏
考虑以下API:
storage.tx( t => { const alice = t.get(0); const bobState = new UserState({id: 1, name: 'Bob'}); const bob = t.create(bobState); alice.swap( oldS => oldS.update('friends', old => old.push(bob.deref.id)) ); });
t.get
和t.create
返回Ref
的实例。
我们打开业务交易t
,通过其标识符找到Alice,创建Bob并指出Alice认为Bob是她的朋友。
对象t
控制ref
的创建。
t
可以在其内部存储实体标识符到包含它们的ref
状态的映射。 即 可以实现身份映射。 在这种情况下, t
充当高速缓存;在爱丽丝的重复请求下,将没有对数据库的请求。
t
可以记住实体的初始状态,以便在事务结束时跟踪需要将哪些更改写入数据库。 即 可以实施工作单元 。 或者,如果将观察者支持添加到Ref
,则可以随着ref
每次更改将更改重置到数据库。 这些是解决变更的乐观和悲观方法。
使用乐观方法,您需要跟踪实体的状态版本。
从数据库更改时,我们必须记住版本,并且在提交更改时,请检查数据库中实体的版本是否与初始版本相同。 否则,您需要重复业务交易。 这种方法允许使用组插入和删除操作以及非常短的数据库事务,从而节省了资源。
使用悲观的方法,数据库事务与业务事务完全一致。 即 我们被迫在业务交易完成时从池中撤回所有连接。
该API允许您一次提取一个实体,这不是很理想。 因为 我们已经实现了Identity Map模式,然后可以在API中输入preload
方法:
storage.tx( t => { t.preload([0, 1, 2, 3]); const alice = t.get(0); // from cache });
查询
如果我们不希望多头交易,那么我们就不能通过任意键进行选择,因为 内存中可能包含脏对象,选择将返回意外结果。
我们可以使用查询来检索事务外的任何数据(状态),并在事务内重新读取数据。
const aliceId = userQuery.findByEmail('alice@mail.com'); storage.tx( t => { const alice = t.getOne(aliceId); });
因此,存在责任分工。 对于查询,我们可以使用搜索引擎使用副本来扩展阅读范围。 并且存储API始终与主存储(主存储)一起使用。 自然,副本将包含过时的数据,重新读取事务中的数据可以解决此问题。
指令
在某些情况下,可以在不读取数据的情况下执行操作。 例如,从所有客户的帐户中扣除月费。 或在发生冲突时插入和更新数据(向上插入)。
如果出现性能问题,可以用这样的命令替换来自Storage and Query的捆绑包。
通讯技术
如果实体彼此随机引用,则更改实体时很难确保其一致性。 关系试图简化,简化,放弃不必要的关系。
聚集是一种组织关系的方式。 每个集合都有一个根实体和嵌套实体。 任何外部实体都只能引用聚合的根。 根确保整个单元的完整性。 交易不能跨越集合边界;换句话说,整个集合都包含在交易中。
聚合可以例如由四边形(根)及其翻译组成。 或订单及其位置。
我们的API适用于整个集合。 同时,聚合之间的引用完整性取决于应用程序。 该API不支持链接的延迟加载。
但是我们可以选择关系的方向。 考虑一对多关系User-Post。 我们可以在帖子中存储用户ID,但是方便吗? 如果我们在用户中存储帖子标识符数组,我们将获得更多信息。
结论
我强调了使用数据库时的问题,并展示了使用免疫的选项。
本文的格式不允许详细显示该主题。
如果您对这种方法感兴趣,那么请从头开始关注我的书应用 ,该书描述了从头开始创建Web应用的过程,重点是架构。 它了解SOLID,Clean Architecture和使用数据库的模式。 本书中的代码示例和应用程序本身都是用Clojure语言编写的,这种语言充满了抗扰性和数据处理的便利性。