Отправка и обработка HTTP запросов в PostgreSQL

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В этой статье я хочу поделиться некоторыми наработками, которые мы используем в своих проектах, в частности, тем, как отправлять и обрабатывать HTTP-запросы непосредственно из/в PostgreSQL.

Сразу оговорюсь, Вам не придется устанавливать в базу данных дополнительные расширения и вызывать функции, написанные на отличном от PL/pgSQL языке программирования. Более того, все запросы будут выполняться асинхронно, а обработка полученных данных будет происходить через функции обратного вызова.

Pgweb

Для демонстрации понадобится предварительно настроенная база данных, доступ к которой можно получить или на нашем сайте по адресу https://apostoldevel.com/pgweb, или локально, запустив Docker контейнер.

При переходе по ссылке откроется pgweb - веб-обозреватель для PostgreSQL. В pgweb можно выполнять SQL-запросы, которые будут приведены ниже, а также ознакомиться с содержимым таблиц и программным кодом функций, написанных на PL/pgSQL.

pgweb
pgweb

Docker

Скачиваем образ Docker:

docker pull apostoldevel/apostol

Запускаем:

docker run -p 8080:8080 -p 8081:8081 -p 5433:5432 --name apostol apostoldevel/apostol

Ждём, когда контейнер загрузится, и открываем pgweb по ссылке http://localhost:8081 в браузере.

Теперь у нас есть всё необходимое для того, чтобы выполнить HTTP-запрос непосредственно из PostgreSQL.

Вместо pgweb можно использовать любой другой инструмент для работы с базами данных. PostgreSQL из контейнера будет доступен на порту 5433.

HTTP клиент

В качестве первого примера выполним HTTP-запрос к самому себе (внутри локального хоста):

SELECT http.fetch('http://localhost:8080/api/v1/time', content => null::text);

Выполнение HTTP-запроса будет происходить асинхронно, поэтому в ответ мы получим не данные HTTP-запроса, а uuid идентификатор.

Исходящие HTTP-запросы будут записаны в таблицу http.request, а результат выполнения HTTP-запроса будет сохранён в таблице http.response.

Для просмотра исходящих HTTP-запросов и полученных на них ответов воспользуемся представлением http.fetch:

SELECT * FROM http.fetch ORDER BY datestart DESC;
http.fetch
http.fetch

В поле status будет код HTTP-ответа (HTTP response status codes) на наш HTTP-запрос, а в поле response - ответ.

В качестве следующего примера запросим данные с JSONPlaceholder:

SELECT http.fetch('https://jsonplaceholder.typicode.com/posts/1', 'GET', content => null::text, type => 'curl');

И в поле response представления http.fetch обнаружим:

{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

Здесь дополнительно указан тип curl, данный параметр необходим для запросов к серверам, которые поддерживают только HTTP/2 протокол (выполнение таких запросов происходит через библиотеку cURL).

HTTP сервер

Входящие HTTP-запросы фиксируются в таблице http.log:

SELECT * FROM http.log ORDER BY id DESC;
Log
Log

Для обработки входящих HTTP-запросов в нашем распоряжении есть две PL/pgSQL функции http.get и http.post.

В качестве входящих параметров они принимают:

  • path - Путь;

  • headers - HTTP заголовки;

  • params - Строка запроса (query string), преобразованная в формат JSON;

  • body - Тело запроса, если это POST запрос.

В качестве ответа функции возвращают множество (SETOF) json строк.

Возвращаемое значение в виде множества позволяет нам более эффективно работать с данными, обрабатывая каждую строку в отдельности. Подробнее тут.

Пример кода из http.get:

 WHEN 'log' THEN
 
    FOR r IN SELECT * FROM http.log ORDER BY id DESC
    LOOP
      RETURN NEXT row_to_json(r);
    END LOOP;

Задействовать приведённый выше код можно так:

SELECT http.fetch('http://localhost:8080/api/v1/log', content => null::text);

Функции обратного вызова

В начале статьи я упомянул, что обработка полученных данных будет происходить через функции обратного вызова.

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

Чтобы было понятно, как это работает в связке с HTTP-сервером, я добавил в функцию http.get следующий код:

  WHEN 'latest' THEN

    FOR r IN SELECT * FROM jsonb_to_record(params) AS x(base text, symbols text)
    LOOP
      IF r.base = 'USD' THEN
        RETURN NEXT jsonb_build_object('success', true, 'timestamp', trunc(extract(EPOCH FROM Now())), 'base', r.base, 'date', to_char(Now(), 'YYYY-MM-DD'), 'rates', jsonb_build_object('RUB', 96.245026, 'EUR', 0.946739, 'BTC', 0.000038));
      ELSIF r.base = 'BTC' THEN
        RETURN NEXT jsonb_build_object('success', true, 'timestamp', trunc(extract(EPOCH FROM Now())), 'base', r.base, 'date', to_char(Now(), 'YYYY-MM-DD'), 'rates', jsonb_build_object('RUB', 2542803.2, 'EUR', 25012.95, 'USD', 26420.1));
      ELSE
        RETURN NEXT jsonb_build_object('success', false, jsonb_build_object('code', 400, 'message', format('Base "%s" not supported.', r.base)));
      END IF;
    END LOOP;

На запрос «самых последних» (latest) курсов мы будем возвращать статические данные в формате API курсов валют. Если у Вас есть доступ к службам курсов валют, Вы можете запросить данные через их API.

Обрабатывать полученные данные мы будем с помощью функции обратного вызова public.exchange_rate_done, с программным кодом которой можно ознакомиться в pgweb, а сами запросы будут следующими:

Из контейнера:

SELECT http.fetch('http://localhost:8080/api/v1/latest?base=USD', 'GET', null, null, 'public.exchange_rate_done', 'public.exchange_rate_fail', 'api.exchangerate.host', null, 'latest');
SELECT http.fetch('http://localhost:8080/api/v1/latest?base=BTC', 'GET', null, null, 'public.exchange_rate_done', 'public.exchange_rate_fail', 'api.exchangerate.host', null, 'latest');

Через наш сервер:

SELECT http.fetch('https://apostoldevel.com/api/v1/latest?base=USD', 'GET', null, null, 'public.exchange_rate_done', 'public.exchange_rate_fail', 'api.exchangerate.host', null, 'latest');
SELECT http.fetch('https://apostoldevel.com/api/v1/latest?base=BTC', 'GET', null, null, 'public.exchange_rate_done', 'public.exchange_rate_fail', 'api.exchangerate.host', null, 'latest');

Через службу курсов валют (при наличии доступа):

SELECT http.fetch('https://api.exchangerate.host/latest?base=USD&symbols=BTC,EUR,RUB', 'GET', null, null, 'public.exchange_rate_done', 'public.exchange_rate_fail', 'api.exchangerate.host', null, 'latest', null, 'curl');
SELECT http.fetch('https://api.exchangerate.host/latest?base=BTC&symbols=USD,EUR,RUB', 'GET', null, null, 'public.exchange_rate_done', 'public.exchange_rate_fail', 'api.exchangerate.host', null, 'latest', null, 'curl');

Результатом этих действий станет заполненная данными таблица public.rate.

Посмотреть актуальные на данный момент курсы можно через представление public.rates:

SELECT * FROM public.rates WHERE validFromDate <= Now() AND validToDate > Now();
Rates
Rates

Асинхронное уведомление

Прежде чем подробно разобрать, как же это всё работает, обратимся к документации PostgreSQL:

34.9. Асинхронное уведомление

PostgreSQL предлагает асинхронное уведомление посредством команд LISTEN и NOTIFY. Клиентский сеанс работы регистрирует свою заинтересованность в конкретном канале уведомлений с помощью команды LISTEN (и может остановить прослушивание с помощью команды UNLISTEN). Все сеансы, прослушивающие конкретный канал, будут уведомляться в асинхронном режиме, когда в рамках любого сеанса команда NOTIFY выполняется с параметром, указывающим имя этого канала. Для передачи дополнительных данных прослушивающим сеансам может использоваться строка payload.

Как Вы уже, наверное, догадались, именно этот механизм лежит в основе взаимодействия PostgreSQL с неким клиентским приложением, но не будем забегать вперёд.

Подробно

Рассмотрим более подробно, что происходит на стороне PostgreSQL после вызова функции http.fetch:

PL/pgSQL функция http.fetch является оберткой для функции http.create_request, в которой происходит запись в таблицу http.request с последующим вызовом команды NOTIFY через триггер добавления новой записи. На этом, собственно, всё.

NOTIFY
NOTIFY

Далее в ход вступает некое клиентское приложение, которое подключено к PostgreSQL и готово принимать асинхронные уведомления, и именно оно выступает в качестве HTTP-клиента и сервера, отправляет HTTP-запрос и сохраняет результат в таблицу http.response.

Клиентское приложение

Клиентское приложение — отдельная программа, в задачу которой входит по сигналу от сервера выполнять те или иные действия, в данном случае выполнять HTTP-запросы на основе тех данных, которые содержатся в таблице http.request.

Если Вы или Ваша команда достаточно опытны, то разработать подобное приложение не должно составить труда. Мы же используем свою собственную open source разработку — Апостол.

Activity
Activity

Заключение

Я продемонстрировал простой в использовании, но в то же время очень гибкий механизм коммуникации с внешними системами непосредственно из PostgreSQL.

Если говорить о практическом применении, то представьте, что в вашей системе формируется счёт на оплату, по которому должно произойти автоматическое списание денег с заранее привязанной карты клиента. Все необходимые данные для реализации этой задачи находятся в базе данных. Следовательно, то приложение (микросервис), которое будет взаимодействовать с платежной системой, должно быть подключено к базе данных и каким-то образом оповещено о наличии нового счета. Приложение должно получить необходимые данные, обработать их, сформировать запрос к платежной системе, сохранить результат в БД. После успешного списания необходимо сформировать электронный чек, а это уже взаимодействие с другим сервисом, а ещё было бы неплохо уведомить клиента по e-mail, в виде СМС или через мобильное приложение (FCM) о наличии счёта и статусе выполнения операции.

Иными словами, мы получаем каскад задач на взаимодействие с внешними системами через их API, при этом оперируя теми данными, которые находятся в СУБД.

Так если данные в СУБД и у нас есть механизм коммуникации с внешними системами из PostgreSQL, то почему бы не формировать запросы к API внешних систем в той же среде, где и данные? Вопрос риторический.

Приведённый выше пример не из разряда теоретических выкладок, а вполне практическая и рабочая реализация.

Если нужен более наглядный пример, то вот: Talking to AI это чат-бот в Telegram для общения с искусственным интеллектом (ChatGPT), который реализован на PL/pgSQL.

Есть пример Telegram бота на PL/pgSQL с исходным кодом, ссылка ниже.

Ссылки на исходные коды:

  • Апостол

  • Postgres Fetch

  • Telegram bot in Postgres

Источник: https://habr.com/ru/articles/767442/


Интересные статьи

Интересные статьи

Просто о том, чем отличаются HTTP1, HTTP2 и HTTP3, а также почему HTTP3 ещё и QUIC. Статья для junior'ов, интересующихся и готовящихся к собеседованиям. Я http-любознательный Всего голосов 4: ↑3 и ...
Уже несколько лет назад я столкнулся с проблемой производительности 1С на PostgreSQL в некоторых запросах, которые на MS SQL выполнялись относительно быстро. Тогда же выяснилось, что в 99% случаев так...
Миграция базы данных 1С с MS SQL на PostgreSQL – по-прежнему насущная тема, особенно в контексте импортозамещения. На наших вебинарах и в беседах с клиентами мы получаем много вопросов по нюансам мигр...
В предыдущих статьях о миграции с Oracle на Postgres мы рассматривали перенос данных из одной системы управления базами данных в другую (часть 1, часть 2). Сегодня разговор пойдёт об особенностя...
Мы продолжаем знакомить вас с самыми интересными новостями по PostgreSQL. Главная новость июня EnterpriseDB приобретена инвестиционным фондом Great Hill Partners. Сумма сделки не разгла...