Studio sulla realizzazione della logica di business a livello di funzioni memorizzate in PostgreSQL

La motivazione principale per scrivere questo studio è stata un articolo «Durante il lockdown il carico è aumentato di 5 volte, ma eravamo pronti». Come Lingualeo è passato a PostgreSQL con 23 milioni di utenti. Ho trovato interessante anche un articolo pubblicato 4 anni fa — Realizzazione della logica di business in MySQL.

È interessante notare che lo stesso concetto-"realizzare la logica di business nel DB".

Studio sulla realizzazione della logica di business a livello di funzioni memorizzate in PostgreSQL

è venuto in mente non solo a me.

Inoltre, vorrei conservare per il futuro, prima di tutto per me stesso, idee interessanti emerse durante l'implementazione. Soprattutto considerando che è stata recentemente presa una decisione strategica di cambiare l'architettura e spostare la logica di business a livello backend. Quindi, tutto ciò che è stato sviluppato presto non sarà più necessario e non interesserà a nessuno.

I metodi descritti non sono una scoperta o un qualcosa di esclusivo know how, tutto secondo tradizione e già realizzato molte volte (io, ad esempio, ho applicato un approccio simile 20 anni fa su Oracle). Ho deciso di raccogliere tutto in un unico posto. Potrebbe essere utile a qualcuno. Come ha dimostrato la pratica, spesso la stessa idea viene in mente a persone diverse in modo indipendente. È anche utile per ricordare.

Certo, però nulla in questo mondo è perfetto, errori e refusi possono purtroppo verificarsi. Critiche e osservazioni sono sempre benvenute e attese. E un ultimo piccolo dettaglio: i dettagli specifici dell'implementazione sono omessi. Dopotutto, tutto viene utilizzato in un progetto funzionante. Quindi, l'articolo è solo un bozza e una descrizione del concetto generale, nient'altro. Spero che i dettagli siano sufficienti per comprendere il quadro generale.

L'idea generale è - «dividi e conquista, nascondi e domina»

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 è 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 target che implementano entità tematiche.

CREA SCHEMA store AUTORIZZAZIONE store ;

Schema delle funzioni di sistema

Funzioni di sistema, in particolare per il logging delle modifiche delle tabelle.

CREA SCHEMA sys_functions AUTORIZZAZIONE sys_functions ;

Schema di audit locale

Funzioni e tabelle per l'implementazione dell'audit locale delle esecuzioni delle funzioni memorizzate e delle modifiche delle tabelle target.

CREA SCHEMA loc_audit_functions AUTORIZZAZIONE loc_audit_functions;

Schema delle funzioni di servizio

Funzioni per funzioni di servizio e DML.

CREA SCHEMA service_functions AUTORIZZAZIONE service_functions;

Schema delle funzioni aziendali

Funzioni per le funzioni aziendali finali richiamate dal cliente.

CREA SCHEMA business_functions AUTORIZZAZIONE business_functions;

Diritti di accesso

Ruolo — DBA ha accesso completo a tutti gli schemi (separato dal ruolo DB Owner).

CREA RUOLO 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 ESEGUI nello schema business_functions.

CREA RUOLO user_role;

Privilegi tra schemi

GRANT
Poiché tutte le funzioni sono create con l'attributo SECURITY DEFINER è necessaria l'istruzione REVOCATE EXECUTE ON ALL FUNCTION… FROM public;

REVOCA ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA sys_functions DA public ; 
REVOCA ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA loc_audit_functions DA public ; 
REVOCA ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA service_functions DA public ; 
REVOCA ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA business_functions DA public ; 

CONCEDI USO SULLO SCHEMA sys_functions A dba_role ; 
CONCEDI ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA sys_functions A dba_role ;
CONCEDI USO SULLO SCHEMA loc_audit_functions A dba_role ; 
CONCEDI ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA loc_audit_functions A dba_role ;
CONCEDI USO SULLO SCHEMA service_functions A dba_role ; 
CONCEDI ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA service_functions A dba_role ;
CONCEDI USO SULLO SCHEMA business_functions A dba_role ; 
CONCEDI ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA business_functions A dba_role ;
CONCEDI ESECUZIONE SU TUTTE LE FUNZIONI NELLO SCHEMA business_functions A user_role ;

CONCEDI TUTTI I PRIVILEGI SULLO SCHEMA store AL GRUPPO business_functions ;
CONCEDI TUTTI I PRIVILEGI SU TUTTE LE TABELLE NELLO SCHEMA store A business_functions ;
CONCEDI USO SU TUTTE LE SEQUENZE NELLO SCHEMA store A business_functions ;

Quindi lo schema del database è pronto. Possiamo iniziare a popolarlo con i dati.

Tabelle di destinazione

La creazione delle tabelle è banale. Nessuna particolarità, tranne che è stato deciso di rinunciare all'uso di SERIAL e generare sequenze esplicitamente. Inoltre, naturalmente, il massimo utilizzo dell'istruzione

COMMENTA SU ...

Commenti per tutti gli oggetti, senza eccezioni.

Audit locale

Per registrare l'esecuzione delle funzioni memorizzate e le modifiche alle tabelle di destinazione, viene utilizzata una tabella di audit locale, che include dettagli sulla connessione del cliente, il nome del modulo chiamato e i valori effettivi dei parametri di input e output in formato JSON.

Funzioni di sistema

Destinate a registrare le modifiche alle tabelle di destinazione. Sono funzioni trigger.

Modello - funzione di sistema

---------------------------------------------------------
-- INSERIRE
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();

---------------------------------------------------------
-- AGGIORNARE
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 ();

---------------------------------------------------------
-- CANCELLARE
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 ();

Funzioni di servizio

Destinate all'implementazione di operazioni di servizio e DML sulle tabelle di destinazione.

Modello — funzione di servizio

--INSERISCI
--RESTITUISCI ID DELLA NUOVA RIGA
CREA O SOSTITUISCI LA FUNZIONE service_functions.table_insert ( new_column store.table.column%TYPE )
RESTITUISCE intero AS $$
DICHIARARE
  new_id intero ;
INIZIO
  -- Genera nuovo id
  new_id = nextval('store.table.seq');

  -- Inserisci nella tabella
  INSERISCI IN store.table
  ( 
    id ,
    column
   )
  VALORI
  (
   new_id ,
   new_column
   );

RESTITUISCI new_id ;
FINE
$$ LINGUAGGIO plpgsql SICUREZZA DEFINER;

--ELIMINA
--RESTITUISCI NUMERI DELLE RIGHE ELIMINATE
CREA O SOSTITUISCI LA FUNZIONE service_functions.table_delete ( current_id intero ) 
RESTITUISCE intero AS $$
DICHIARARE
  rows_count intero  ;    
INIZIO
  ELIMINA DA store.table DOVE id = current_id; 

  OTTIENI DIAGNOSTICA rows_count = ROW_COUNT;                                                                           

  RESTITUISCI rows_count ;
FINE
$$ LINGUAGGIO plpgsql SICUREZZA DEFINER;
 
-- AGGIORNA DETTAGLI
-- RESTITUISCI NUMERI DELLE RIGHE AGGIORNATE
CREA O SOSTITUISCI LA FUNZIONE service_functions.table_update_column 
(
  current_id intero 
  ,new_column store.table.column%TYPE
) 
RESTITUISCE intero AS $$
DICHIARARE
  rows_count intero  ; 
INIZIO
  AGGIORNA  store.table
  IMPOSTA
    column = new_column
  DOVE id = current_id;

  OTTIENI DIAGNOSTICA rows_count = ROW_COUNT;                                                                           

  RESTITUISCI rows_count ;
FINE
$$ LINGUAGGIO plpgsql SICUREZZA DEFINER;

Funzioni aziendali

Progettate per funzioni aziendali finali invocabili dal cliente. Restituiscono sempre — JSON. Per intercettare e registrare gli errori di esecuzione, si utilizza il blocco ECCEZIONE.

Template — funzione aziendale

CREA O SOSTITUISCI LA FUNZIONE business_functions.business_function_template(
--Parametri di input
)
RESTITUISCE JSON AS $$
DICHIARARE
  ------------------------
  --per la cattura delle eccezioni
  error_message text;
  error_json json;
  result json;
  ------------------------ 
INIZIO
--REGISTRAZIONE
  ESEGUI loc_audit_functions.make_log
  (
    'business_function_template',
    'AVVIATO',
    json_build_object
    (
	--Parametri di input
    ) 
   );

  ESEGUI business_functions.notice('business_function_template');            

  --INIZIO PARTE AZIENDALE
  --FINE PARTE AZIENDALE

  -- RISULTATO SUCCESSO
  ESEGUI business_functions.notice('result');
  ESEGUI business_functions.notice(result);

  ESEGUI loc_audit_functions.make_log
  (
    'business_function_template',
    'FINITO', 
    json_build_object( 'result',result )
  );

  RESTITUISCI result;
----------------------------------------------------------------------------------------------------------
-- CATTURA ECCEZIONI
ECCEZIONE                        
  QUANDO ALTRO ALLORA    
    ESEGUI loc_audit_functions.make_log
    (
      'business_function_template',
      'AVVIATO',
      json_build_object
      (
	--Parametri di input	
      ) , TRUE );

     ESEGUI loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERRORE',
       json_build_object('SQLSTATE',SQLSTATE ), TRUE 
     );

     ESEGUI loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERRORE',
       json_build_object('SQLERRM',SQLERRM  ), TRUE 
      );

     OTTIENI DIAGNOSTICA IMPILATA error_message = RETURNED_SQLSTATE ;
     ESEGUI loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERRORE-RETURNED_SQLSTATE',json_build_object('RETURNED_SQLSTATE',error_message  ), TRUE );

     OTTIENI DIAGNOSTICA IMPILATA error_message = COLUMN_NAME ;
     ESEGUI loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERRORE-COLUMN_NAME',
       json_build_object('COLUMN_NAME',error_message  ), TRUE );

     OTTIENI DIAGNOSTICA IMPILATA error_message = CONSTRAINT_NAME ;
     ESEGUI loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERRORE-CONSTRAINT_NAME',
      json_build_object('CONSTRAINT_NAME',error_message  ), TRUE );

     OTTIENI DIAGNOSTICA IMPILATA error_message = PG_DATATYPE_NAME ;
     ESEGUI loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERRORE-PG_DATATYPE_NAME',
       json_build_object('PG_DATATYPE_NAME',error_message  ), TRUE );

     OTTIENI DIAGNOSTICA IMPILATA error_message = MESSAGE_TEXT ;
     ESEGUI loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERRORE-MESSAGE_TEXT',json_build_object('MESSAGE_TEXT',error_message  ), TRUE );

     OTTIENI DIAGNOSTICA IMPILATA error_message = SCHEMA_NAME ;
     ESEGUI loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERRORE-SCHEMA_NAME',json_build_object('SCHEMA_NAME',error_message  ), TRUE );

     OTTIENI DIAGNOSTICA IMPILATA error_message = PG_EXCEPTION_DETAIL ;
     ESEGUI loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERRORE-PG_EXCEPTION_DETAIL',
      json_build_object('PG_EXCEPTION_DETAIL',error_message  ), TRUE );

     OTTIENI DIAGNOSTICA IMPILATA error_message = PG_EXCEPTION_HINT ;
     ESEGUI loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERRORE-PG_EXCEPTION_HINT',json_build_object('PG_EXCEPTION_HINT',error_message  ), TRUE );

     OTTIENI DIAGNOSTICA IMPILATA error_message = PG_EXCEPTION_CONTEXT ;
     ESEGUI loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERRORE-PG_EXCEPTION_CONTEXT',json_build_object('PG_EXCEPTION_CONTEXT',error_message  ), TRUE );                                      

    SOLLEVA AVVISO 'ALLARME: %' , SQLERRM ;

    SELEZIONA 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 che sia più che sufficiente. Se qualcuno è interessato ai dettagli e ai risultati, scrivete nei commenti, sarò lieto di arricchire l'immagine con ulteriori dettagli.

P.S.

Logging di un errore semplice — tipo di parametro di ingresso

-[ RECORD 1 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1072
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | STARTED
jsonb_pretty    | {
                |     "dko": {
                |         "id": 4,
                |         "type": "Type1",
                |         "title": "CREATO DA addKD",
                |         "Weight": 10,
                |         "Tr": "300",
                |         "reduction": 10,
                |         "isTrud": "TRUE",
                |         "description": "descrizione",
                |         "lowerTr": "100",
                |         "measurement": "misura1",
                |         "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          |  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-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          |  ERRORE-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          |  ERRORE-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          |  ERRORE-MESSAGE_TEXT
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-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          |  ERRORE-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          |  ERRORE-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          |  ERRORE-PG_EXCEPTION_CONTEXT
jsonb_pretty    | {
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  ERRORE-MESSAGE_TEXT
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "sintassi di input non valida per tipo numerico: "null""
                | }

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster