函数式编程原理在ERP设计中的应用

哈Ha!

在本文中,我们将尝试从功能编程的角度来看会计系统(ERP,CRM,WMS,MES,B2B等)的体系结构。 现有系统很复杂。 它们基于关系数据模式,并且以数百个相关表的形式具有巨大的可变状态。 而且,在这种系统中,唯一的“真理来源”是按时间顺序排列的原始文档日志(现实事件的打印),显然,该日志必须是不可变的(并且在禁止回溯更正的已审计系统中遵守此规则)。 文档日志强制使用了20%的数据库量,其他所有内容都是中间的抽象和聚合,它们在SQL语言中使用起来很方便,但需要与文档以及彼此之间保持不断的同步。

如果我们回到源头(消除数据冗余并拒绝存储聚合),并以直接应用于主文档流的功能形式实施所有业务算法,我们将获得一个功能强大的DBMS和一个功能强大的ERP。 由于有了备忘录 ,性能问题得以解决,功能代码的数量将与声明性SQL的数量相称,并且不难理解。 在本文中,我们将通过在TypeScript和Deno运行时 (类似于Node.js)中开发最简单的文件DBMS来演示该方法,并以典型的业务任务为例来测试卷积的性能。

为什么这很重要


1)可变状态+数据冗余性很差,尤其是在有必要确保其与文档流的持续同步时。 这是潜在的凭证差异的来源(余额不收敛)并且很难检测到副作用。

2)在大数据,异构系统以及快速变化的条件下(即实际上在任何地方),用于存储源数据和中间数据的刚性关系方案都很昂贵。 我们建议以原始格式存储文档,并按时间排序,以允许“从新到旧”的连接,反之亦然。 这样一来,大多数聚合就可以直接通过单次算法从文档中进行计算,而无需其他所有表格。

3) SQL已过时,因为它假定随时都有任何数据可用,并且在分布式系统中显然不是这种情况-开发大数据算法时,您需要为一些必要的数据做好准备以便稍后出现,而某些早已出现。 这就需要对查询语言进行一些重新思考,并有意识地关注缓存。

4) Modern PL使您可以创建一个响应式系统,该系统可在笔记本电脑上本地处理数百万条记录,而RDBMS根本不会在其中安装。 而且,如果我们谈论服务器-提出的方案在SPARK集群上具有更多的并行性可能性。

背景知识


在使用各种业务软件(会计,计划,WMS)已有相当长的时间后,我几乎到处都遇到两个问题-更改数据方案的难度,以及进行这些更改时的性能频繁下降。 通常,这些系统具有复杂的结构,因为对它们施加了冲突的要求:

1)可审核性。 必须保留所有原始文档。 目录和操作的划分是非常有条件的;在成人系统中,目录仅限于版本控制,其中每个更改都由特殊文档进行。 因此,源文档是系统的不变部分,并且是唯一的“真理源”,所有其他数据都可以从中恢复。

2)查询性能。 例如,在创建销售订单行时,系统必须考虑折扣的情况下计算产品价格,为此必须提取客户的状态,其当前余额,购买历史记录,该地区的当前份额等。 自然,所有必要信息都不能“即时”计算,而必须以半成品形式提供。 因此,现有系统在文档行(过帐)以及预先计算的汇总(累积寄存器,时间片,当前余额,汇总过帐等)上存储了方便的抽象。 它们的容量高达数据库大小的80%,表结构是固定不变的,并且算法有任何更改-程序员必须注意聚合的正确更新。 实际上,这是系统的可变状态。

3)交易绩效。 在保存任何文档时,您需要重新计算所有汇总,这通常是一项阻塞操作。 因此,聚合更新算法是系统中最痛苦的一点,当进行大量更改时,很有可能发生中断的风险,然后数据将“被破坏”,也就是说,聚合将不再与文档相对应。 这种情况是所有实施项目和后续支持的祸害。

我们建立新架构的基础


1)存放。 数据库的基础是按时间顺序排列的文档日记,反映了现实世界的既成事实。 目录也是文件,只是长效的。 文档和目录条目的每个版本都是不可变的。 系统中没有存储交易/记帐/余额形式的其他数据(强烈的挑衅性陈述,它在生活中会发生不同的变化,但您需要努力做到完美 )。 该文档具有许多系统属性:

{
    "sys": {
        "code": "partner.1",  // - ,  
        "ts": 1578263624612,  //   
        "id": "partner.1^1578263624612",  //    ID
        "cache": 1  //    
    },
    ...
}

code ts , , — . cache, -, -, id, code.

, . () — , ts. , - , ( , , ).

2) . id. «sql foreign key» — , , , , id . . , , ( — , ).

3) . , (.. ) , , 2 — . , , . — , . . — , 1, . , - — , .

4) . — , , , — . - — filter(), reduce(), get(), gettop(), . JOIN, , , , . , , , id / code, , ( ).

5) , . :

  • cache, reduce() -, -;
  • id / code, ;
  • reduce() , , .

, - «» , , , . , , , filter() , reduce() — . .

6) 3- . — . , reduce() , — . — , (.. ). -, . fullscan . — , - , .

7) . -. — - , ( ), — .


, - . TypeScript , Deno — TypeScript WASM, Rust API, ( ).
2- , JSON, "\x01", . API 3- :

type Document = any
type Result = any

public async get(id: string): Promise<Document | undefined>

public async gettop(code: string): Promise<Document | undefined>

public async reduce(
    filter: (result: Result, doc: Document) => Promise<boolean>, 
    reducer: (result: Result, doc: Document) => Promise<void>,
    result: Result
): Promise<Result>

id, code, , , . filter().reduce(), , — , — . reduce() , .

, filter reducer, - . , , get() reduce(). ( ) (. ).


. , . , — , .



{
    "sys": {
        "code": "partner.1",
        "ts": 1578263624612,
        "id": "partner.1^1578263624612",
        "cache": 1     
    },
    "type": "partner.retail",
    "name": "   "
}
{
    "sys": {
        "code": "invent.1",
        "ts": 1578263624612,
        "id": "invent.1^1578263624612",
        "cache": 1     
    },
    "type": "invent.material",
    "name": "  20"
}

type — , , . code — .



{
    "sys": {
        "code": "purch.1",
        "ts": 1578263624613,
        "id": "purch.1^1578263624613"  
    },
    "type": "purch",
    "date": "2020-01-07",
    "partner": "partner.3^1578263624612",
    "lines": [
        {
            "invent": "invent.0^1578263624612",
            "qty": 2,
            "price": 232.62838134273366
        },
        {
            "invent": "invent.1^1578263624917",
            "qty": 24,
            "price": 174.0459600393788
        }
    ]
}

(purch | sale), c ( ).



, , .

import { FuncDB } from './FuncDB.ts'
const db = FuncDB.open('./sample_database/')

let res = await db.reduce(
    async (_, doc) => doc.type == 'sale',  //   
    async (result, doc) => {
        result.doccou++
        doc.lines.forEach(line => {  //    
            result.linecou++
            result.amount += line.price * line.qty
        })
    },
    {amount: 0, doccou: 0, linecou: 0}  //  
)

console.log(`
    amount total = ${res.amount}
    amount per document = ${res.amount / res.doccou}
    lines per document = ${res.linecou / res.doccou}`
)


, Map.

class ResultRow { //   
    invent_name = ''
    partner_name = ''
    debit_qty = 0
    debit_amount = 0
    credit_qty = 0
    credit_amount = 0
}

let res = await db.reduce(
    async (_, doc) => doc.type == 'purch' || doc.type == 'sale',
    async (result, doc) => {
        //      await -   
        const promises = doc.lines.map(async (line) => {
            const key = line.invent + doc.partner
            let row = result.get(key)
            if (row === undefined) {
                row = new ResultRow()
                //      ( )
                row.invent_name = (await db.get(line.invent))?.name ?? ' not found'
                row.partner_name = (await db.get(doc.partner))?.name ?? ' not found'
                result.set(key, row)
            }
            if (doc.type == 'purch') {
                row.debit_qty += line.qty
                row.debit_amount += line.qty * line.price
            } else if (doc.type == 'sale') {
                row.credit_qty += line.qty
                row.credit_amount += line.qty * line.price
            }
        })
        await Promise.all(promises)
    },
    new Map<string, ResultRow>() //   ()
)

, . , , . , — — fullscan, .


:
: 100 + 100 + 100 .
: 10 + 10 + 10 .
Intel Celeron CPU N2830 @ 2.16 GHz

— , , . , 10 .

- 100 . 11.1 :
file: database_immutable.json:
 100200 docs parsed (0 errors)
 50018 docs processed (0 errors)
11.098s elapsed
file: database_current.json:
 10020 docs parsed (0 errors)
 4987 docs processed (0 errors)
1.036s elapsed
amount total = 623422871.2641689
amount per document = 11389.839613851627
lines per document = 3.6682561432355896

file: database_current.json:
 10021 docs parsed (0 errors)
 4988 docs processed (0 errors)
1.034s elapsed
amount total = 623433860.2641689
amount per document = 11389.832290707558
lines per document = 3.6682073954983925

, . , :
8.8s — JSON, "\x01"
1.9s — JSON
0.4s — +
Deno, , unicode, V8 - . , WASM/Rust , , JSON , — . .

- , . 3 , , , , , fullscan. , :

- 100 . 13.3 :
file: database_immutable.json:
 100200 docs parsed (0 errors)
 100000 docs processed (0 errors)
13.307s elapsed
file: database_current.json:
 10020 docs parsed (0 errors)
 10000 docs processed (0 errors)
1.296s elapsed

invent name | partner name | debet qty | debet amount | credit qty | credit amount | balance amount
===========================================================================
invent 92 | partner 50 | 164 | 34795.53690513125 | 338 | 64322.24591475369 | -29526.709009622435
invent 44 | partner 32 | 285 | 57382.115033253926 | 209 | 43572.164405352596 | 13809.95062790133
invent 95 | partner 32 | 340 | 73377.08274368728 | 205 | 42007.69685305944 | 31369.38589062784
invent 73 | partner 32 | 325 | 57874.269249290744 | 300 | 58047.56414301135 | -173.29489372060198
invent 39 | partner 32 | 333 | 69749.88883753444 | 415 | 86369.07805766111 | -16619.189220126675
invent 80 | partner 49 | 388 | 74965.03809449819 | 279 | 51438.03787960939 | 23527.0002148888
invent 99 | partner 49 | 337 | 69360.84770099446 | 292 | 58521.2605634746 | 10839.587137519862
invent 38 | partner 45 | 302 | 63933.21291162898 | 217 | 44866.95192796074 | 19066.26098366824
invent 34 | partner 45 | 343 | 69539.75051653324 | 205 | 41155.65340219566 | 28384.09711433758
invent 41 | partner 45 | 278 | 63474.209440233106 | 258 | 45246.446456763035 | 18227.76298347007
< tail skipped >

- , 2 , — 2.6s. , , , .


, , , — . , (-, EDI, - ), .



UPD
1) , VolCh .
2) CouchDB, apapacy .

PS

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


All Articles