Étude sur la mise en œuvre de la logique métier au niveau des fonctions stockées PostgreSQL

Le moteur de rédaction de cet essai a été un article «Pendant le confinement, la charge a augmenté de 5 fois, mais nous étions prêts». Comment Lingualeo a migré vers PostgreSQL avec 23 millions d'utilisateurs. Il m'a également semblé intéressant de lire un article publié il y a 4 ans — La mise en œuvre de la logique métier dans MySQL.

Ce qui m'a semblé intéressant, c'est que la même idée -"mettre en œuvre la logique métier dans la base de données".

Étude sur la mise en œuvre de la logique métier au niveau des fonctions stockées PostgreSQL

m'est venue à l'esprit non seulement à moi.

Il était également important, pour l'avenir, de conserver des travaux intéressants développés au cours de la mise en œuvre. Étant donné qu'il a récemment été décidé de changer l'architecture et de déplacer la logique métier au niveau du backend. Ainsi, tout ce qui a été développé ne sera bientôt plus nécessaire et n'intéressera personne.

Les méthodes décrites ne constituent pas une découverte ou une technique exceptionnelle know how, tout cela est classique et a été réalisé de nombreuses fois (par exemple, j'ai appliqué une approche similaire il y a 20 ans sur Oracle). J'ai simplement décidé de rassembler tout au même endroit. Peut-être que cela servira à quelqu'un. Comme l'a montré la pratique, souvent la même idée vient indépendamment à différentes personnes. Et c'est aussi utile de laisser des souvenirs pour soi.

Bien sûr, rien dans ce monde n'est parfait, des erreurs et des coquilles sont malheureusement possibles. Les critiques et les remarques sont toujours les bienvenues et attendues. Et un petit détail — des détails spécifiques de mise en œuvre sont omis. Tout cela est encore utilisé dans un projet réellement fonctionnel. Donc, cet article est davantage un essai et une description d'un concept général, rien de plus. J'espère que pour comprendre l'image générale, les détails sont suffisants.

L'idée générale est — «sépare et domine, cache et possède»

L'idée classique — un schéma distinct pour les tables, un schéma distinct pour les fonctions stockées.
Le client n'a pas accès aux données directement. Tout ce que le client peut faire — c'est appeler une fonction stockée et traiter la réponse reçue.

Rôles

CREATE ROLE store;

CREATE ROLE sys_functions;

CREATE ROLE loc_audit_functions;

CREATE ROLE service_functions;

CREATE ROLE business_functions;

Schémas

Schéma de stockage des tables

Tables cibles, réalisant des entités sujettes.

CREATE SCHEMA store AUTHORIZATION store;

Schéma des fonctions systèmes

Fonctions systèmes, notamment pour enregistrer les modifications des tables.

CREATE SCHEMA sys_functions AUTHORIZATION sys_functions;

Schéma d'audit local

Fonctions et tables pour la mise en œuvre de l'audit local de l'exécution des fonctions stockées et de la modification des tables cibles.

CREATE SCHEMA loc_audit_functions AUTHORIZATION loc_audit_functions;

Schéma des fonctions de service

Fonctions pour les fonctions de service et DML.

CREATE SCHEMA service_functions AUTHORIZATION service_functions;

Schéma des fonctions commerciales

Fonctions pour les fonctions commerciales finales appelées par le client.

CREATE SCHEMA business_functions AUTHORIZATION business_functions;

Droits d'accès

Rôle — DBA a un accès complet à tous les schémas (séparé du rôle de propriétaire de base de données).

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;

Rôle — UTILISATEUR dispose du privilège EXECUTE dans le schéma business_functions.

CREATE ROLE user_role;

Privilèges entre les schémas

GRANT
Comme toutes les fonctions sont créées avec l'attribut SECURITY DEFINER une instruction est nécessaire 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 ;

Ainsi, le schéma de la base de données est prêt. On peut commencer à le remplir avec des données.

Tables cibles

La création des tables est triviale. Aucune particularité, sauf qu'il a été décidé d'abandonner l'utilisation de SERIAL et de générer explicitement les séquences. De plus, bien sûr, un maximum d'utilisation de l'instruction

COMMENT ON ...

Commentaires pour tous les objets, sans exception.

Audit local

Pour le suivi de l'exécution des fonctions stockées et la modification des tables cibles, une table d'audit local est utilisée, qui comprend également les détails de la connexion client, l'étiquette du module appelé, ainsi que les valeurs réelles des paramètres d'entrée et de sortie au format JSON.

Fonctions systèmes

Destinées à enregistrer les modifications dans les tables cibles. Elles représentent des fonctions de déclenchement.

Modèle — fonction système

---------------------------------------------------------
-- INSÉRER
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();

---------------------------------------------------------
-- METTRE À JOUR
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 ();

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

Fonctions de service

Destinées à réaliser des opérations de service et DML sur les tables cibles.

Modèle — fonction de service

--INSÉRER
--RETOURNER l'id DE LA NOUVELLE LIGNE
CREATE OR REPLACE FUNCTION service_functions.table_insert ( new_column store.table.column%TYPE )
RETURNS integer AS $$
DECLARE
  new_id integer ;
BEGIN
  -- Générer un nouvel id
  new_id = nextval('store.table.seq');

  -- Insérer dans la table
  INSERT INTO store.table
  ( 
    id ,
    column
   )
  VALUES
  (
   new_id ,
   new_column
   );

RETURN new_id ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

--SUPPRIMER
--RETOURNER LE NOMBRE DE LIGNES SUPPRIMÉES
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;
 
-- METTRE À JOUR LES DÉTAILS
-- RETOURNER LE NOMBRE DE LIGNES MIS À JOUR
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;

Fonctions métier

Destinées aux fonctions métier finales appelées par le client. Renvoient toujours — JSON. Pour intercepter et journaliser les erreurs d'exécution, le bloc est utilisé EXCEPTION.

Modèle — fonction métier

CREATE OR REPLACE FUNCTION business_functions.business_function_template(
--Paramètres d'entrée        
 )
RETURNS JSON AS $$
DECLARE
  ------------------------
  --pour la gestion des exceptions
  error_message text ;
  error_json json ;
  result json ;
  ------------------------ 
BEGIN
--ENREGISTREMENT
  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'DÉMARRÉ',
    json_build_object
    (
	--Paramètres EN
    ) 
   );

  PERFORM business_functions.notice('business_function_template');            

  --DÉBUT DE LA PARTIE ENTREPRISE
  --FIN DE LA PARTIE ENTREPRISE

  -- RÉSULTAT AVEC SUCCÈS
  PERFORM business_functions.notice('result');
  PERFORM business_functions.notice(result);

  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'TERMINÉ', 
    json_build_object( 'result',result )
  );

  RETURN result ;
----------------------------------------------------------------------------------------------------------
-- GESTION DES EXCEPTIONS
EXCEPTION                        
  WHEN OTHERS THEN    
    PERFORM loc_audit_functions.make_log
    (
      'business_function_template',
      'DÉMARRÉ',
      json_build_object
      (
	--Paramètres EN	
      ) , TRUE );

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

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

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

    RAISE WARNING 'ALERTE : %' , SQLERRM ;

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

  RETURN  error_json ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

Conclusion

Pour décrire le tableau général, je pense que c'est largement suffisant. Si quelqu'un est intéressé par des détails ou des résultats, laissez un commentaire, je me ferai un plaisir d'ajouter des éléments supplémentaires.

P.S.

Journalisation d'une erreur simple — type de paramètre d'entrée

-[ ENREGISTREMENT 1 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1072
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | DÉMARRÉ
jsonb_pretty    | {
                |     "dko": {
                |         "id": 4,
                |         "type": "Type1",
                |         "title": "CRÉÉ PAR addKD",
                |         "Weight": 10,
                |         "Tr": "300",
                |         "reduction": 10,
                |         "isTrud": "VRAI",
                |         "description": "description",
                |         "lowerTr": "100",
                |         "measurement": "mesure1",
                |         "methodology": "m1",
                |         "passportUrl": "fichiers",
                |         "upperTr": "200",
                |         "weightingFactor": 100.123,
                |         "actualTrValue": null,
                |         "upperTrCalcNumber": "120"
                |     },
                |     "CardId": 3
                | }
-[ ENREGISTREMENT 2 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1073
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR
jsonb_pretty    | {
                |     "SQLSTATE": "22P02"
                | }
-[ ENREGISTREMENT 3 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1074
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR
jsonb_pretty    | {
                |     "SQLERRM": "syntaxe d'entrée invalide pour le type numérique : "null""
                | }
-[ ENREGISTREMENT 4 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1075
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-RETURNED_SQLSTATE
jsonb_pretty    | {
                |     "RETURNED_SQLSTATE": "22P02"
                | }
-[ ENREGISTREMENT 5 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1076
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-COLUMN_NAME
jsonb_pretty    | {
                |     "COLUMN_NAME": ""
                | }

-[ ENREGISTREMENT 6 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1077
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-CONSTRAINT_NAME
jsonb_pretty    | {
                |     "CONSTRAINT_NAME": ""
                | }
-[ ENREGISTREMENT 7 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1078
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-PG_DATATYPE_NAME
jsonb_pretty    | {
                |     "PG_DATATYPE_NAME": ""
                | }
-[ ENREGISTREMENT 8 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1079
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-MESSAGE_TEXT
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "syntaxe d'entrée invalide pour le type numérique : "null""
                | }
-[ ENREGISTREMENT 9 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1080
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-SCHEMA_NAME
jsonb_pretty    | {
                |     "SCHEMA_NAME": ""
                | }
-[ ENREGISTREMENT 10 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1081
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-PG_EXCEPTION_DETAIL
jsonb_pretty    | {
                |     "PG_EXCEPTION_DETAIL": ""
                | }
-[ ENREGISTREMENT 11 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1082
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-PG_EXCEPTION_HINT
jsonb_pretty    | {
                |     "PG_EXCEPTION_HINT": ""
                | }
-[ ENREGISTREMENT 12 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1083
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-PG_EXCEPTION_CONTEXT
jsonb_pretty    | {
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | ERREUR-MESSAGE_TEXT
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "syntaxe d'entrée invalide pour le type numérique : "null""
                | }

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster