Anwendung der Prinzipien der funktionalen Programmierung bei der Gestaltung von ERP

Hallo habr

In diesem Artikel werden wir versuchen, die Architektur von Buchhaltungssystemen (ERP, CRM, WMS, MES, B2B, ...) aus der Perspektive der funktionalen Programmierung zu betrachten. Bestehende Systeme sind komplex. Sie basieren auf einem relationalen Datenschema und haben einen riesigen verĂ€nderlichen Status in Form von Hunderten von verknĂŒpften Tabellen. DarĂŒber hinaus ist die einzige "Quelle der Wahrheit" in solchen Systemen ein chronologisch geordnetes Journal von PrimĂ€rdokumenten (Drucke von Ereignissen der realen Welt), die offensichtlich unverĂ€nderlich sein mĂŒssen (und diese Regel wird in geprĂŒften Systemen beachtet, in denen RĂŒckdatierungskorrekturen verboten sind). Das Dokumentenjournal macht zwangslĂ€ufig 20% ​​des Datenbankvolumens aus, und alles andere sind Zwischenabstraktionen und -aggregate, mit denen in SQL bequem gearbeitet werden kann, die jedoch eine stĂ€ndige Synchronisierung mit Dokumenten und untereinander erfordern.

Wenn wir zu den Quellen zurĂŒckkehren (Datenredundanz beseitigen und die Speicherung von Aggregaten verweigern) und alle GeschĂ€ftsalgorithmen in Form von Funktionen implementieren, die direkt auf den Datenstrom der PrimĂ€rdokumente angewendet werden, erhalten wir ein funktionsfĂ€higes DBMS und ein darauf aufgebautes funktionsfĂ€higes ERP. Das Leistungsproblem wird durch das Auswendiglernen gelöst, und die Menge an funktionalem Code wird der Menge an deklarativem SQL entsprechen und nicht schwieriger zu verstehen sein. In diesem Artikel werden wir den Ansatz demonstrieren, indem wir das einfachste Datei-DBMS in TypeScript- und Deno-Laufzeit (analog zu Node.js) entwickeln und die Leistung von Windungen anhand typischer GeschĂ€ftsaufgaben testen.

Warum ist das relevant?


1) Ein verĂ€nderlicher Zustand + Datenredundanz ist schlecht, insbesondere wenn eine stĂ€ndige Synchronisation mit dem Dokumentenfluss sichergestellt werden muss. Dies ist eine Quelle fĂŒr mögliche Unstimmigkeiten bei den Anmeldeinformationen (das Gleichgewicht konvergiert nicht) und schwierig zu erkennende Nebenwirkungen.

2) Ein starres relationales Schema zum Speichern von Quell- und Zwischendaten ist in Big Data, in heterogenen Systemen und unter Bedingungen des raschen Wandels teuer - das heißt tatsĂ€chlich ĂŒberall. Wir empfehlen, Dokumente in ihrer ursprĂŒnglichen Form zu speichern, sortiert nach Zeit, um Verbindungen von „neu nach alt“ und niemals umgekehrt zu ermöglichen. Auf diese Weise können die meisten Aggregate mit Single-Pass-Algorithmen direkt aus den Dokumenten berechnet werden, und alle anderen Tabellen werden nicht benötigt .

3) SQL ist veraltet, da es davon ausgeht, dass zu jedem Zeitpunkt Daten verfĂŒgbar sind. In verteilten Systemen ist dies offensichtlich nicht der Fall. Bei der Entwicklung von Big-Data-Algorithmen mĂŒssen Sie darauf vorbereitet sein, dass einige der erforderlichen Daten spĂ€ter und andere bereits frĂŒher angezeigt werden. Dies erfordert ein kleines Überdenken der Abfragesprache und eine bewusste Sorge um das Caching.

4) Mit modernen PL können Sie ein reaktionsfĂ€higes System erstellen, das mit Millionen von DatensĂ€tzen lokal auf einem Laptop arbeitet, auf dem das RDBMS einfach nicht installiert wird. Und wenn wir ĂŒber Server sprechen - das vorgeschlagene Schema bietet mehr Möglichkeiten fĂŒr ParallelitĂ€t, auch in SPARK-Clustern.

Hintergrund


Nachdem ich einige Zeit mit verschiedenen Unternehmenssoftware (Buchhaltung, Planung, WMS) gearbeitet hatte, stieß ich fast ĂŒberall auf zwei Probleme - die Schwierigkeit, Änderungen am Datenschema vorzunehmen, und den hĂ€ufigen Leistungseinbruch, wenn diese Änderungen vorgenommen wurden. Im Allgemeinen haben diese Systeme eine komplexe Struktur, da an sie widersprĂŒchliche Anforderungen gestellt werden:

1) PrĂŒfbarkeit. Es ist notwendig, alle PrimĂ€rdokumente unverĂ€ndert zu speichern. Die Aufteilung in Verzeichnisse und Operationen ist sehr bedingt: In Systemen fĂŒr Erwachsene sind Verzeichnisse auf die Versionierung beschrĂ€nkt, bei der jede Änderung durch ein spezielles Dokument vorgenommen wird. Somit sind die Quelldokumente ein unverĂ€nderlicher Teil des Systems, und es ist die einzige "Quelle der Wahrheit", und alle anderen Daten können daraus wiederhergestellt werden.

2) Abfrageleistung. Beispielsweise muss das System beim Erstellen einer Kundenauftragsposition den Preis des Produkts unter BerĂŒcksichtigung von Rabatten berechnen, fĂŒr die der Status des Kunden, sein aktueller Kontostand, die Kaufhistorie, die aktuellen Anteile in der Region usw. extrahiert werden mĂŒssen. NatĂŒrlich können nicht alle notwendigen Informationen "on the fly" berechnet werden, sondern mĂŒssen in einer halbfertigen Form vorliegen. Daher speichern vorhandene Systeme bequeme Abstraktionen ĂŒber Belegzeilen (Buchungen) sowie vorberechnete Aggregate (Akkumulationsregister, Zeitscheiben, aktuelle Salden, Zusammenfassungsbuchungen usw.). Ihr Volumen betrĂ€gt bis zu 80% der DatenbankgrĂ¶ĂŸe, die Tabellenstruktur ist bei Änderungen der Algorithmen starr festgelegt - der Programmierer muss fĂŒr die korrekte Aktualisierung der Aggregate sorgen. In der Tat ist dies der verĂ€nderbare Zustand des Systems.

3) Transaktionsleistung. Wenn Sie ein Dokument halten, mĂŒssen Sie alle Aggregate neu zĂ€hlen. Dies ist normalerweise eine Sperroperation. Aggregataktualisierungsalgorithmen sind daher der schmerzhafteste Punkt im System, und wenn eine große Anzahl von Änderungen vorgenommen wird, besteht ein erhebliches Risiko, dass etwas kaputt geht, und die Daten "korrodieren", dh die Aggregate entsprechen nicht mehr den Dokumenten. Diese Situation ist die Geißel aller Implementierungsprojekte und der nachfolgenden UnterstĂŒtzung.

Wir legen die Grundlagen der neuen Architektur fest


1) Lagerung. Grundlage der Datenbank ist ein chronologisch geordnetes Tagebuch mit Dokumenten, die die vollendeten Tatsachen der realen Welt widerspiegeln. Verzeichnisse sind auch Dokumente, die nur lange wirken. Sowohl das Dokument als auch jede Version des Verzeichniseintrags sind unverĂ€nderlich. Es werden keine weiteren Daten in Form von Transaktionen / Registern / Salden im System gespeichert (eine starke provokative Aussage, die im Leben anders vorkommt, aber Sie mĂŒssen nach Perfektion streben). Das Dokument weist eine Reihe von Systemattributen auf:

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


All Articles