Penerapan prinsip-prinsip pemrograman fungsional dalam desain ERP

Halo, Habr!

Pada artikel ini kita akan mencoba melihat arsitektur sistem akuntansi (ERP, CRM, WMS, MES, B2B, ...) dari perspektif pemrograman fungsional. Sistem yang ada sangat kompleks. Mereka didasarkan pada skema data relasional, dan memiliki keadaan sangat besar yang bisa berubah dalam bentuk ratusan tabel terkait. Selain itu, satu-satunya "sumber kebenaran" dalam sistem semacam itu adalah jurnal dokumen primer yang dipesan secara kronologis (cetakan peristiwa dunia nyata), yang, jelas, tidak dapat diubah (dan aturan ini diamati dalam sistem yang diaudit di mana koreksi backdating dilarang). Jurnal dokumen membentuk 20% dari volume basis data secara paksa, dan yang lainnya adalah abstraksi menengah dan agregat, yang nyaman untuk bekerja dengan dalam bahasa SQL, tetapi yang membutuhkan sinkronisasi konstan dengan dokumen dan satu sama lain.

Jika kita kembali ke sumber (menghilangkan redundansi data dan menolak untuk menyimpan agregat), dan mengimplementasikan semua algoritma bisnis dalam bentuk fungsi yang diterapkan langsung ke aliran dokumen utama, kita akan mendapatkan DBMS fungsional dan ERP fungsional yang dibangun di atasnya. Masalah kinerja diselesaikan berkat memoisasi , dan jumlah kode fungsional akan cukup sepadan dengan jumlah SQL deklaratif, dan tidak lebih sulit untuk dipahami. Pada artikel ini, kami akan menunjukkan pendekatan dengan mengembangkan file DBMS paling sederhana dalam TypeScript dan Deno runtime (analog Node.js), serta menguji kinerja konvolusi menggunakan tugas-tugas bisnis yang khas sebagai contoh.

Mengapa ini relevan?


1) Keadaan + redundansi data yang dapat berubah buruk, terutama bila perlu untuk memastikan sinkronisasi yang konstan dengan aliran dokumen. Ini adalah sumber perbedaan potensial kredensial (keseimbangan tidak menyatu) dan sulit untuk mendeteksi efek samping.

2) Skema relasional yang kaku untuk menyimpan sumber dan data antara mahal di Big Data, sistem heterogen, dan dalam kondisi perubahan yang cepat - yaitu, bahkan di mana-mana. Kami menyarankan untuk menyimpan dokumen dalam bentuk aslinya, disortir berdasarkan waktu, memungkinkan koneksi "dari yang baru ke yang lama" dan tidak pernah sebaliknya. Ini akan memungkinkan sebagian besar agregat dihitung dengan algoritma single-pass langsung dari dokumen, dan semua tabel lainnya tidak diperlukan .

3) SQL sudah usang, karena mengasumsikan ketersediaan data setiap saat, dan dalam sistem terdistribusi ini jelas tidak terjadi - ketika mengembangkan algoritma Big Data, Anda harus siap untuk beberapa data yang diperlukan untuk muncul kemudian, dan beberapa sudah muncul sebelumnya. Ini membutuhkan sedikit pemikiran ulang dari bahasa query, dan perhatian yang sadar untuk caching.

4) PL modern memungkinkan Anda untuk membuat sistem responsif yang beroperasi dengan jutaan catatan secara lokal di laptop, di mana RDBMS tidak diinstal. Dan jika kita berbicara tentang server - skema yang diusulkan memiliki lebih banyak kemungkinan untuk paralelisme, termasuk pada kluster SPARK.

Latar belakang


Setelah bekerja cukup lama dengan berbagai perangkat lunak bisnis (akunting, perencanaan, WMS), saya menghadapi dua masalah hampir di mana-mana: kesulitan membuat perubahan pada skema data, dan seringnya penurunan produktivitas ketika perubahan ini dilakukan. Secara umum, sistem ini memiliki struktur yang kompleks, karena persyaratan yang saling bertentangan dikenakan pada mereka:

1) Auditabilitas. Penting untuk menyimpan semua dokumen utama tidak berubah. Pembagian menjadi direktori dan operasi sangat kondisional, dalam sistem dewasa, direktori terbatas pada versi, di mana setiap perubahan dilakukan oleh dokumen khusus. Dengan demikian, dokumen sumber adalah bagian yang tidak dapat diubah dari sistem, dan itu adalah satu-satunya "sumber kebenaran", dan semua data lainnya dapat dipulihkan darinya.

2) Kinerja permintaan. Misalnya, ketika membuat jalur pesanan penjualan, sistem harus menghitung harga produk, dengan mempertimbangkan diskon akun, untuk itu perlu mengekstraksi status klien, saldo saat ini, riwayat pembelian, saham saat ini di wilayah tersebut, dll. Secara alami, semua informasi yang diperlukan tidak dapat dihitung "on the fly", tetapi harus tersedia dalam bentuk setengah jadi. Oleh karena itu, sistem yang ada menyimpan abstraksi yang nyaman di atas baris dokumen (posting), serta agregat yang telah dihitung sebelumnya (register akumulasi, irisan waktu, saldo saat ini, posting ringkasan, dll.). Volume mereka hingga 80% dari ukuran basis data, struktur tabel kaku diperbaiki, dengan perubahan apa pun pada algoritme - pemrogram harus menangani pembaruan agregat yang benar. Bahkan, agregat ini adalah keadaan yang bisa berubah dari sistem.

3) Kinerja transaksional. Saat memegang dokumen apa pun, Anda harus menghitung ulang semua agregat, dan ini biasanya operasi pemblokiran. Oleh karena itu, algoritma pembaruan agregat adalah titik paling menyakitkan dalam sistem, dan ketika sejumlah besar perubahan dilakukan, ada risiko signifikan terhadap sesuatu yang melanggar, dan kemudian data akan "terkorosi", yaitu, agregat tidak akan lagi sesuai dengan dokumen. Situasi ini menjadi momok bagi semua proyek implementasi dan dukungan selanjutnya.

Kami menetapkan dasar-dasar arsitektur baru


1) Penyimpanan. Basisdata adalah jurnal dokumen yang disusun secara kronologis yang mencerminkan fait accompli dari dunia nyata. Direktori juga dokumen, hanya kerja jangka panjang. Baik dokumen dan setiap versi dari entri direktori tidak dapat diubah. Tidak ada data lain dalam bentuk postingan / register / saldo yang disimpan dalam sistem (pernyataan provokatif yang kuat, itu terjadi secara berbeda dalam kehidupan, tetapi Anda perlu berusaha untuk kesempurnaan ). Dokumen memiliki sejumlah atribut sistem:

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


All Articles