Backend веб-сервиса в базе данных. Как заложить бизнес логику и сделать микросервиc-ретранслятор для API фронтенду

от автора

Эта статья будет рассказывать как организовать легкое проектирование бизнес логики веб-сервиса в базе данных на встроенном PL-SQL.

Я расскажу как сделать простой сервис ретранслятор для фронтенда (пример будет на php), а так же как легко проектировать в базе аналог API и регистрировать это для ретрансляции фронтенду.

PS: пример бекенда и фронтенда будет на PHP, база данных firebird. Но это не обязательно, можно использовать любой язык программирования и любую БД.

PS: Прошу шапками не закидывать. Знаю, сейчас это не популярно, в силу определенных общественных причин, специалистов по PHP или Python на рынке больше чем SQL-программистов, они стоят дешевле, все любят ОРМ. Считается что при закладке логики в базе сложнее организовать микросервиcную архитектуру. Go компилируется и должен работать быстрее чем БД. Популярно закладывать бизнес логику в фреймворках на php и т.п.
Причин много и у всех есть аргументы и с одной и с другой стороны, и абсолютно по каждому можно глубоко поспорить.

Но эта статья для тех, кто хочет задавать бизнес логику именно в базе. Здесь нет цели пропагандировать такой способ. Будьте пожалуйста корректны в комментариях.

В целом способ очень прост и сердит по быстродействию. Если написать грамотный микросервис-ретранслятор и активно использовать служебные таблицы в БД, то регистрация новых API требует только одной служебной таблицы вида:

CREATE TABLE DFM_PROC (     ID           INTEGER NOT NULL,     NAME         VARCHAR(70) NOT NULL,     DISCRIPTION  VARCHAR(1000) );

Например:

image

Схема следующая:

image

Здесь мы будем рассматривать как организовать работу микросервиса «Service 
for interface».

Пример фронтенда приведу на php

function ser1($proc_id,$json) { //Формируем запрос к микросервису $post='hash='.$_SESSION['sess_id'].'&user_id='.$_SESSION['id_user'].'&proc_id='.$proc_id.'&json='.base64_encode($json);  //Адрес микросервиса $url = 'http://192.168.128.1/ser.php'; 	  $ch = curl_init();     curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);     curl_setopt($ch, CURLOPT_URL, $url);     curl_setopt($ch, CURLOPT_POST , true);     curl_setopt($ch, CURLOPT_POSTFIELDS ,$post);     $result = curl_exec($ch);     $arr_res=json_decode($result,true);			 curl_close($ch); 	 return $arr_res; }  //В фронтенде например при обработке нажатия кнопки формируем запрос к бекенду //В данном примере будет создаваться новый профиль юзера //Процедура 4 - добавляет новый профиль в нашем примере if (isset($_POST['go_new_prof'])) {     unset($arr);     $arr['0']=$_SESSION['id_user'];     $arr['1']=(int)$_GET['tp'];     $arr['2']=(int)$_POST['lang_list'];     $arr_prof=ser1(4,json_encode($arr)); } 

Пример вызываемой процедуры на SQL

create or alter procedure DFM_PROF_ADD (     USER_ID smallint,     TYPE_ varchar(10),     LANG smallint) as begin    INSERT INTO DFM_PROFILES (USER_ID, TYPE_, LANG)     VALUES (:USER_ID, :TYPE_, :LANG);   suspend;  end

Теперь разбираемся как микросервис-ретранслятор поймет что надо выполнить именно эту процедуру, как поймет какие у нее параметры, как ее дернет. Приведу пример кода, подробности в комментариях в коде

 <?php  $proc_id=(int)$_POST['proc_id']; $json= base64_decode($_POST['json']);  $arr_json = json_decode($json,true);  //Валидируем JSON switch (json_last_error()) {    case JSON_ERROR_NONE:       $err = null;    break;    case JSON_ERROR_DEPTH:       $err = 'Достигнута максимальная глубина стека';    break;    case JSON_ERROR_STATE_MISMATCH:       $err = 'Некорректные разряды или не совпадение режимов';    break;    case JSON_ERROR_CTRL_CHAR:       $err = 'Некорректный управляющий символ';    break;    case JSON_ERROR_SYNTAX:       $err = 'Синтаксическая ошибка, не корректный JSON';    break;    case JSON_ERROR_UTF8:       $err = 'Некорректные символы UTF-8, возможно неверная кодировка';    break;    default:       $err = 'Неизвестная ошибка';    break; }  //Выполняем только если нет ошибки JSON if ($err==null)  {        //Перед всем этим кодом авторизируйте юзера и задайте его ID    $user_id=1;     //Получаем название процедуры по ее ID    $sql_proc = "select p.name from DFM_PROC p where p.id=".$proc_id;    $res_proc = ibase_query($dbh, $sql_proc);    $prom_proc = ibase_fetch_row($res_proc);        //Начинаем формировать текст запроса в базу    $sql_in='select ';        //Получаем исходящие параметры процедуры,     //в служебной таблице RDB$PROCEDURE_PARAMETERS содержится название входящих и исходящих параметров процедуры    $sql = 'select pp.rdb$parameter_number, pp.rdb$parameter_name from DFM_PROC p left join RDB$PROCEDURE_PARAMETERS pp on pp.rdb$procedure_name=UPPER(p.name) where p.id='.$proc_id.' and pp.rdb$parameter_type=1 --исходящие order by pp.rdb$parameter_number';    $res = ibase_query($dbh, $sql);    $i_cou_out=0;    while($prom = ibase_fetch_row($res))    {       //p. - это будет ниже мнемоника процедуры       $i_cou_out++;       if ($prom[0]>0) $sql_in.=',';        $sql_in.='p.'.$prom[1];              $name_out[$prom[0]]=$prom[1];    }        //После того как сформировали строку с параметрами идем дальше в формировании запроса - вбиваем название процедуры    $sql_in.=' from '.$prom_proc[0];        //Если ответа быть не должно, а чисто выполнение процедуры, то делаем execute procedure    if ($i_cou_out==0) $sql_in='execute procedure '.$prom_proc[0];        //заполняем входящие параметры с учетом типа данных    $sql = 'select pp.rdb$parameter_number, pp.rdb$parameter_name, case    when f.rdb$field_type=7 then 1 --smalint    when f.rdb$field_type=8 then 2 --integer    when f.rdb$field_type=16 and f.rdb$field_scale=0 then 3 --bigint    when f.rdb$field_type=16 and f.rdb$field_scale<0 then 4 --frloat    when f.rdb$field_type=12 then 5 --date    when f.rdb$field_type=13 then 6 --time    when f.rdb$field_type=35 then 7 --timestamp    when f.rdb$field_type=37 then 8 --varcahr end, f.rdb$field_type, --тип данных f.rdb$character_length, --длина текста f.rdb$field_precision, --целых знаков f.rdb$field_scale --дробных знаков from DFM_PROC p left join RDB$PROCEDURE_PARAMETERS pp on pp.rdb$procedure_name=UPPER(p.name) left join RDB$FIELDS f on f.rdb$field_name=pp.rdb$field_source where p.id='.$proc_id.' and pp.rdb$parameter_type=0 --входящие order by pp.rdb$parameter_number';    $res = ibase_query($dbh, $sql);                 $i_cou=0;    while($prom = ibase_fetch_row($res))    {       $i_cou++;       if ($prom[0]>0)  $sql_in.=','; else $sql_in.='(';                     if (($prom[2]==5)or($prom[2]==6)or($prom[2]==7)or($prom[2]==8))          //Обрабатываем параметры где нужны кавычки          //Елси пришла пустота подставляем null          if ($arr_json[$prom[0]]=='')             $sql_in.="null";          else             $sql_in.="'".($arr_json[$prom[0]])."'";       else          //Обрабатываем параметры без кавычек          if ($arr_json[$prom[0]]=='')             $sql_in.="null";          else             $sql_in.=($arr_json[$prom[0]]);           }        //Закрываем входящие параметры процедуры    if ($i_cou_out==0)       {if ($i_cou>0) $sql_in.=')';}    else       {if ($i_cou>0) $sql_in.=') p'; else $sql_in.=' p';}       //Тут задали мнемонику p. для исходящих параметров        //Выполняем процедуру    $res_in = ibase_query($dbh, $sql_in);            if ($i_cou_out==0)    {       //Задаем ответ фронтенду если execute procedure       $json_out='{ "error_json":"", "error_code_json":"", "result":"выполнено execute procedure", "result_code":0 }';     }    else    {       //Заполняем в json ответ фронтенду       $i_json=0;       $json_out='{';       while($prom_in = ibase_fetch_row($res_in))       {          if ($i_json>0) $json_out.=',';          $json_out.='"'.$i_json.'":{';          foreach($prom_in as $k => $v)          {             if ($k>0) $json_out.=', ';             $json_out.='"'.trim($name_out[$k]).'":"'.$v.'"';          }          $json_out.='}';          $i_json++;       }       $json_out.='}';    }        //Если процедура была выполнена с ошибкой - отправляем код ошибки, если нет, то результат    if (ibase_errmsg()=='')    {       //Результат для фронтенда       echo $json_out;       }    else    {       //Код ошибки для фронтенда       $err_json='{ "error_json":"'.$err.'", "error_code_json":'.json_last_error().', "result":"'.str_replace('"','\'',ibase_errmsg()).'", "result_code":2, "sql_err":"'.$sql_in.'"}';       echo $err_json;    }                  } else  {    //Код ошибки валидации входящего JSON    $err_json='{ "error_json":"'.$err.'", "error_code_json":'.json_last_error().', "result":"Ошибка json", "result_code":3 }';    echo $err_json;        }  ?> 

Что получаем в итоге.

1) Микросервис-ретранслятор пишем 1 раз. Все остальное проектирование архитектуры бекенда происходит в базе. В PHP (или Go, Python- на чем будет микросервис) мы больше не лезем.

Например понадобилась новая фишка на сайте — делается фронтенд, делается процедура. Процедура регистрируется в табличке и фронтенд по ее регистрационному номеру обращается к API микросервиса. ВСЕ.

Рекомендую микросервис-ретранслятор писать на Go как на компилируемом и существенно более быстром. Преимущество в том — что это надо написать только 1 раз и программа не сложна. При этом это будет высоконагруженный элемент, к которому происходят интенсивные обращения.

Библиотека для подключения firebird к goland: https://github.com/nakagami/firebirdsql.

2) Вся обработка уходит в процедуры — мы получаем компилированные элементы, которые обрабатывают все на уровне базы данных и выдают РЕЗУЛЬТАТ. Т.е. если операция состоит из нескольких select, insert, update и т.п. мы не гоняем данные по сети к бекенду, а сразу обрабатываем в базе.

Плюс процедура — компилированный элемент с сформированным планом запроса. На это тоже ресурсы не тратятся.

3) Микросервис-ретранслятор, чтобы сформировать процедуру обращается в базу данных 4 раза.

1. Получает ее название
2. Получает ее исходящие параметры
3. Получает ее входящие параметры
4. Выполняет процедуру
4 — раза, потому что рассмотрели универсальный способ. Это можно сократить до одного раза.

Как:

1. Это можно хранить локально в текстовике на веб сервере например. Периодически просто дергая эту таблицу и обновляя когда добавляется новое.
2,3. Аналогично можно хранить локально, дергая все это когда что-то обновляется.

4) Не весь бекенд можно сделать на базе данных! Это существенная часть всего, но надо руководствоваться соображениями целесообразности.

Например какие-нибудь чаты, рассылки и т.п. стороны жизни веб-сервиса конечно надо делать отдельными микросервисами на языке программирования, наиболее удобного для этого. Не надо всю логику веб-сервиса переносить в базу во чтобы это ни стало! Надо переносить только ту часть, которая удобна для этого!

ссылка на оригинал статьи https://habr.com/ru/post/539480/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *