Application des principes de programmation fonctionnelle dans la conception d'ERP

Bonjour, Habr!

Dans cet article, nous tenterons d'examiner l'architecture des systèmes comptables (ERP, CRM, WMS, MES, B2B, ...) sous l'angle de la programmation fonctionnelle. Les systèmes existants sont complexes. Ils sont basés sur un schéma de données relationnelles et ont un énorme état mutable sous la forme de centaines de tables liées. De plus, la seule "source de vérité" dans de tels systèmes est un journal chronologiquement ordonné de documents primaires (empreintes d'événements du monde réel), qui, évidemment, doit être immuable (et cette règle est observée dans les systèmes audités où les corrections antidatées sont interdites). Le journal des documents représente 20% du volume de la base de données par force, et tout le reste est des abstractions et des agrégats intermédiaires, qui sont pratiques à utiliser avec SQL, mais qui nécessitent une synchronisation constante avec les documents et entre eux.

Si nous revenons aux sources (éliminons la redondance des données et refusons de stocker les agrégats), et implémentons tous les algorithmes commerciaux sous la forme de fonctions qui sont appliquées directement au flux de documents primaires, nous obtiendrons un SGBD fonctionnel et un ERP fonctionnel construit dessus. Le problème de performances est résolu grâce à la mémorisation , et la quantité de code fonctionnel sera proportionnelle à la quantité de SQL déclaratif, et pas plus difficile à comprendre. Dans cet article, nous allons démontrer l'approche en développant le SGBD de fichier le plus simple dans l' environnement d'exécution TypeScript et Deno (un analogue de Node.js), ainsi que tester les performances des convolutions en utilisant des tâches métier typiques comme exemple.

Pourquoi est-ce pertinent


1) Un état mutable + redondance des données est mauvais, surtout lorsqu'il est nécessaire d'assurer sa synchronisation constante avec le flux de documents. Il s'agit d'une source de divergences potentielles des titres de compétences (le solde ne converge pas) et difficiles à détecter les effets secondaires.

2) Un schéma relationnel rigide pour stocker les données source et intermédiaires coûte cher dans les Big Data, les systèmes hétérogènes et dans des conditions de changement rapide - c'est-à-dire en fait partout. Nous vous suggérons de stocker les documents dans leur forme originale, triés par temps, permettant des connexions «du nouveau au plus ancien» et jamais l'inverse. Cela permettra à la plupart des agrégats d'être calculés avec des algorithmes à passage unique directement à partir des documents, et toutes les autres tables ne sont pas nécessaires .

3) SQL est obsolète, car il suppose la disponibilité de toutes les données à tout moment, et dans les systèmes distribués, ce n'est évidemment pas le cas - lors du développement d'algorithmes de Big Data, vous devez être préparé pour que certaines des données nécessaires apparaissent plus tard, et que certaines soient déjà apparues plus tôt. Cela nécessite un peu de repenser le langage de requête et un souci conscient de la mise en cache.

4) Le PL moderne vous permet de créer un système réactif qui fonctionne avec des millions d'enregistrements localement sur un ordinateur portable, où le SGBDR ne s'installe tout simplement pas. Et si nous parlons de serveurs - le schéma proposé a plus de possibilités de parallélisme, y compris sur les clusters SPARK.

Contexte


Ayant travaillé pendant un certain temps avec différents logiciels d'entreprise (comptabilité, planification, WMS), j'ai rencontré presque partout deux problèmes: la difficulté d'apporter des modifications au schéma de données et la baisse fréquente des performances lors de ces modifications. En général, ces systèmes ont une structure complexe, car des exigences contradictoires leur sont imposées:

1) Auditabilité. Il est nécessaire de conserver tous les documents principaux inchangés. La division en répertoires et opérations est très conditionnelle; dans les systèmes pour adultes, les répertoires sont limités à la gestion des versions, où chaque modification est effectuée par un document spécial. Ainsi, les documents sources sont une partie immuable du système, et c'est la seule "source de vérité", et toutes les autres données peuvent en être restaurées.

2) Performances des requêtes. Par exemple, lors de la création d'une ligne de commande client, le système doit calculer le prix du produit, en tenant compte des remises, pour lesquelles il est nécessaire d'extraire le statut du client, son solde actuel, l'historique des achats, les parts actuelles dans la région, etc. Naturellement, toutes les informations nécessaires ne peuvent pas être calculées "à la volée", mais doivent être disponibles sous une forme semi-finie. Par conséquent, les systèmes existants stockent des abstractions pratiques sur des lignes de documents (écritures), ainsi que des agrégats pré-calculés (registres d'accumulation, tranches de temps, soldes courants, écritures récapitulatives, etc.). Leur volume peut atteindre 80% de la taille de la base de données, la structure de la table est fixée de manière rigide, avec tout changement dans les algorithmes - le programmeur doit veiller à la mise à jour correcte des agrégats. En fait, il s'agit de l'état mutable du système.

3) Performance transactionnelle. Lorsque vous détenez un document, vous devez recompter tous les agrégats, ce qui est généralement une opération de blocage. Par conséquent, les algorithmes de mise à jour d'agrégats sont le point le plus douloureux du système, et lorsqu'un grand nombre de modifications sont apportées, il y a un risque important de rupture, et les données se "corrodent", c'est-à-dire que les agrégats ne correspondent plus aux documents. Cette situation est le fléau de tous les projets de mise en œuvre et le soutien ultérieur.

Nous établissons les bases de la nouvelle architecture


1) Stockage. La base de la base de données est un journal chronologiquement ordonné de documents reflétant le fait accompli du monde réel. Les répertoires sont également des documents qui n'ont qu'une longue durée de vie. Le document et chaque version de l'entrée de répertoire sont immuables. Aucune autre donnée sous forme de transactions / registres / soldes n'est stockée dans le système (une déclaration provocatrice forte, cela se produit différemment dans la vie, mais vous devez viser la perfection ). Le document possède un certain nombre d'attributs système:

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


All Articles