L'idea principale per la stesura dell'analisi è stata un articolo . Sembrava interessante anche un articolo pubblicato 4 anni fa — .
Mi è sembrato interessante che lo stesso pensiero-"implementare la logica di business nel database".

non sia venuto solo a me.
Inoltre, vorrei conservare, per me innanzitutto, idee interessanti emerse durante l'implementazione. Soprattutto considerando che relativamente recentemente è stata presa una decisione strategica di cambiare architettura e trasferire la logica di business a livello backend. Quindi, tutto ciò che è stato sviluppato presto non servirà più a nessuno e non interesserà più a nessuno.
I metodi descritti non rappresentano una scoperta o qualcosa di eccezionale know how, tutto secondo la tradizione e già implementato più volte (ad esempio, io ho applicato un approccio simile 20 anni fa su Oracle). Ho semplicemente deciso di raccogliere tutto in un unico posto. Chissà, potrebbe servire a qualcuno. Come ha dimostrato la pratica — piuttosto spesso la stessa idea viene in mente a persone diverse indipendentemente. E tenerla a mente è utile.
Naturalmente, nulla è perfetto in questo mondo, errori e refusi sono purtroppo possibili. Critiche e commenti sono sempre benvenuti e attesi. E un altro piccolo dettaglio — i dettagli specifici dell'implementazione sono stati omessi. Del resto, tutto è in uso in un progetto attualmente funzionante. Quindi, l'articolo è un'analisi e una descrizione di un concetto generale, non di più. Spero che per capire il quadro generale, i dettagli siano sufficienti.
L'idea generale è — «dividi e conquista, nascondi e possiedi»
L'idea è classica — uno schema separato per le tabelle, uno schema separato per le funzioni memorizzate.
Il cliente non ha accesso diretto ai dati. Tutto ciò che il cliente può fare è solo chiamare una funzione memorizzata e elaborare la risposta ricevuta.
Ruoli
CREATE ROLE store;
CREATE ROLE sys_functions;
CREATE ROLE loc_audit_functions;
CREATE ROLE service_functions;
CREATE ROLE business_functions;
Schemi
Schema di archiviazione delle tabelle
Tabelle di destinazione che implementano entità tematiche.
CREATE SCHEMA store AUTHORIZATION store ;
Schema delle funzioni di sistema
Funzioni di sistema, in particolare per il logging delle modifiche delle tabelle.
CREATE SCHEMA sys_functions AUTHORIZATION sys_functions ;
Schema di audit locale
Funzioni e tabelle per l'audit locale dell'esecuzione di funzioni memorizzate e della modifica delle tabelle di destinazione.
CREATE SCHEMA loc_audit_functions AUTHORIZATION loc_audit_functions;
Schema delle funzioni di servizio
Funzioni per funzioni di servizio e DML.
CREATE SCHEMA service_functions AUTHORIZATION service_functions;
Schema delle funzioni di business
Funzioni per le funzioni di business finali invocate dal cliente.
CREATE SCHEMA business_functions AUTHORIZATION business_functions;
Diritti di accesso
Ruolo — DBA ha accesso completo a tutti gli schemi (distinto dal ruolo di 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;
Ruolo — UTENTE ha il privilegio EXECUTE nello schema business_functions.
CREATE ROLE user_role;
Privilegi tra schemi
GRANT
Poiché tutte le funzioni vengono create con l'attributo SECURITY DEFINER è necessaria l'istruzione 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 ;
Quindi, lo schema del DB è pronto. Si può iniziare a popolare con i dati.
Tabelle di destinazione
La creazione delle tabelle è banale. Nessuna caratteristica particolare, tranne per il fatto che è stato deciso di rinunciare all'uso di SERIAL e generare sequenze esplicitamente. Inoltre, ovviamente, utilizzo massimo dell'istruzione
COMMENT ON ...Commenti per tutti oggetti, senza eccezioni.
Audit locale
Per tenere un registro dell'esecuzione di funzioni memorizzate e della modifica delle tabelle di destinazione, viene utilizzata una tabella di audit locale, che include anche i dettagli della connessione del cliente, l'etichetta del modulo invocato e i valori effettivi dei parametri di input e output in formato JSON.
Funzioni di sistema
Destinate alla registrazione delle modifiche nelle tabelle di destinazione. Si tratta di funzioni di attivazione.
Modello — funzione di sistema
---------------------------------------------------------
-- INSERISCI
CREATE OR REPLACE FUNCTION sys_functions.table_insert_log ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM loc_audit_functions.make_log( ' '||'table' , 'inserire' , 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();
---------------------------------------------------------
-- AGGIORNA
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' , 'aggiornare' , 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 ();
---------------------------------------------------------
-- ELIMINA
CREATE OR REPLACE FUNCTION sys_functions.table_delete_log ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM loc_audit_functions.make_log( ' '||'table' , 'eliminare' , 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 ();Funzioni di servizio
Destinato all'implementazione di operazioni di servizio e DML sulle tabelle di destinazione.
Modello - funzione di servizio
--INSERISCI
--RESTITUISCI l'id DEL NUOVO RIGA
CREATE OR REPLACE FUNCTION service_functions.table_insert ( new_column store.table.column%TYPE )
RETURNS integer AS $$
DECLARE
new_id integer ;
BEGIN
-- Genera nuovo id
new_id = nextval('store.table.seq');
-- Inserisci nella tabella
INSERT INTO store.table
(
id ,
column
)
VALUES
(
new_id ,
new_column
);
RETURN new_id ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;
--ELIMINA
--RESTITUISCI IL NUMERO DELLE RIGHE ELIMINATE
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;
-- DETTAGLI AGGIORNAMENTO
-- RESTITUISCI IL NUMERO DELLE RIGHE AGGIORNATE
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;Funzioni di business
Destinato alle funzioni aziendali finali chiamate dal cliente. Restituiscono sempre — JSON. Per intercettare e registrare gli errori di esecuzione, si utilizza il blocco ECCEZIONE.
Modello - funzione di business
CREA O SOSTITUISCI LA FUNZIONE business_functions.business_function_template(
--Parametri di input
)
RESTITUISCE JSON AS $$
DICHIARARE
------------------------
--per la gestione delle eccezioni
error_message text ;
error_json json ;
result json ;
------------------------
INIZIO
--LOGGING
PERFORM loc_audit_functions.make_log
(
'business_function_template',
'AVVIATO',
json_build_object
(
--Parametri di input
)
);
PERFORM business_functions.notice('business_function_template');
--INIZIO PARTE AZIENDALE
--FINE PARTE AZIENDALE
-- RISULTATO SUCCESSOSO
PERFORM business_functions.notice('result');
PERFORM business_functions.notice(result);
PERFORM loc_audit_functions.make_log
(
'business_function_template',
'FINITO',
json_build_object( 'result',result )
);
RESTITUISCI result ;
----------------------------------------------------------------------------------------------------------
-- GESTIONE DELLE ECCEZIONI
ECCEZIONE
QUANDO ALTRO ALLORA
PERFORM loc_audit_functions.make_log
(
'business_function_template',
'AVVIATO',
json_build_object
(
--Parametri di input
) , TRUE );
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERRORE',
json_build_object('SQLSTATE',SQLSTATE ), TRUE
);
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERRORE',
json_build_object('SQLERRM',SQLERRM ), TRUE
);
GET STACKED DIAGNOSTICS error_message = RETURNED_SQLSTATE ;
PERFORM loc_audit_functions.make_log
(
'business_function_template',
' ERRORE-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',
' ERRORE-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',
' ERRORE-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',
' ERRORE-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',
' ERRORE-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',
' ERRORE-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',
' ERRORE-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',
' ERRORE-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',
' ERRORE-PG_EXCEPTION_CONTEXT',json_build_object('PG_EXCEPTION_CONTEXT',error_message ), TRUE );
ALZA AVVISO 'ALLARME: %' , SQLERRM ;
SELECT json_build_object
(
'isError' , TRUE ,
'errorMsg' , SQLERRM
) INTO error_json ;
RESTITUISCI error_json ;
FINE
$$ LINGUAGGIO plpgsql SECURITY DEFINER;Risultato
Per descrivere il quadro generale, penso sia più che sufficiente. Se qualcuno è interessato ai dettagli e ai risultati, scrivete nei commenti, sarò lieto di arricchire il quadro con ulteriori dettagli.
P.S.
Il logging di un errore semplice — tipo di parametro di input
-[ RECORD 1 ]-
date_trunc | 2020-08-19 13:15:46
id | 1072
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | INIZIATO
jsonb_pretty | {
| "dko": {
| "id": 4,
| "type": "Tipo1",
| "title": "CREATO DA addKD",
| "Weight": 10,
| "Tr": "300",
| "reduction": 10,
| "isTrud": "VERO",
| "description": "descrizione",
| "lowerTr": "100",
| "measurement": "misura1",
| "methodology": "m1",
| "passportUrl": "file",
| "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 | ERRORE
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 | ERRORE
jsonb_pretty | {
| "SQLERRM": "sintassi di input non valida per tipo numerico: "null""
| }
-[ RECORD 4 ]-
date_trunc | 2020-08-19 13:15:46
id | 1075
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | ERRORE-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 | ERRORE-NOME_COLONNA
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 | ERRORE-NOME vincolato
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 | ERRORE-NOME_TIPO_DATI_PG
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 | ERRORE-TEXT_MESSAGE
jsonb_pretty | {
| "MESSAGE_TEXT": "sintassi di input non valida per tipo numerico: "null""
| }
-[ RECORD 9 ]-
date_trunc | 2020-08-19 13:15:46
id | 1080
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | ERRORE-NOME_SCHEMA
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 | ERRORE-DETTAGLIO ECCEZIONE PG
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 | ERRORE-SUGGERIMENTO ECCEZIONE PG
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 | ERRORE-CONTEXT ECCEZIONE PG
jsonb_pretty | {
usename | emp1
log_module | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status | ERRORE-TEXT_MESSAGE
jsonb_pretty | {
| "MESSAGE_TEXT": "sintassi di input non valida per tipo numerico: "null""
| }Fonte: habr.com
