Etúda o realizácii obchodnej logiky na úrovni uložených funkcií PostgreSQL

Podnetom na napísanie etúdy bola článok „V karanténe sa záťaž zvýšila 5-krát, no boli sme pripravení“. Ako sa Lingualeo presunul na PostgreSQL s 23 miliónmi používateľov. Takisto sa zdala zaujímavá článok zverejnený pred 4 rokmi — Realizácia obchodnej logiky v MySQL.

Zaujímavé bolo to, že tá istá myšlienka - "realizovať obchodnú logiku v DB".

Etúda o realizácii obchodnej logiky na úrovni uložených funkcií PostgreSQL

prišla na myseľ nielen mne samotnému.

Taktiež som si chcel v budúcnosti uchovať, pre seba predovšetkým, zaujímavé poznatky, ktoré vznikli počas realizácie. Zvlášť keď sa relatívne nedávno prijalo strategické rozhodnutie o zmene architektúry a presune obchodnej logiky na úroveň backendu. Takže všetko, čo bolo vyvinuté, čoskoro nikomu nebude potrebné a nikomu nebude zaujímavé.

Opísané metódy nie sú žiadnym objavom ani výnimočným know how, všetko je klasické a bolo realizované viackrát (ja som napríklad podobný prístup aplikoval pred 20 rokmi na Oracle). Rozhodol som sa len všetko zhromaždiť na jednom mieste. Možno to niekomu príde vhod. Ako ukázala prax — dosť často tá istá myšlienka príde nezávisle rôznym ľuďom. A pre seba si to nechať na pamiatku je užitočné.

Samozrejme, že nič na tomto svete nie je dokonalé, chyby a preklepy sú bohužiaľ možné. Kritiku a pripomienky vítam a očakávam. A ešte jedna maličkosť — konkrétne detaily realizácie sú vynechané. Nakoniec všetko je stále používané v skutočne fungujúcom projekte. Takže článok je skôr etúda a opis všeobecnej koncepcie, nič viac. Dúfam, že na pochopenie celkového obrazu sú detaily postačujúce.

Hlavná myšlienka — „rozdeľuj a panuj, skrývaj a maj v moci“

Klasická myšlienka — samostatná schéma pre tabuľky, samostatná schéma pre uložené funkcie.
Klient nemá prístup k údajom priamo. Všetko, čo klient môže vykonať — je len zavolať uloženú funkciu a spracovať prijatú odpoveď.

Úlohy

CREATE ROLE store;

CREATE ROLE sys_functions;

CREATE ROLE loc_audit_functions;

CREATE ROLE service_functions;

CREATE ROLE business_functions;

Schémy

Schéma ukladania tabuliek

Cieľové tabuľky, ktoré realizujú predmetné entity.

CREATE SCHEMA store AUTHORIZATION store ;

Schéma systémových funkcií

Systémové funkcie, najmä pre logovanie zmien tabuliek.

CREATE SCHEMA sys_functions AUTHORIZATION sys_functions ;

Schéma lokálneho auditu

Funkcie a schémy na realizáciu lokálneho auditu vykonávania uložených funkcií a zmeny cieľových tabuliek.

CREATE SCHEMA loc_audit_functions AUTHORIZATION loc_audit_functions;

Schéma servisných funkcií

Funkcie pre servisné a DML funkcie.

CREATE SCHEMA service_functions AUTHORIZATION service_functions;

Schéma obchodných funkcií

Funkcie pre konečné obchodné funkcie vyvolávané klientom.

CREATE SCHEMA business_functions AUTHORIZATION business_functions;

Práva prístupu

