Motivul pentru a scrie acest studiu a fost un articol . De asemenea, a părut interesant un articol publicat acum 4 ani — .
A părut interesant că aceeași idee a venit în minte nu doar mie.să implementeze logica de afaceri în baza de date".

s-a născut nu doar în mintea mea.
De asemenea, pentru viitor, am vrut să păstrez, în special pentru mine, realizările interesante apărute în timpul implementării. Având în vedere că recent a fost luată o decizie strategică de a schimba arhitectura și de a muta logica de afaceri la nivelul backend. Așa că tot ce a fost realizat, în curând nu va mai fi necesar și nu va mai interesa pe nimeni.
Metodele descrise nu sunt o descoperire sau ceva excepțional know how, totul conform tradiției și a fost implementat de nenumărate ori (eu, de exemplu, am aplicat o abordare similară acum 20 de ani pe Oracle). Pur și simplu am decis să adun totul într-un singur loc. Poate că va fi de folos cuiva. Așa cum a arătat practica — destul de des aceeași idee vine în mintea diferitelor persoane. Și este util să păstrez pentru mine, ca amintire.
Desigur, nu există nimic perfect în această lume, erori și tipărituri greșite sunt, din păcate, posibile. Critica și observațiile sunt binevenite și așteptate. Și un mic detaliu — detaliile concrete ale implementării sunt omise. Totul este folosit în prezent în proiectul real de lucru. Așa că articolul este un studiu și o descriere a conceptului general, nu mai mult. Sper că pentru a înțelege imaginea de ansamblu, detaliile sunt suficiente.
Ideea generală — „împarte și conduc”
Ideea este clasică — un schelet separat pentru tabele, un schelet separat pentru funcțiile stocate.
Clientul nu are acces direct la date. Tot ceea ce clientul poate face este să invoce o funcție stocată și să proceseze răspunsul primit.
Roluri
CREATE ROLE store;
CREATE ROLE sys_functions;
CREATE ROLE loc_audit_functions;
CREATE ROLE service_functions;
CREATE ROLE business_functions;
Schemes
Schema de stocare a tabelelor
Tabelele țintă care implementează entitățile de subiect.
CREATE SCHEMA store AUTHORIZATION store ;
Schema funcțiilor de sistem
Funcțiile de sistem, în special pentru logarea modificărilor tabelului.
CREATE SCHEMA sys_functions AUTHORIZATION sys_functions ;
Schema de audit local
Funcții și tabele pentru implementarea auditului local al executării funcțiilor stocate și modificărilor în tabelele țintă.
CREATE SCHEMA loc_audit_functions AUTHORIZATION loc_audit_functions;
Schema funcțiilor de serviciu
Funcții pentru funcțiile de serviciu și DML.
CREATE SCHEMA service_functions AUTHORIZATION service_functions;
Schema funcțiilor de afaceri
Funcții pentru funcțiile de afaceri finale invocate de client.
CREATE SCHEMA business_functions AUTHORIZATION business_functions;
Permisiuni
Rolul — DBA are acces complet la toate schemările (separat de rolul DB Owner).
CREATE ROLE dba_role;
GRANT store TO dba_role;
GRANT sys_functions TO dba_role;
GRANT loc_audit_functions TO dba_role;
GRANT service_functions TO dba_role;
GRANT business_functions TO dba_role;
Rolul — USER are privilegiul EXECUTE în schema business_functions.
CREATE ROLE user_role;
Privilegiile între scheme
GRANT
Deoarece toate funcțiile sunt create cu atributul SECURITY DEFINER se necesită instrucțiunea REVOKE EXECUTE ON ALL FUNCTION… FROM public;
REVOKE EXECUTE ON ALL FUNCTION IN SCHEMA sys_functions FROM public ;
REVOKE EXECUTE ON ALL FUNCTION IN SCHEMA loc_audit_functions FROM public ;
REVOKE EXECUTE ON ALL FUNCTION IN SCHEMA service_functions FROM public ;
REVOKE EXECUTE ON ALL FUNCTION IN SCHEMA business_functions FROM public ;
GRANT USAGE ON SCHEMA sys_functions TO dba_role ;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA sys_functions TO dba_role ;
GRANT USAGE ON SCHEMA loc_audit_functions TO dba_role ;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA loc_audit_functions TO dba_role ;
GRANT USAGE ON SCHEMA service_functions TO dba_role ;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA service_functions TO dba_role ;
GRANT USAGE ON SCHEMA business_functions TO dba_role ;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA business_functions TO dba_role ;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA business_functions TO user_role ;
GRANT ALL PRIVILEGES ON SCHEMA store TO GROUP business_functions ;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA store TO business_functions ;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA store TO business_functions ;
Așadar, schema DB — este gata. Se pot începe procesarea datelor.
Tabelele țintă
Crearea tabelelor este trivială. Nu sunt particularități, cu excepția că s-a decis abandonarea utilizării SERIAL și generarea secvențelor în mod explicit. Plus, desigur, utilizarea maximă a instrucțiunii
COMMENT ON ...Comentarii pentru tuturor obiecte, fără excepții.
Audit local
Pentru a păstra un jurnal al executării funcțiilor stocate și modificărilor tabelelor țintă, se folosește o tabelă de audit local, care include detalii despre conexiunea clientului, eticheta modulului apelat, valorile reale ale parametrilor de intrare și ieșire sub formă de JSON.
Funcții sistemice
Destinate înregistrării modificărilor în tabelele țintă. Reprezintă funcții de declanșare.
Șablon — funcție sistematică
---------------------------------------------------------
-- INSERARE
CREATE OR REPLACE FUNCTION sys_functions.table_insert_log ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM loc_audit_functions.make_log( ' '||'table' , 'insert' , json_build_object('id', NEW.id) );
RETURN NULL ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER table_after_insert AFTER INSERT ON storage.table FOR EACH ROW EXECUTE PROCEDURE sys_functions.table_insert_log();
---------------------------------------------------------
-- ACTUALIZARE
CREATE OR REPLACE FUNCTION sys_functions.table_update_log ()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.column != NEW.column
THEN
PERFORM loc_audit_functions.make_log( ' '||'table' , 'update' , json_build_object('OLD.column', OLD.column , 'NEW.column' , NEW.column ) );
END IF ;
RETURN NULL ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER table_after_update AFTER UPDATE ON storage.table FOR EACH ROW EXECUTE PROCEDURE sys_functions.table_update_log ();
---------------------------------------------------------
-- ȘTERGERE
CREATE OR REPLACE FUNCTION sys_functions.table_delete_log ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM loc_audit_functions.make_log( ' '||'table' , 'delete' , json_build_object('id', OLD.id ) );
RETURN NULL ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER table_after_delete AFTER DELETE ON storage.table FOR EACH ROW EXECUTE PROCEDURE sys_functions.table_delete_log ();Funcții de serviciu
Destinate implementării operațiunilor de serviciu și DML pe tabelele țintă.
Șablon — funcție de serviciu
--INSERARE
--REPORTEAZĂ id NOULUI RÂND
CREATE OR REPLACE FUNCTION service_functions.table_insert ( new_column store.table.column%TYPE )
RETURNS integer AS $$
DECLARE
new_id integer ;
BEGIN
-- Generare nou id
new_id = nextval('store.table.seq');
-- Inserare în tabel
INSERT INTO store.table
(
id ,
column
)
VALUES
(
new_id ,
new_column
);
RETURN new_id ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;
--ȘTERGERE
--REPORTEAZĂ NUMĂRUL RÂNDURILOR ȘTERSE
CREATE OR REPLACE FUNCTION service_functions.table_delete ( current_id integer )
RETURNS integer AS $$
DECLARE
rows_count integer ;
BEGIN
DELETE FROM store.table WHERE id = current_id;
GET DIAGNOSTICS rows_count = ROW_COUNT;
RETURN rows_count ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- ACTUALIZARE DETALII
-- REPORTEAZĂ NUMĂRUL RÂNDURILOR ACTUALIZATE
CREATE OR REPLACE FUNCTION service_functions.table_update_column
(
current_id integer
,new_column store.table.column%TYPE
)
RETURNS integer AS $$
DECLARE
rows_count integer ;
BEGIN
UPDATE store.table
SET
column = new_column
WHERE id = current_id;
GET DIAGNOSTICS rows_count = ROW_COUNT;
RETURN rows_count ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;Funcții de afaceri
Destinate funcțiilor finale de afaceri apelate de client. Întotdeauna returnează — JSON. Pentru interceptarea și logarea erorilor de execuție, se utilizează blocul EXCEPȚIE.
Șablon — funcție de afaceri
CREATE OR REPLACE FUNCTION business_functions.business_function_template(
--Parametrii de intrare
)
RETURNS JSON AS $$
DECLARE
------------------------
--pentru captarea excepțiilor
error_message text ;
error_json json ;
result json ;
------------------------
BEGIN
--LOGGING
PERFORM loc_audit_functions.make_log
(
'business_function_template',
'STARTED',
json_build_object
(
--IN Parametrii
)
);
PERFORM business_functions.notice('business_function_template');
--ÎNCEPE PARTEA DE AFACERI
--SFÂRȘIT PARTEA DE AFACERI
-- REZULTATUL CU SUCCES
PERFORM business_functions.notice('result');
PERFORM business_functions.notice(result);
PERFORM loc_audit_functions.make_log
(
'business_function_template',
'FINISHED',
json_build_object( 'result',result )
);
RETURN result ;
----------------------------------------------------------------------------------------------------------
-- CAPTAREA EXCEPȚIILOR
EXCEPTION
WHEN OTHERS THEN
PERFORM loc_audit_functions.make_log
(
'business_function_template',
'STARTED',
json_build_object
(
--IN Parametrii
) , TRUE );
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR',
json_build_object('SQLSTATE',SQLSTATE ), TRUE
);
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR',
json_build_object('SQLERRM',SQLERRM ), TRUE
);
GET STACKED DIAGNOSTICS error_message = RETURNED_SQLSTATE ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR-RETURNED_SQLSTATE',json_build_object('RETURNED_SQLSTATE',error_message ), TRUE );
GET STACKED DIAGNOSTICS error_message = COLUMN_NAME ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR-COLUMN_NAME',
json_build_object('COLUMN_NAME',error_message ), TRUE );
GET STACKED DIAGNOSTICS error_message = CONSTRAINT_NAME ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR-CONSTRAINT_NAME',
json_build_object('CONSTRAINT_NAME',error_message ), TRUE );
GET STACKED DIAGNOSTICS error_message = PG_DATATYPE_NAME ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR-PG_DATATYPE_NAME',
json_build_object('PG_DATATYPE_NAME',error_message ), TRUE );
GET STACKED DIAGNOSTICS error_message = MESSAGE_TEXT ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR-MESSAGE_TEXT',json_build_object('MESSAGE_TEXT',error_message ), TRUE );
GET STACKED DIAGNOSTICS error_message = SCHEMA_NAME ;
PERFORM loc_audit_functions.make_log
(s
'business_function_template',
' ERROR-SCHEMA_NAME',json_build_object('SCHEMA_NAME',error_message ), TRUE );
GET STACKED DIAGNOSTICS error_message = PG_EXCEPTION_DETAIL ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR-PG_EXCEPTION_DETAIL',
json_build_object('PG_EXCEPTION_DETAIL',error_message ), TRUE );
GET STACKED DIAGNOSTICS error_message = PG_EXCEPTION_HINT ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR-PG_EXCEPTION_HINT',json_build_object('PG_EXCEPTION_HINT',error_message ), TRUE );
GET STACKED DIAGNOSTICS error_message = PG_EXCEPTION_CONTEXT ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERROR-PG_EXCEPTION_CONTEXT',json_build_object('PG_EXCEPTION_CONTEXT',error_message ), TRUE );
RAISE WARNING 'ALARM: %' , SQLERRM ;
SELECT json_build_object
(
'isError' , TRUE ,
'errorMsg' , SQLERRM
) INTO error_json ;
RETURN error_json ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;Rezultatul
Pentru a descrie imaginea generală, cred că este suficient. Dacă pe cineva i-au interesat detaliile și rezultatele, lăsați comentarii, voi completa cu plăcere imaginea cu detalii suplimentare.
P.S.
Logarea unei erori simple — tipul parametrului de intrare
-[ RECORD 1 ]-
date_trunc | 2020-08-19 13:15:46
id | 1072
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | ÎNCEPUT
jsonb_pretty | {
| "dko": {
| "id": 4,
| "type": "Type1",
| "title": "CREAT DE addKD",
| "Weight": 10,
| "Tr": "300",
| "reduction": 10,
| "isTrud": "DA",
| "description": "descriere",
| "lowerTr": "100",
| "measurement": "măsurare1",
| "methodology": "m1",
| "passportUrl": "files",
| "upperTr": "200",
| "weightingFactor": 100.123,
| "actualTrValue": null,
| "upperTrCalcNumber": "120"
| },
| "CardId": 3
| }
-[ RECORD 2 ]-
date_trunc | 2020-08-19 13:15:46
id | 1073
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE
jsonb_pretty | {
| "SQLSTATE": "22P02"
| }
-[ RECORD 3 ]-
date_trunc | 2020-08-19 13:15:46
id | 1074
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE
jsonb_pretty | {
| "SQLERRM": "sintaxă de intrare invalidă pentru tipul numeric: "null""
| }
-[ RECORD 4 ]-
date_trunc | 2020-08-19 13:15:46
id | 1075
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-RETURNED_SQLSTATE
jsonb_pretty | {
| "RETURNED_SQLSTATE": "22P02"
| }
-[ RECORD 5 ]-
date_trunc | 2020-08-19 13:15:46
id | 1076
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-COLUMN_NAME
jsonb_pretty | {
| "COLUMN_NAME": ""
| }
-[ RECORD 6 ]-
date_trunc | 2020-08-19 13:15:46
id | 1077
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-CONSTRAINT_NAME
jsonb_pretty | {
| "CONSTRAINT_NAME": ""
| }
-[ RECORD 7 ]-
date_trunc | 2020-08-19 13:15:46
id | 1078
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-PG_DATATYPE_NAME
jsonb_pretty | {
| "PG_DATATYPE_NAME": ""
| }
-[ RECORD 8 ]-
date_trunc | 2020-08-19 13:15:46
id | 1079
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-MESSAGE_TEXT
jsonb_pretty | {
| "MESSAGE_TEXT": "sintaxă de intrare invalidă pentru tipul numeric: "null""
| }
-[ RECORD 9 ]-
date_trunc | 2020-08-19 13:15:46
id | 1080
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-SCHEMA_NAME
jsonb_pretty | {
| "SCHEMA_NAME": ""
| }
-[ RECORD 10 ]-
date_trunc | 2020-08-19 13:15:46
id | 1081
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-PG_EXCEPTION_DETAIL
jsonb_pretty | {
| "PG_EXCEPTION_DETAIL": ""
| }
-[ RECORD 11 ]-
date_trunc | 2020-08-19 13:15:46
id | 1082
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-PG_EXCEPTION_HINT
jsonb_pretty | {
| "PG_EXCEPTION_HINT": ""
| }
-[ RECORD 12 ]-
date_trunc | 2020-08-19 13:15:46
id | 1083
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-PG_EXCEPTION_CONTEXT
jsonb_pretty | {
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | EROARE-MESSAGE_TEXT
jsonb_pretty | {
| "MESSAGE_TEXT": "sintaxă de intrare invalidă pentru tipul numeric: "null""
| }Sursa: habr.com
