Studie o implementaci obchodní logiky na úrovni uložených funkcí PostgreSQL

Podnětem k napsání studie byl článek „Během karantény vzrostlo zatížení pětkrát, ale byli jsme připraveni“. Jak Lingualeo přešel na PostgreSQL s 23 miliony uživatelů. Také se mi zdálo zajímavé, že článek byl publikován před 4 lety — Implementace obchodní logiky v MySQL.

Zajímavé mi přišlo, že stejná myšlenka — "implementovat obchodní logiku v DB".

Studie o implementaci obchodní logiky na úrovni uložených funkcí PostgreSQL

napadla nejen mě.

Také bych chtěl do budoucna uchovat zajímavé poznatky, které vznikly během realizace, především pro sebe. Zvlášť vzhledem k tomu, že poměrně nedávno bylo přijato strategické rozhodnutí o změně architektury a přesunu obchodní logiky na úroveň backendu. Takže to, co bylo vytvořeno, brzy nebude nikomu potřebné a nikoho nezajímá.

Popisované metody nejsou žádným objevem či výjimečným know how, vše podle klasiky a bylo realizováno mnohokrát (já například podobný přístup použil před 20 lety na Oracle). Jen jsem se rozhodl všechno shromáždit na jedno místo. Možná se to někomu bude hodit. Jak ukázala praxe — poměrně často stejný nápad přichází nezávisle různým lidem. A také pro sebe jako vzpomínku je to užitečné.

Samozřejmě, že nic na tomto světě není dokonalé, chyby a překlepy jsou, bohužel, možné. Kritika a připomínky jsou vítány a očekávány. A ještě jedna maličkost — konkrétní detaily realizace jsou opomenuty. Všechno se totiž zatím používá v reálném pracovním projektu. Takže článek je jako studie a popis obecné koncepce, nic víc. Doufám, že pro pochopení celkového obrázku, jsou detaily dostatečné.

Obecná myšlenka — „dělení a vládnutí, skrývání a vlastnění“

Klasická myšlenka — samostatný schéma pro tabulky, samostatný schéma pro uložené funkce.
Klient nemá přístup k datům přímo. Všechno, co může klient provést — pouze zavolat uloženou funkci a zpracovat přijatou odpověď.

Role

CREATE ROLE store;

CREATE ROLE sys_functions;

CREATE ROLE loc_audit_functions;

CREATE ROLE service_functions;

CREATE ROLE business_functions;

Schémata

Schéma pro ukládání tabulek

Cílové tabulky, které realizují předmětové entity.

CREATE SCHEMA store AUTHORIZATION store;

Schéma systémových funkcí

Systémové funkce, zejména pro logování změn tabulek.

CREATE SCHEMA sys_functions AUTHORIZATION sys_functions;

Schéma lokálního auditu

Funkce a schémata pro realizaci lokálního auditu provádění uložených funkcí a změn cílových tabulek.

VYTVORIT SCHÉMA loc_audit_functions AUTORIZACE loc_audit_functions;

Schéma servisních funkcí

Funkce pro servisní a DML funkce.

VYTVORIT SCHÉMA service_functions AUTORIZACE service_functions;

Schéma obchodních funkcí

Funkce pro konečné obchodní funkce vyvolávané klientem.

VYTVORIT SCHÉMA business_functions AUTORIZACE business_functions;

Práva přístupu

Role — DBA má plný přístup ke všem schématům (odděleno od role DB Owner).

VYTVORIT ROLE dba_role;
UDĚLOVAT store PRO dba_role;
UDĚLOVAT sys_functions PRO dba_role;
UDĚLOVAT loc_audit_functions PRO dba_role;
UDĚLOVAT service_functions PRO dba_role;
UDĚLOVAT business_functions PRO dba_role;

Role — UŽIVATEL má privilegium PROVÉST ve schématu business_functions.

VYTVORIT ROLE user_role;

Privilegie mezi schématy

UDĚLOVAT
Vzhledem k tomu, že všechny funkce jsou vytvářeny s atributem SECURITY DEFINER je potřeba příkaz ODEBRAT EXECUTE NA VŠECH FUNKCÍCH… OD veřejnosti;

ODEBRAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA sys_functions OD veřejnosti;
ODEBRAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA loc_audit_functions OD veřejnosti;
ODEBRAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA service_functions OD veřejnosti;
ODEBRAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA business_functions OD veřejnosti;

UDĚLOVAT POUŽITÍ VE SCHÉMA sys_functions PRO dba_role;
UDĚLOVAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA sys_functions PRO dba_role;
UDĚLOVAT POUŽITÍ VE SCHÉMA loc_audit_functions PRO dba_role;
UDĚLOVAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA loc_audit_functions PRO dba_role;
UDĚLOVAT POUŽITÍ VE SCHÉMA service_functions PRO dba_role;
UDĚLOVAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA service_functions PRO dba_role;
UDĚLOVAT POUŽITÍ VE SCHÉMA business_functions PRO dba_role;
UDĚLOVAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA business_functions PRO dba_role;
UDĚLOVAT EXECUTE NA VŠECH FUNKCÍCH VE SCHÉMA business_functions PRO user_role;

UDĚLOVAT VŠECHNA PRÁVA VE SCHÉMA store PRO SKUPINU business_functions;
UDĚLOVAT VŠECHNA PRÁVA NA VŠECH TABELÁCH VE SCHÉMA store PRO business_functions;
UDĚLOVAT POUŽITÍ NA VŠECH SEKVENCÍCH VE SCHÉMA store PRO business_functions;

Takže schéma DB je hotové. Můžeme začít s naplňováním daty.

Cílové tabulky

Vytvoření tabulek je triviální. Žádné zvláštnosti, s výjimkou toho, že bylo rozhodnuto vzdát se používání SERIAL a generovat sekvence explicitně. Plus, samozřejmě maximální využití příkazu

KOMENTÁŘ K ...

Komentáře pro všechny objekty, bez výjimek.

Lokální audit

Pro vedení záznamu o provádění uložených funkcí a změn cílových tabulek se používá tabulka lokálního auditu, která obsahuje také podrobnosti o klientském spojení, značku vyvolávaného modulu, skutečné hodnoty vstupních a výstupních parametrů ve formátu JSON.

Systémové funkce

Jsou určeny pro logování změn v cílových tabulkách. Představují spouštěcí funkce.

Šablona — systémová funkce

---------------------------------------------------------
-- VLOŽIT
VYTVORIT NEBO REPLACE FUNCTION sys_functions.table_insert_log ()
VRACÍ TRIGGER AS $$
ZAČÍT
  PERFORM loc_audit_functions.make_log( ' '||'table' , 'insert' , json_build_object('id', NEW.id)  );
  VRÁTIT NULL ;
KONEC
$$ JAZYK plpgsql BEZPEČNOSTNÍ DEFINER;

VYTVOŘIT TRIGGER table_after_insert PO VLOŽENÍ NA storage.table PRO KAŽDÝ ŘÁDEK VYKONAT PROCEDURU sys_functions.table_insert_log();

---------------------------------------------------------
-- AKTUALIZACE
VYTVORIT NEBO REPLACE FUNCTION sys_functions.table_update_log ()
VRACÍ TRIGGER AS $$
ZAČÍT
  POKUD OLD.column != NEW.column
  POTOM
    PERFORM loc_audit_functions.make_log( ' '||'table' , 'update' , json_build_object('OLD.column', OLD.column , 'NEW.column' , NEW.column )  );
  KONEC POKUD ;
  VRÁTIT NULL ;
KONEC
$$ JAZYK plpgsql BEZPEČNOSTNÍ DEFINER;

VYTVOŘIT TRIGGER table_after_update PO AKTUALIZACI NA storage.table PRO KAŽDÝ ŘÁDEK VYKONAT PROCEDURU sys_functions.table_update_log();

---------------------------------------------------------
-- SMAZAT
VYTVORIT NEBO REPLACE FUNCTION sys_functions.table_delete_log ()
VRACÍ TRIGGER AS $$
ZAČÍT
  PERFORM loc_audit_functions.make_log( ' '||'table' , 'delete' , json_build_object('id', OLD.id )  );
  VRÁTIT NULL ;
KONEC
$$ JAZYK plpgsql BEZPEČNOSTNÍ DEFINER;

VYTVOŘIT TRIGGER table_after_delete PO SMAZÁNÍ NA storage.table PRO KAŽDÝ ŘÁDEK VYKONAT PROCEDURU sys_functions.table_delete_log ();

Služební funkce

Jsou určeny k provádění servisních a DML operací na cílových tabulkách.

Šablona — služební funkce

--VLOŽIT
--VRÁTIT id NOVÉHO ŘÁDKU
VYTVORIT NEBO REPLACE FUNCTION service_functions.table_insert ( new_column store.table.column%TYPE )
VRACÍ integer AS $$
DECLARE
  new_id integer ;
ZAČÍT
  -- Generovat nové id
  new_id = nextval('store.table.seq');

  -- Vložit do tabulky
  VLOŽIT DO store.table
  ( 
    id ,
    column
   )
  HODNOTY
  (
   new_id ,
   new_column
   );

VRÁTIT new_id ;
KONEC
$$ JAZYK plpgsql BEZPEČNOSTNÍ DEFINER;

--SMAZAT
--VRÁTIT ČÍSLO ŘÁDKŮ SMAZANÝCH
VYTVORIT NEBO REPLACE FUNCTION service_functions.table_delete ( current_id integer ) 
VRACÍ integer AS $$
DECLARE
  rows_count integer  ;    
ZAČÍT
  SMAZAT Z store.table KDE id = current_id; 

  ZÍSKAT DIAGNOSTIKU rows_count = ROW_COUNT;                                                                           

  VRÁTIT rows_count ;
KONEC
$$ JAZYK plpgsql BEZPEČNOSTNÍ DEFINER;
 
-- AKTUALIZACE DETAILŮ
-- VRÁTIT ČÍSLO ŘÁDKŮ AKTUALIZOVANÝCH
VYTVORIT NEBO REPLACE FUNCTION service_functions.table_update_column 
(
  current_id integer 
  ,new_column store.table.column%TYPE
) 
VRACÍ integer AS $$
DECLARE
  rows_count integer  ; 
ZAČÍT
  AKTUALIZOVAT store.table
  NASTAVIT
    column = new_column
  KDE id = current_id;

  ZÍSKAT DIAGNOSTIKU rows_count = ROW_COUNT;                                                                           

  VRÁTIT rows_count ;
KONEC
$$ JAZYK plpgsql BEZPEČNOSTNÍ DEFINER;

Obchodní funkce

Jsou určeny pro koncové obchodní funkce vyvolávané klientem. Vždy vracejí — JSON. Pro zachycení a logování chyb provádění se používá blok VYJÍMKY.

Šablona — obchodní funkce

VYTVOŘIT NEBO ZMĚNIT FUNKCI business_functions.business_function_template(
-- Vstupní parametry        
 )
VRACÍ JSON AS $$
DECLARE
  ------------------------
  -- pro zachytávání výjimek
  error_message text ;
  error_json json ;
  result json ;
  ------------------------ 
BEGIN
-- ZAZNAMENÁNÍ
  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'ZAČÁTEK',
    json_build_object
    (
	-- Vstupní parametry
    ) 
   );

  PERFORM business_functions.notice('business_function_template');            

  -- ZAČÁTEK OBCHODNÍ ČÁSTI
  -- KONEC OBCHODNÍ ČÁSTI

  -- ÚSPĚŠNÝ VÝSLEDOK
  PERFORM business_functions.notice('výsledek');
  PERFORM business_functions.notice(result);

  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'SKONČENO', 
    json_build_object( 'výsledek',result )
  );

  RETURN result ;
----------------------------------------------------------------------------------------------------------
-- ZACHYTÁVÁNÍ VÝJIMEK
EXCEPTION                        
  WHEN OTHERS THEN    
    PERFORM loc_audit_functions.make_log
    (
      'business_function_template',
      'ZAČÁTEK',
      json_build_object
      (
	-- Vstupní parametry	
      ) , 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 'ALARM: %' , SQLERRM ;

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

  RETURN  error_json ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

Výsledek

Pro popsání celkového obrazu si myslím, že to stačí. Pokud někoho zaujaly detaily a výsledky, pište do komentářů, rád doplním obraz dalšími detaily.

P.S.

Logování jednoduché chyby — typ vstupního parametru

-[ ZÁZNAM 1 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1072
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | SPUŠTĚNO
jsonb_pretty    | {
                |     "dko": {
                |         "id": 4,
                |         "type": "Type1",
                |         "title": "VYTVOŘENO pomocí addKD",
                |         "Weight": 10,
                |         "Tr": "300",
                |         "reduction": 10,
                |         "isTrud": "TRUE",
                |         "description": "decription",
                |         "lowerTr": "100",
                |         "measurement": "measurement1",
                |         "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á syntaxe vstupu pro typ numerický: "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-RETURNED_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-JMÉNO_SLUPCE
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ÁZEV_OHLAŠOVANÉHO_OHROZENÍ
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-PG_DATATYPE_NAME
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-ZPRÁVA_TEXTU
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "neplatná syntaxe vstupu pro typ numerický: "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ÁZEV_SČÍTACÍHO_KONTEXTU
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_EXCEPTION_DETAIL
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_EXCEPTION_HINT
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_EXCEPTION_CONTEXT
jsonb_pretty    | {
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  CHYBA-ZPRÁVA_TEXTU
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "neplatná syntaxe vstupu pro typ numerický: "null""
                | }

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster