Давайте знакомиться. Меня зовут Андрей Жуйков, я уже больше 15 лет в IT и успел поработать как в России, так и в Европе. Сегодня я занимаюсь темой, которая у одних вызывает скептическую усмешку, а у других — настоящий интерес: импортозамещение в России, особенно в области СУБД. Скажу сразу: всё ниже — это исключительно мой взгляд, основанный на практических кейсах. И именно об этом хочу с вами поговорить.
Если присмотреться к рынку, то становится заметно: многие отечественные решения на базе PostgreSQL представляют собой довольно стандартный рецепт — берём «ванильный» PostgreSQL, слегка дорабатываем, добавляем российское шифрование, популярные расширения и утилиты, после чего регистрируем продукт как отечественный.
В итоге пользователям всё равно приходится переписывать свою хранимую логику, поскольку поддержки конструкций из Oracle и Microsoft в таких продуктах нет.
Такой подход не решает задачу полноценного перехода.
А как в мире смотрят на решение этого вопроса?
В мире на это смотрят по-разному. Есть два подхода.
Первый — всё переписать. Кому-то этот вариант ближе: бизнес-логика становится «чистой», всё работает строго под новую систему. В России тоже есть инструменты миграции под PostgreSQL, вебинары и практики — многие их уже видели.
Второй подход более интересный: вместо того чтобы переписывать код, — научить саму СУБД понимать чужие диалекты. В Амазоне есть наработки для поддержки T-SQL поверх PostgreSQL. В Южной Корее развивают Tibero — старое, но известное в России название, хотя актуальное имя сейчас TmaxData, — это Oracle-совместимая СУБД. А в Китае у Alibaba Cloud есть ProximaDB for Oracle, и да, это та самая Alibaba, которая у большинства в России ассоциируется с AliExpress, только в данном случае они делают ещё и базы данных. Эти решения объединяет одно: они позволяют запустить процедуры и функции без переписывания. И вот тут начинается самое интересное, потому что ровно такой же подход используется в Diasoft. У меня был опыт и с миграторами, и с конвертерами, и я хорошо понимаю их ограничения.
Практика показывает: без поддержки диалектов всё равно упрёшься в переписывание. Работая в банковском секторе, я повстречал Digital Q.DataBase — систему, которая расширяет PostgreSQL и позволяет выполнять процедуры на PL/SQL и T-SQL напрямую, без изменений.
Чтобы было понятнее, приведу пример. Берём процедуру, написанную для Oracle на PL/SQL.
Обычно её нужно перелопачивать, чтобы она заработала в PostgreSQL.
В Digital Q.DataBase я могу запустить её в исходном виде — и она работает.
За этим стоит серьёзная работа: доработка ядра, добавление недостающих конструкций и функций, но конечный результат для разработчика выглядит просто и удобно — бизнес-логика переехала без хирургической операции.
Похожие вещи, причем существенно ранее появились в Tibero (ныне TMaxData) и в ProximaDB for Oracle. Обе реализации очень хороши, но ProximaDB for Oracle работает только в облаке Alibaba Cloud и потому не подходит тем, кто привык к on-premise. А вот с Tibero у ряда крупных российских компаний был вполне положительный опыт перехода на неё с Oracle. В частности, на ней какое-то время работала НСПК — это все пластиковые карты в России. К сожалению, так как Южная Корея поддержала международные санкции в отношении РФ, то это решение не подходит для процессов импортозамещения, хотя и весьма элегантно по своему техническому исполнению. Что же касается эмуляции MS SQL Server от Amazon - она очень хороша для проектов на Java, а вот решения, написанные на Delphi или 1С с ней работать не могут — нет полной поддержки необходимых для такой работы механизмов. Но, поскольку это американская компания, то использование их облака вместо MS SQL Server — это замена «шила на мыло» — и то и другое подпадает под американские санкции против РФ.
Однако, сам факт успешности этих продуктов в мире подтверждает: концепция, реализованная в них верна, а я нахожусь на правильном пути.
Например, в моей эмуляции Oracle работает следующий пример:
REM Таблица товаров магазина, прибывших со склада
CREATE TABLE product_descriptions (
product_id NUMBER PRIMARY KEY, --id товара
product_name VARCHAR2(100), --наименование товара
description CLOB, --описание товара
return_date DATE, --дата отправки обратно на склад
created_date DATE DEFAULT SYSDATE --дата создания карточки принятого товара
);
REM Пакет с 2 процедурами
CREATE OR REPLACE PACKAGE product_utils AS
--Добавление нового товара в систему
PROCEDURE add_product_description(
p_id NUMBER, --id товара
p_name VARCHAR2, --наименование товара
p_text VARCHAR2 --описание товара
);
--Отображение информации о товаре по id
PROCEDURE show_description(
p_id NUMBER --id товара
);
END product_utils;
/
REM Реализация пакета
CREATE OR REPLACE PACKAGE BODY product_utils AS
PROCEDURE add_product_description(
p_id NUMBER,
p_name VARCHAR2,
p_text VARCHAR2
) IS
v_clob CLOB; --Локальные переменные
v_current_date DATE;
BEGIN
DBMS_LOB.CREATETEMPORARY(v_clob, TRUE); --Создаем временный CLOB
DBMS_LOB.WRITE(v_clob, LENGTH(p_text), 1, p_text); --Записываем текст p_text в v_clob
SELECT SYSDATE INTO v_current_date FROM DUAL; --Записываем текущую дату в переменную
--Сохраняем все в таблицу, прибавив 2 месяца к дате отправки на склад:
INSERT INTO product_descriptions (product_id, product_name, description, return_date)
VALUES (p_id, p_name, v_clob, ADD_MONTHS(v_current_date, 2));
DBMS_LOB.FREETEMPORARY(v_clob); --Освобождаем временный CLOB
END;
PROCEDURE show_description(
p_id NUMBER
) IS
v_clob CLOB; --Локальные переменные
v_buffer VARCHAR2(4000);
v_amount_to_read NUMBER := 4000;
v_product_name VARCHAR2(100);
v_return_date DATE;
BEGIN
SELECT product_name INTO v_product_name FROM product_descriptions WHERE product_id = p_id;
SELECT description INTO v_clob FROM product_descriptions WHERE product_id = p_id;
DBMS_LOB.READ(v_clob, v_amount_to_read, 1, v_buffer); --Читаем первые 4000 символов
SELECT return_date INTO v_return_date FROM product_descriptions WHERE product_id = p_id;
--Выводим результат
DBMS_OUTPUT.PUT_LINE('Наименование товара: ' || v_product_name);
DBMS_OUTPUT.PUT_LINE('Описание товара: ' || v_buffer);
DBMS_OUTPUT.PUT_LINE('Дата отправки обратно на склад: ' || v_return_date);
END;
END product_utils;
/
REM Внепакетная функция для проверки истечения срока возврата товара
CREATE OR REPLACE FUNCTION is_return_date_expired(
p_product_id NUMBER
) RETURN VARCHAR2
IS
v_return_date DATE;
v_result VARCHAR2(100);
BEGIN
SELECT return_date INTO v_return_date
FROM product_descriptions
WHERE product_id = p_product_id;
IF v_return_date < SYSDATE THEN
v_result := 'Дата возврата на склад ИСТЕКЛА';
ELSE
v_result := 'Дата возврата на склад НЕ истекла';
END IF;
RETURN v_result;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 'Товар с указанным ID не найден';
END is_return_date_expired;
/
SET SERVEROUTPUT ON
REM Тестовый блок
DECLARE
v_product_id NUMBER := 1; -- id товара
v_product_name VARCHAR2(100) := 'ASUS TUF Gaming A15';
v_description VARCHAR2(4000) := '1920x1080, IPS, AMD Ryzen 5 7535HS, ядра: 6, RAM 16 ГБ, SSD 512 ГБ, GeForce RTX 2050 для ноутбуков 4 ГБ, без ОС';
BEGIN
-- Очищаем таблицу перед тестом
DELETE FROM product_descriptions WHERE product_id = v_product_id;
-- Добавляем описание товара
product_utils.add_product_description(v_product_id, v_product_name, v_description);
-- Просматриваем описание
product_utils.show_description(v_product_id);
-- Проверяем срок возврата товара с помощью внепакетной функции
DBMS_OUTPUT.PUT_LINE('Проверка срока возврата: ' || is_return_date_expired(v_product_id));
-- Очищаем таблицу после теста
DELETE FROM product_descriptions WHERE product_id = v_product_id;
END;
/
REM Зачистка объектов
DROP PACKAGE product_utils;
DROP FUNCTION is_return_date_expired;
DROP TABLE product_descriptions;
В работе у Digital Q.DataBase уже есть много привычных для Oracle вещей.
работает синтаксис PL/SQL, включая пакеты;
понимаются типы данных Oracle;
завезли самые ходовые UTL- и DBMS-пакеты;
конструкциями вроде %type, %rowtype и %rowcount тоже можно пользоваться;
есть поддержка пользовательских типов и nested tables;
встроенные функции из официальной документации Oracle тоже на месте;
для клиента сделали библиотеку, эмулирующую Oracle Instant Client — можно подключаться через неё или через Java-драйвер.
Справедливости ради, поддержка DBMS-пакетов пока не полная. Но уже сейчас её заметно больше, чем у Tibero, и уж точно шире, чем в orafce для PostgreSQL.
В перспективе развития разработчики обещают: сначала добить самые популярные пакеты, а в горизонте пары лет закрыть и редко используемые.
А в моей эмуляции MS SQL Server можно успешно выполнить вот такое:
-- Меняем БД на demo в стиле MS SQL
USE demo
-- Установка формата дат и получение id-соединения - все в стиле MS SQL
SET dateformat dmy
select @@spid
go
-- никаких "точек с запятой", использование конструкции GO - это все также характерно для MS SQL
--Функции из MS SQL, которые часто применяются в запросах и отсутствуют в обычном PostgreSQL
SELECT DATEDIFF(DAY, '01.01.2024', '01.01.2025')
SELECT DATEADD(DAY, 5, '01.01.2025')
-- Удалим таблицу если она есть, заодно проверим функции работы с метаданными из MS SQL
IF OBJECT_ID ( 'AUTO_PK_SUPPORT', 'U' ) IS NOT NULL
DROP TABLE AUTO_PK_SUPPORT
-- Создадим таблицу
CREATE TABLE AUTO_PK_SUPPORT (
TABLE_NAME VARCHAR(18) NOT NULL,
NEXT_ID INTEGER DEFAULT 0 NOT NULL
)
-- Удалим процедуру если она есть
IF OBJECT_ID ( 'AUTO_PK_GEN', 'P' ) IS NOT NULL
DROP PROCEDURE AUTO_PK_GEN
-- Создадим хранимую процедуру используя "родной" для MS SQL синтаксис
-- Обычный PostgreSQL такое никогда исполнить не сможет!
CREATE PROCEDURE AUTO_PK_GEN
@TABLENAME VARCHAR(18),
@BATCHSIZE INTEGER,
@NEXT_ID INTEGER OUT
AS
BEGIN
BEGIN TRANSACTION
UPDATE AUTO_PK_SUPPORT SET NEXT_ID = NEXT_ID + @BATCHSIZE
WHERE TABLE_NAME = @TABLENAME
IF @@ROWCOUNT = 0
INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID)
VALUES (@TABLENAME, 1000)
SET @NEXT_ID = (SELECT NEXT_ID FROM AUTO_PK_SUPPORT WHERE TABLE_NAME = @TABLENAME)
COMMIT
END
-- Удалим функцию если она есть
IF OBJECT_ID ( 'AUTO_PK_FOR_TABLE', 'P' ) IS NOT NULL
DROP PROCEDURE AUTO_PK_FOR_TABLE
-- Создадим функцию используя "родной" для MS SQL синтаксис
CREATE PROCEDURE AUTO_PK_FOR_TABLE
@TNAME VARCHAR(18),
@PKBATCHSIZE INTEGER
AS
BEGIN
DECLARE @NEXT_ID INTEGER
UPDATE AUTO_PK_SUPPORT SET @NEXT_ID = NEXT_ID = NEXT_ID + @PKBATCHSIZE
WHERE TABLE_NAME = @TNAME
SELECT @NEXT_ID as NEXT_ID
END
-- Позовем процедуру, один из параметров - переменная для возврата значений OUTPUT-параметра
DECLARE @pk_value int
EXEC dbo.[AUTO_PK_GEN] @TABLENAME = 'BOOKS', @BATCHSIZE = 10, @NEXT_ID = @pk_value OUTPUT
-- В этом exec есть обращение к ролям и квотирование идентификаторов в стиле MS SQL
SELECT @pk_value as RES
-- Вызовем функцию
EXECUTE AUTO_PK_FOR_TABLE @TNAME = N'BOOKS', @PKBATCHSIZE = 1
С T-SQL получилось как-то неожиданно живо.
синтаксис работает полностью;
типы данных из MS SQL понимаются;
IDENTITY-поля и конструкции вроде
@@IDENTITY
— в наличии;сессии узнаются через
@@SPID;
встроенные функции из документации MS SQL — тоже есть;
процедуры умеют работать с OUTPUT и IN/OUT параметрами;
многие системные таблицы, представления и sp_-хранимые процедуры — на месте;
а подключение по протоколу TDS позволяет внешним приложениям (вроде Microsoft SSMS) думать, что перед ними самый настоящий MS SQL Server.
Честно говоря, мне такой подход кажется более правильным. Он экономит мои силы, время и ресурсы работодателя. А я человек практичный, поэтому теперь всем буду советовать именно так: ничего не переписывать руками и не бегать потом за автоматическими миграторами, исправляя их косяки. Особенно приятно, что тут с уважением относятся к тем десяткам лет, которые я вложил в диалекты Oracle и Microsoft — знания не выбрасываются на свалку, а продолжают работать.
Isiirk
....Особенно приятно, что тут с уважением относятся к тем десяткам лет, которые я вложил в диалекты Oracle и Microsoft....
Так 15 лет в ИТ или десятки?