Úloha — DBA má plný prístup ku všetkým schémam (oddelené od úlohy 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;

Úloha — USER má privilégiu EXECUTE v schéme business_functions.

CREATE ROLE user_role;

Privilégiá medzi schémami

GRANT
Keďže všetky funkcie sú vytvorené s atribútom SECURITY DEFINER je potrebné pokyn 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 ;

Schéma DB je teda pripravená. Môžeme pristúpiť k naplneniu údajmi.

Cieľové tabulky

Vytváranie tabuliek je triviálne. Žiadne zvláštne aspekty, okrem toho, že sa rozhodlo vzdať sa použitia SERIAL a explicitne generovať sekvencie. Plus, samozrejme maximálne využitie pokynu

COMMENT ON ...

Komentáre pre všetkých objekty, bez výnimky.

Lokálny audit

Na vedenie denníka vykonávania uložených funkcií a zmeny cieľových tabuliek sa používa tabuľka lokálneho auditu, ktorá zahŕňa aj detaily klientskej spojenia, označenie vyvolávaného modulu, skutočné hodnoty vstupných a výstupných parametrov vo forme JSON.

Systémové funkcie

Sú určené na zaznamenávanie zmien v cieľových tabuľkách. Predstavujú spúšťacie funkcie.

Šablóna — systémová funkcia

---------------------------------------------------------
-- VLOŽIŤ
VYTVORIŤ ALEBO NATRLO VIAC FUNKCIU sys_functions.table_insert_log ()
VRACIA TRIGGER AS $$
ZAČAŤ
  VYKONAŤ loc_audit_functions.make_log( ' '||'table' , 'insert' , json_build_object('id', NEW.id)  );
  VRÁTIŤ NULL ;
KONIEC
$$ JAZYK plpgsql SECURITY DEFINER;

VYTVORIŤ TRIGGER table_after_insert PO VLOŽENÍ NA storage.table PRE KAŽDÝ RÁD EXECUTE PROCEDÚRU sys_functions.table_insert_log();

---------------------------------------------------------
-- AKTUALIZOVAŤ
VYTVORIŤ ALEBO NATRLO VIAC FUNKCIU sys_functions.table_update_log ()
VRACIA TRIGGER AS $$
ZAČAŤ
  AK OLD.column != NEW.column
  POTOM
    VYKONAŤ loc_audit_functions.make_log( ' '||'table' , 'update' , json_build_object('OLD.column', OLD.column , 'NEW.column', NEW.column )  );
  KONIEC AK ;
  VRÁTIŤ NULL ;
KONIEC
$$ JAZYK plpgsql SECURITY DEFINER;

VYTVORIŤ TRIGGER table_after_update PO AKTUALIZÁCII NA storage.table PRE KAŽDÝ RÁD EXECUTE PROCEDÚRU sys_functions.table_update_log ();

---------------------------------------------------------
-- ZMAZANIE
VYTVORIŤ ALEBO NATRLO VIAC FUNKCIU sys_functions.table_delete_log ()
VRACIA TRIGGER AS $$
ZAČAŤ
  VYKONAŤ loc_audit_functions.make_log( ' '||'table' , 'delete' , json_build_object('id', OLD.id )  );
  VRÁTIŤ NULL ;
KONIEC
$$ JAZYK plpgsql SECURITY DEFINER;

VYTVORIŤ TRIGGER table_after_delete PO ZMAZANÍ NA storage.table PRE KAŽDÝ RÁD EXECUTE PROCEDÚRU sys_functions.table_delete_log ();

Služobné funkcie

Sú určené na realizáciu služobných a DML operácií nad cieľovými tabuľkami.

Šablóna — služobná funkcia

--VLOŽIŤ
--VRÁTIŤ ID NOVÉHO RÁD
VYTVORIŤ ALEBO NATRLO VIAC FUNKCIU service_functions.table_insert ( new_column store.table.column%TYPE )
VRACIA integer AS $$
DEKLAROVAŤ
  new_id integer ;
ZAČAŤ
  -- Vygenerovať nové id
  new_id = nextval('store.table.seq');

  -- Vložiť do tabuľky
  VLOŽIŤ DO store.table
  ( 
    id ,
    column
   )
  HODNOTY
  (
   new_id ,
   new_column
   );

VRÁTIŤ new_id ;
KONIEC
$$ JAZYK plpgsql SECURITY DEFINER;

--ZMAZAŤ
--VRÁTIŤ ČÍSLA RÁD ZMAZANÝCH
VYTVORIŤ ALEBO NATRLO VIAC FUNKCIU service_functions.table_delete ( current_id integer ) 
VRACIA integer AS $$
DEKLAROVAŤ
  rows_count integer  ;    
ZAČAŤ
  ZMAZAŤ Z store.table KDE id = current_id; 

  ZÍSKAŤ DIAGNOSTIKY rows_count = ROW_COUNT;                                                                           

  VRÁTIŤ rows_count ;
KONIEC
$$ JAZYK plpgsql SECURITY DEFINER;
 
-- AKTUALIZOVAŤ PODROBNOSTI
-- VRÁTIŤ ČÍSLA RÁD AKTUALIZOVANÝCH
VYTVORIŤ ALEBO NATRLO VIAC FUNKCIU service_functions.table_update_column 
(
  current_id integer 
  ,new_column store.table.column%TYPE
) 
VRACIA integer AS $$
DEKLAROVAŤ
  rows_count integer  ; 
ZAČAŤ
  AKTUALIZOVAŤ store.table
  NASTAVIŤ
    column = new_column
  KDE id = current_id;

  ZÍSKAŤ DIAGNOSTIKY rows_count = ROW_COUNT;                                                                           

  VRÁTIŤ rows_count ;
KONIEC
$$ JAZYK plpgsql SECURITY DEFINER;

Obchodné funkcie

Sú určené pre konečné obchodné funkcie volané klientom. Vždy vracajú — JSON. Na zachytenie a protokolovanie chýb vykonania sa používa blok VÝNIMKA.

Šablóna — obchodná funkcia

CREATE OR REPLACE FUNCTION business_functions.business_function_template(
--Vstupné parametre        
 )
RETURNS JSON AS $$
DECLARE
  ------------------------
  --na zachytávanie výnimiek
  error_message text ;
  error_json json ;
  result json ;
  ------------------------ 
BEGIN
--LOGOVANIE
  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'ZAČATÉ',
    json_build_object
    (
	--IN Parametre
    ) 
   );

  PERFORM business_functions.notice('business_function_template');            

  --ZAČIATOK OBCHODNEJ ČASTI
  --KONIEC OBCHODNEJ ČASTI

  -- ÚSPEŠNÝ VÝSLEDOK
  PERFORM business_functions.notice('výsledok');
  PERFORM business_functions.notice(result);

  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'SKONČENÉ', 
    json_build_object( 'výsledok',result )
  );

  RETURN result ;
----------------------------------------------------------------------------------------------------------
-- ZACHYTÁVANIE VÝNIMIEK
EXCEPTION                        
  WHEN OTHERS THEN    
    PERFORM loc_audit_functions.make_log
    (
      'business_function_template',
      'ZAČATÉ',
      json_build_object
      (
	--IN Parametre	
      ) , TRUE );

     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' CHYBA',
       json_build_object('SQLSTATE',SQLSTATE ), TRUE 
     );

     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' CHYBA',
       json_build_object('SQLERRM',SQLERRM  ), TRUE 
      );

     GET STACKED DIAGNOSTICS error_message = RETURNED_SQLSTATE ;
     PERFORM loc_audit_functions.make_log
     (
      'business_function_template',
      ' CHYBA-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',
       ' CHYBA-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',
      ' CHYBA-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',
       ' CHYBA-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',
       ' CHYBA-MESSAGE_TEXT',json_build_object('MESSAGE_TEXT',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = SCHEMA_NAME ;
     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' CHYBA-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',
      ' CHYBA-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',
       ' CHYBA-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',
      ' CHYBA-PG_EXCEPTION_CONTEXT',json_build_object('PG_EXCEPTION_CONTEXT',error_message  ), TRUE );                                      

    RAISE WARNING 'POPLACH: %' , SQLERRM ;

    SELECT json_build_object
    (
      'isError' , TRUE ,
      'errorMsg' , SQLERRM
     ) INTO error_json ;

  RETURN  error_json ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

Záver

Na opísanie celkového obrazu si myslím, že to úplne postačuje. Ak niekoho zaujímajú podrobnosti a výsledky, píšte komentáre, s radosťou doplním obraz ďalšími detailmi.

P.S.

Zaznamenávanie jednoduchej chyby — typ vstupného parametra

-[ ZÁZNAM 1 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1072
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ZAČATÉ
jsonb_pretty    | {
                |     "dko": {
                |         "id": 4,
                |         "type": "Typ1",
                |         "title": "VYTVORENÉ pomocou addKD",
                |         "Weight": 10,
                |         "Tr": "300",
                |         "reduction": 10,
                |         "isTrud": "TRUE",
                |         "description": "popis",
                |         "lowerTr": "100",
                |         "measurement": "mierka1",
                |         "methodology": "m1",
                |         "passportUrl": "files",
                |         "upperTr": "200",
                |         "weightingFactor": 100.123,
                |         "actualTrValue": null,
                |         "upperTrCalcNumber": "120"
                |     },
                |     "CardId": 3
                | }
-[ ZÁZNAM 2 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1073
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA
jsonb_pretty    | {
                |     "SQLSTATE": "22P02"
                | }
-[ ZÁZNAM 3 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1074
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA
jsonb_pretty    | {
                |     "SQLERRM": "neplatná vstupná syntax pre typ číselný: "null""
                | }
-[ ZÁZNAM 4 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1075
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-VRÁTENÝ_SQLSTATE
jsonb_pretty    | {
                |     "RETURNED_SQLSTATE": "22P02"
                | }
-[ ZÁZNAM 5 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1076
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-NÁZOV_STĹPCA
jsonb_pretty    | {
                |     "COLUMN_NAME": ""
                | }

-[ ZÁZNAM 6 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1077
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-NÁZOV_OBMEDZENIA
jsonb_pretty    | {
                |     "CONSTRAINT_NAME": ""
                | }
-[ ZÁZNAM 7 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1078
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-NÁZOV_DATOVÉHO_TYPU_PG
jsonb_pretty    | {
                |     "PG_DATATYPE_NAME": ""
                | }
-[ ZÁZNAM 8 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1079
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-TEKST_SPRÁVY
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "neplatná vstupná syntax pre typ číselný: "null""
                | }
-[ ZÁZNAM 9 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1080
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-NÁZOV_SCHÉMY
jsonb_pretty    | {
                |     "SCHEMA_NAME": ""
                | }
-[ ZÁZNAM 10 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1081
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-PG_VYJASNENIE_VÝNIMKY
jsonb_pretty    | {
                |     "PG_EXCEPTION_DETAIL": ""
                | }
-[ ZÁZNAM 11 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1082
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-PG_NÁZOV_NÁPRAVY_VÝNIMKY
jsonb_pretty    | {
                |     "PG_EXCEPTION_HINT": ""
                | }
-[ ZÁZNAM 12 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1083
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-PG_KONTEXT_VÝNIMKY
jsonb_pretty    | {
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | CHYBA-TEKST_SPRÁVY
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "neplatná vstupná syntax pre typ číselný: "null""
                | }

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster