Aplicação dos princípios de programação funcional no design de ERP

Olá Habr!

Neste artigo, tentaremos analisar a arquitetura dos sistemas de contabilidade (ERP, CRM, WMS, MES, B2B, ...) da perspectiva da programação funcional. Os sistemas existentes são complexos. Eles são baseados em um esquema de dados relacionais e têm um enorme estado mutável na forma de centenas de tabelas relacionadas. Além disso, a única "fonte de verdade" em tais sistemas é um diário ordenado cronologicamente de documentos primários (impressões de eventos do mundo real), os quais, obviamente, devem ser imutáveis ​​(e essa regra é observada em sistemas auditados onde as correções de datações são proibidas). O diário de documentos compõe 20% do volume do banco de dados à força, e todo o resto são abstrações e agregados intermediários, convenientes para trabalhar com SQL, mas que exigem sincronização constante com os documentos e entre si.

Se retornarmos às fontes (eliminar a redundância de dados e recusar armazenar agregados) e implementar todos os algoritmos de negócios na forma de funções aplicadas diretamente ao fluxo de documentos primários, obteremos um DBMS funcional e um ERP funcional construído sobre ele. O problema de desempenho foi resolvido graças à memorização e a quantidade de código funcional será proporcional à quantidade de SQL declarativa e não será mais difícil de entender. Neste artigo, demonstraremos a abordagem desenvolvendo o DBMS de arquivo mais simples no tempo de execução TypeScript e Deno (analógico ao Node.js), além de testar o desempenho de convoluções usando tarefas comerciais típicas como exemplo.

Por que isso é relevante


1) Um estado mutável + redundância de dados é ruim, especialmente quando é necessário garantir sua sincronização constante com o fluxo de documentos. Essa é uma fonte de discrepâncias de credenciais em potencial (a balança não converge) e difícil de detectar efeitos colaterais.

2) Um esquema relacional rígido para armazenar dados de origem e intermediários é caro em Big Data, sistemas heterogêneos e em condições de rápidas mudanças - isto é, de fato em toda parte. Sugerimos armazenar documentos em sua forma original, classificados por tempo, permitindo conexões “do novo ao antigo” e nunca vice-versa. Isso permitirá que a maioria das agregações seja calculada com algoritmos de passagem única diretamente dos documentos, e todas as outras tabelas não são necessárias .

3) O SQL está desatualizado, pois assume a disponibilidade de qualquer dado a qualquer momento e, em sistemas distribuídos, isso obviamente não é o caso - ao desenvolver algoritmos de Big Data, você precisa estar preparado para que alguns dos dados necessários apareçam mais tarde, e alguns já apareceram anteriormente. Isso requer um pouco de repensar a linguagem de consulta e uma preocupação consciente com o cache.

4) O PL moderno permite que você crie um sistema responsivo que opera com milhões de registros localmente em um laptop, onde o RDBMS simplesmente não é instalado. E se falamos de servidores - o esquema proposto tem mais possibilidades de paralelismo, inclusive nos clusters SPARK.

Antecedentes


Tendo trabalhado por algum tempo com vários softwares de negócios (contabilidade, planejamento, WMS), encontrei dois problemas em quase todos os lugares - a dificuldade de fazer alterações no esquema de dados e a frequente queda no desempenho quando essas alterações foram feitas. Em geral, esses sistemas têm uma estrutura complexa, pois requisitos conflitantes lhes são impostos:

1) Auditabilidade. É necessário armazenar todos os documentos primários inalterados. A divisão em diretórios e operações é muito condicional; nos sistemas adultos, os diretórios são limitados ao controle de versão, onde cada alteração é feita por um documento especial. Assim, os documentos de origem são uma parte imutável do sistema, e é a única "fonte de verdade", e todos os outros dados podem ser restaurados a partir dele.

2) Desempenho da consulta. Por exemplo, ao criar uma linha de ordem de venda, o sistema deve calcular o preço do produto, levando em consideração descontos, para os quais é necessário extrair o status do cliente, seu saldo atual, histórico de compras, ações atuais na região etc. Naturalmente, todas as informações necessárias não podem ser calculadas "on the fly", mas devem estar disponíveis em um formato semi-acabado. Portanto, os sistemas existentes armazenam abstrações convenientes em linhas de documentos (lançamentos), bem como agregados pré-calculados (registros de acumulação, intervalos de tempo, saldos atuais, lançamentos de resumo, etc.). Seu volume é de até 80% do tamanho do banco de dados, a estrutura da tabela é rigidamente fixa, com alterações nos algoritmos - o programador deve cuidar da atualização correta dos agregados. De fato, agregado é o estado mutável do sistema.

3) Desempenho transacional. Ao manter qualquer documento, você precisa recontar todos os agregados, e isso geralmente é uma operação de bloqueio. Portanto, os algoritmos de atualização agregada são o ponto mais doloroso do sistema e, quando um grande número de alterações é feito, há um risco significativo de algo quebrar e, em seguida, os dados "corroem", ou seja, os agregados não correspondem mais aos documentos. Esta situação é o flagelo de todos os projetos de implementação e o suporte subsequente.

Estabelecemos o básico da nova arquitetura


1) armazenamento. A base do banco de dados é um diário de documentos cronologicamente ordenados que reflete os fatos realizados do mundo real. Diretórios também são documentos, apenas de ação prolongada. O documento e cada versão da entrada do diretório são imutáveis. Nenhum outro dado na forma de transações / registros / saldos é armazenado no sistema (uma forte declaração provocativa, acontece de forma diferente na vida, mas é preciso buscar a perfeição ). O documento possui vários atributos do sistema:

{
    "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/pt482938/


All Articles