从应用程序处理数据库

首先,我将概述使用数据库时的一些问题和功能,我将展示抽象方面的漏洞。 接下来,我们将分析基于免疫的更简单抽象。


读者应该对Active RecordData MaperIdentity MapUnit of Work模式有点熟悉。


在足够大的项目中考虑问题和解决方案,这些项目不能扔掉并迅速重写。


身份图


第一个问题是保持身份的问题。 身份是唯一标识实体的事物。 在数据库中,这是主键,在内存中,是链接(指针)。 链接仅指向一个对象是很好的选择。


对于Ruby ActiveRecord库,情况并非如此:


post_a = Post.find 1 post_b = Post.find 1 post_a.object_id != post_b.object_id # true post_a.title = "foo" post_b.title != "foo" # true 

即 我们获得2个对内存中2个不同对象的引用。


因此,如果我们不经意地开始使用相同的实体,但是由不同的对象表示,则可能会丢失更改。


Hibernate有一个会话,实际上是一个第一级缓存,用于将实体标识符到对象的映射存储在内存中。 如果我们重新请求相同的实体,我们将获得指向现有对象的链接。 即 Hibernate实现了Identity Map模式。


多头交易


但是,如果我们不按标识符选择怎么办? 为了防止对象状态和数据库状态不同步,请在请求选择之前先休眠Hibernate。
即 将脏对象转储到数据库中,以便请求读取约定的数据。


这种方法迫使您在业务事务进行时保持数据库事务处于打开状态。
如果业务交易很长,则负责数据库本身中连接的进程也将处于空闲状态。 例如,如果业务交易通过网络请求数据或执行复杂的计算,则会发生这种情况。


N + 1


ORM抽象中最大的“漏洞”也许是N + 1查询问题。


关于ActiveRecord库的ruby示例:


 posts = Post.all # select * from posts posts.each do |post| like = post.likes.order(id: :desc).first # SELECT * FROM likes WHERE post_id = ? ORDER BY id DESC LIMIT 1 # ... end 

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.gett.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语言编写的,这种语言充满了抗扰性和数据处理的便利性。

Source: https://habr.com/ru/post/zh-CN433100/


All Articles