DevGang
Авторизоваться

Миграция SQL между базами данных

Приложения, работающие с разными базами данных, сталкиваются с проблемой несовместимости SQL-запросов, несмотря на общую схожесть синтаксиса. Ручная адаптация запросов трудоемка и подвержена ошибкам.

Полная автоматизация перевода SQL невозможна из-за различий в функциональности. Однако, основная проблема — разный синтаксис SQL-функций, особенно для работы с датами и строками. Отсутствие стандарта приводит к тому, что каждая СУБД реализует эти функции по-своему (например, преобразование строки «2020-02-05» в дату).

ORACLE:

select TO_DATE('2020-02-05', 'YYYY-MM-DD') from USER

SQL-сервер:

select CONVERT(varchar(100), '2020-02-05', 23) from USER

MySQL:

select DATE_FORMAT('2020-02-05','%Y-%m-%d') from USER

Для обеспечения совместимости с разными базами данных, SQL-запросы приходится переписывать при смене СУБД.

Решение SPL решает эту проблему, предлагая механизм автоматического преобразования стандартного SQL в запросы, совместимые с конкретными базами данных. Это упрощает миграцию и обеспечивает бесшовный переход между различными СУБД.

Функция sql.sqltranslate(dbtype) переводит SQL-запрос. sql — это исходный запрос, а dbtype — тип целевой базы данных. Функция поддерживает только определённые SQL-функции (список см. в справочнике esProc). Неподдерживаемые функции не будут преобразованы.

Миграция SQL в среде IDE SPL

Теперь рассмотрим миграцию SQL в среде IDE SPL. Преобразуем запрос SELECT EID, NAME, BIRTHDAY, ADDDAYS(BIRTHDAY,10) DAY10 FROM EMP в синтаксис, совместимый с другими базами данных.

Функция ADDDAYS успешно преобразована для разных баз данных, демонстрируя успешную межбазовую миграцию SQL.

Дополнительные примеры

Пример 1: Например, добавление 10 к значению месяца

SELECT EID, NAME, BIRTHDAY, ADDMONTHS(BIRTHDAY,10) DAY10 FROM EMP

Переведите этот оператор SQL в синтаксис для разных баз данных:

ORACLE:

SELECT EID, NAME, BIRTHDAY, BIRTHDAY+NUMTOYMINTERVAL(10,'MONTH') DAY10 FROM EMP

SQLSVR:

SELECT EID, NAME, BIRTHDAY, DATEADD(MM,10,BIRTHDAY) DAY10 FROM EMP

DB2:

SELECT EID, NAME, BIRTHDAY, BIRTHDAY+10 MONTHS DAY10 FROM EMP

MySQL:

SELECT EID, NAME, BIRTHDAY, BIRTHDAY+INTERVAL 10 MONTH DAY10 FROM EMP

POSTGRES:

SELECT EID, NAME, BIRTHDAY, BIRTHDAY+interval '10 months' DAY10 FROM EMP

Функция ADDMONTHS реализована по-разному в разных СУБД. В SQL Server используется DATEADD, в MySQL и PostgreSQL — прямое сложение, а Oracle комбинирует оба подхода.

Пример 2: Получить QUARTER

SELECT EID,AREA,QUARTER(ORDERDATE) QUA, AMOUNT FROM ORDERS

Преображение выражения в:

ORACLE:

SELECT EID,AREA,FLOOR((EXTRACT(MONTH FROM ORDERDATE)+2)/3) QUA, AMOUNT FROM ORDERS

SQLSVR:

SELECT EID,AREA,DATEPART(QQ,ORDERDATE) QUA, AMOUNT FROM ORDERS

POSTGRES:

SELECT EID,AREA,EXTRACT(QUARTER FROM ORDERDATE) QUA, AMOUNT FROM ORDERS

TERADATA:

SELECT EID,AREA,TD_QUARTER_OF_YEAR(ORDERDATE) QUA, AMOUNT FROM ORDERS

Функция QUARTER реализована в разных СУБД с вариациями в именовании и параметрах.

Пример 3: Преобразование типов

SELECT EID, NAME, DATETOCHAR(BIRTHDAY) FROM EMP

Перевод утверждение в:

ORACLE:

SELECT EID, NAME, TO_CHAR(BIRTHDAY,'YYYY-MM-DD HH:MI:SS') FROM EMP

SQLSVR:

SELECT EID, NAME, CONVERT(CHAR,BIRTHDAY,120) FROM EMP

DB2:

SELECT EID, NAME, TO_CHAR(BIRTHDAY,'YYYY-MM-DD HH:MI:SS') FROM EMP

MySQL:

SELECT EID, NAME, DATE_FORMAT(BIRTHDAY, '%Y-%m-%d %H:%i:%S) FROM EMP

POSTGRES:

SELECT EID, NAME, TO_CHAR(BIRTHDAY,'YYYY-MM-DD HH:MI:SS') FROM EMP

TERADATA:

SELECT EID, NAME, TO_CHAR(BIRTHDAY,'YYYY-MM-DD HH:MI:SS') FROM EMP

Функции преобразования типов также имеют несоответствия в именовании и синтаксисе между различными СУБД. Функция SPL sqltranslate() решает эту проблему.

Определение и расширение функций

Список поддерживаемых СУБД и функций находится в файле /com/scudata/dm/sql/function.xml внутри esproc-bin.jar.

<?xml version="1.0" encoding="utf-8"?>
<STANDARD>
    <FUNCTIONS type="FixParam">
        <FUNCTION name="ADDDAYS" paramcount="2" value="">
            <INFO dbtype="ORACLE" value="?1+NUMTODSINTERVAL(?2,'DAY')"></INFO>
            <INFO dbtype="SQLSVR" value="DATEADD(DD,?2,?1)"></INFO>
            <INFO dbtype="DB2" value="?1+?2 DAYS"></INFO>
            <INFO dbtype="MYSQL" value="?1+INTERVAL ?2 DAY"></INFO>
            <INFO dbtype="HSQL" value="DATEADD('dd', ?2, ?1)"></INFO>
            <INFO dbtype="TERADATA" value="?1+CAST(?2 AS INTERVAL DAY)"></INFO>
            <INFO dbtype="POSTGRES" value="?1+interval '?2 days'"></INFO>
            <INFO dbtype="ESPROC" value="elapse(?1,?2)"></INFO>
        </FUNCTION>
    </FUNCTIONS>
</STANDARD>

XML-файл function.xml описывает соответствие SQL-функций для разных СУБД. Раздел FUNCTIONS группирует функции по типу (type), например, FixParam для функций с фиксированным числом параметров. Каждый элемент FUNCTION определяет функцию (name, paramcount), а value — её значение по умолчанию (пустое значение означает отсутствие преобразования). Раздел INFO задаёт преобразование функции для каждой СУБД (dbtype, value), используя ? или ?n как плейсхолдеры для параметров. Если для данной СУБД в FUNCTION отсутствует INFO, функция не преобразуется. function.xml содержит не все функции; новые функции можно добавлять вручную, как показано на примере добавления функции DATEDIFF для вычисления разницы между датами.

<FUNCTION name="DATEDIFF" paramcount="2" value="">
    <INFO dbtype="ORACLE" value="?1-?2"></INFO>
    <INFO dbtype="SQLSVR" value="DATEDIFF(day,?1,?2)"></INFO>
    <INFO dbtype="MYSQL" value="DATEDIFF(?1,?2)"></INFO>
    <INFO dbtype="POSTGRES" value="?1-?2"></INFO>
    <INFO dbtype="ESPROC" value="interval(?2,?1)"></INFO>
</FUNCTION>

Поддержка новых СУБД добавляется путём создания новых узлов INFO с соответствующими настройками. Например, для добавления поддержки SQLite и реализации перевода функции вычисления разницы дат, достаточно добавить необходимые данные в узел INFO.

<FUNCTION name="DATEDIFF" paramcount="2" value="">
    <INFO dbtype="ORACLE" value="?1-?2"></INFO>
    <INFO dbtype="SQLSVR" value="DATEDIFF(day,?1,?2)"></INFO>
    <INFO dbtype="MYSQL" value="DATEDIFF(?1,?2)"></INFO>
    <INFO dbtype="POSTGRES" value="?1-?2"></INFO>
    <INFO dbtype="ESPROC" value="interval(?2,?1)"></INFO>
    <INFO dbtype="SQLite" value="JULIANDAY(?1) - JULIANDAY(?2)"></INFO>
</FUNCTION>

Фиксированное число параметров

В отличие от функций с фиксированным числом параметров, некоторые функции, такие как конкатенация строк, CASE WHEN или поиск первого ненулевого значения, требуют динамического количества аргументов. SPL поддерживает это, используя тип AnyParam в узле FUNCTIONS.

<FUNCTIONS type="AnyParam">
    <FUNCTION classname="com.scudata.dm.sql.simple.Case" name="case">
        <INFO dbtype="ESPROC" classname="com.scudata.dm.sql.simple.Case"></INFO>
    </FUNCTION>
    <FUNCTION classname="com.scudata.dm.sql.simple.Coalesce" name="coalesce">
        <INFO dbtype="ESPROC" classname="com.scudata.dm.sql.simple.Coalesce"></INFO>
    </FUNCTION>
    <FUNCTION classname="com.scudata.dm.sql.simple.Concat" name="concat">
        <INFO dbtype="ESPROC" classname="com.scudata.dm.sql.simple.Concat"></INFO>
    </FUNCTION>
</FUNCTIONS>

Необходимо создать классы Java для определенных функций в каждой базе данных. Например, чтобы добавить поддержку строковой функции Oracle CONCAT, мы можем написать следующий код:

public class Concat implements IFunction
{
    public String getFormula(String[] params)
    {
        StringBuffer sb = new StringBuffer();
        for(int i = 0, len = params.length; i < len; i++) {
            if(params[i].isEmpty()) {
                throw new RQException("Concat function parameter cannot be empty");
            }
            if(i > 0) {
                sb.append(" || ");
            }
            sb.append(params[i]);
        }
        return sb.toString();
    }
}

Необходимо скомпилировать этот фрагмент кода и поместить его внутри пути /com/scudata/dm/sql/oracle внутри esproc-bin.jar.

Затем настройте класс трансляции, соответствующий Oracle в function.xml.

<FUNCTIONS type="AnyParam">
    <FUNCTION classname="com.scudata.dm.sql.simple.Case" name="case">
        <INFO dbtype="ESPROC" classname="com.scudata.dm.sql.simple.Case"></INFO>
    </FUNCTION>
    <FUNCTION classname="com.scudata.dm.sql.simple.Coalesce" name="coalesce">
        <INFO dbtype="ESPROC" classname="com.scudata.dm.sql.simple.Coalesce"></INFO>
    </FUNCTION>
    <FUNCTION classname="com.scudata.dm.sql.simple.Concat" name="concat">
        <INFO dbtype="ESPROC" classname="com.scudata.dm.sql.simple.Concat"></INFO>
        <INFO dbtype="ORACLE" classname="com.scudata.dm.sql.oracle.Concat"></INFO>**
    </FUNCTION>
</FUNCTIONS>

Перезапустите IDE после изменения jar-файла.

Функция sqltranslate() демонстрирует различия в синтаксисе между СУБД: например, конкатенация трёх параметров в Oracle использует ||, а в esProc — -. Мы рассмотрели настройку sqltranslate(), добавление функций и СУБД, включая функции с динамическим числом параметров.

Интеграция с приложениями

Интеграция SPL проста: добавить esproc-bin-xxxx.jar, icu4j-60.3.jar в [installation directory]\esProc\lib и скопировать raqsoftConfig.xml из [installation directory]\esProc\config в classpath приложения. raqsoftConfig.xml — обязательный файл конфигурации.

Работа с одной базой данных

Использование 1 — Использовать только трансляцию SQL

Простейший способ использования — трансляция SQL-запросов с помощью sqltranslate() и последующее выполнение. Функция com.scudata.dm.sql.SQLUtil.translate обеспечивает эту трансляцию.

String sql = "select name, birthday, adddays(birthday,10) day10 from emp";
sql = com.scudata.dm.sql.SQLUtil.translate(sql, "MYSQL");

Хотя SPL рекомендует использовать JDBC, прямое использование API практичнее для простых преобразований SQL-запросов. Для обеспечения прозрачности миграции между базами данных, тип базы данных хранится в конфигурационном файле (например, dbconfig.properties), исключая необходимость переписывания или перекомпиляции кода при смене СУБД. Пример файла dbconfig.properties для MySQL приведён ниже.

database.type=MYSQL

Затем мы можем инкапсулировать метод перевода, который вызывает API SPL для реализации перевода SQL.

public static String translateSQL(String sql) {
    String dbType = null;
    try (InputStream input = SQLTranslator.class.getClassLoader().getResourceAsStream("dbconfig.properties")) {
        Properties prop = new Properties();
        if (input == null) {
            System.out.println("Sorry, unable to find dbconfig.properties");
            return null;
        }
        prop.load(input);
        dbType = prop.getProperty("database.type");
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    return SQLUtil.translate(sql, dbType);
}

Перевод SQL выполняется внешней функцией. В основной программе достаточно добавить вызов этой функции (sql = translateSQL) — остальной код (настройка параметров, выполнение запроса и обработка результатов) остаётся без изменений.

Использование 2 – прозрачное выполнение SQL

Первый метод, использующий `sqltranslate()` для каждого запроса, неэффективен при большом количестве SQL-запросов и использует нерекомендованные интерфейсы. Альтернативный подход — прозрачное выполнение SQL через SPL, используя стандартный JDBC. При этом изменение базы данных требует лишь настройки драйвера и URL в SPL, сам код приложения остаётся неизменным.

public static void main(String[] args) {
    String driver = "com.esproc.jdbc.InternalDriver";
    String url = "jdbc:esproc:local://";
    try {
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url);
        String sql = "SELECT orderid, employeeid, adddays(orderdate,10) day10,amount "
            + "FROM orders WHERE employeeid > ? AND amount > ?";
        PreparedStatement st = conn.prepareStatement(sql);
        st.setObject(1,"506");
        st.setObject(2,9900);
        ResultSet rs = st.executeQuery();
        while (rs.next()) {
            String employeeid = rs.getString("employeeid");
            System.out.print(employeeid+",");
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Механизм SQL-трансляции основан на шлюзе JDBC в SPL. Предварительно настроенный SPL-скрипт (например, gateway.splx) обрабатывает и выполняет все SQL-запросы, передаваемые ему, автоматизируя как трансляцию, так и выполнение. Настройка этого шлюза осуществляется в файле raqsoftConfig.xml через узел JDBC.

<JDBC>
    <load>Runtime,Server</load>
    <gateway>gateway.splx</gateway>
</JDBC>

Script шлюза требуются два параметра: один — sql для получения операторов SQL, а другой — args для получения параметров, которые передаются из JDBC в SQL и используются в операторе SQL.

Для получения нескольких параметров оператора SQL необходимо отметить опцию ниже «Последний параметр является динамическим параметром (The last parameter is a dinamic parameter)».

Вот сценарий:

A1 оценивает, существует ли переменная dbName. Если ее нет, вызовите скрипт инициализации initGlobalVars.splx в B1:

Этот скрипт считывает имя источника данных и тип базы данных из файла конфигурации и помещает их в глобальные переменные dbType и dbName с помощью функции env().

Вот содержимое файла конфигурации dbconfig.properties:

database.type=MYSQL
database.name=MYDATASOURCE

Метод A2 выполняет трансляцию SQL-запроса (описано ранее). A3 определяет количество параметров, а B3 конкатенирует их в одну строку (например, для двух параметров). A4 устанавливает соединение с источником данных, настроенным в raqsoftConfig.xml путём добавления информации о подключении в узел DB. Поддержка нескольких источников данных реализована.

<DB name="MYDATASOURCE">
    <property name="url" value="jdbc:mysql://127.0.0.1:3306/mydb?useCursorFetch=true"></property>
    <property name="driver" value="com.mysql.jdbc.Driver"></property>
    <property name="type" value="10"></property>
    <property name="user" value="root"></property>
    <property name="password" value="root"></property>
    <property name="batchSize" value="0"></property>
    <property name="autoConnect" value="false"></property>
    <property name="useSchema" value="false"></property>
    <property name="addTilde" value="false"></property>
    <property name="caseSentence" value="false"></property>
</DB>

A5 определяет, является ли SQL-запрос оператором SELECT. Так как запросы SELECT (DQL) и остальные (DML) обрабатываются по-разному, необходимо разделение. Для SELECT-запросов (B5) используется функция db.query() для получения данных с последующим закрытием соединения (@x). В случае других типов запросов (A6) применяется db.execute(). Простота сценария и интерпретируемая природа SPL позволяют изменять его без перезапуска приложения, обеспечивая поддержку горячей замены. Этот же механизм подходит и для операторов DML, таких как UPDATE.

public static void main(String[] args) {
    String driver = "com.esproc.jdbc.InternalDriver";
    String url = "jdbc:esproc:local://";
    try {
        Class.forName(driver);
        Connection conn = DriverManager.getConnection(url);
        String sql = "update orders set customername = ? where orderid = ? ";
        PreparedStatement st = conn.prepareStatement(sql);
        st.setObject(1,"PTCAG001");
        st.setObject(2,"1");
        st.executeUpdate();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Работа с несколькими базами данных

Как нам следует поступать в ситуациях, когда приложение использует несколько баз данных?

Использование 1 - Использовать только преобразование SQL

Давайте сначала рассмотрим использование только для перевода.

Нам все равно нужно сохранить имя и тип источника данных в файле конфигурации. Добавьте следующее в файл dbconfig.properties:

database.oracleds.type=ORACLE
database.mysqlds.type=MYSQL
database.pgds.type=POSTGRESQL

Средняя часть строки перед знаком равенства обозначает название источника данных, например oracleds, а часть справа от него указывает тип источника данных, например ORACLE. Поскольку существует несколько баз данных, нам нужно искать типы в соответствии с названиями источников данных.

Напишите метод перевода, выполните поиск типа по названию источника данных, загрузите файл конфигурации для получения свойств и переведите инструкции SQL:

public static String translateSQL(String sql, String dataSourceName) {
    try (InputStream input = SQLTranslator.class.getClassLoader().getResourceAsStream("dbsconfig.properties")) {
        Properties prop = new Properties();
        if (input == null) {
            System.out.println("Sorry, unable to find dbsconfig.properties");
            return null;
        }
        prop.load(input);
        String dbType = prop.getProperty("database." + dataSourceName.toLowerCase() + ".type");
        if (dbType == null) {
            throw new RuntimeException("Data source " + dataSourceName + " not configured in the configuration file.");
        }
        return SQLUtil.translate(sql, dbType);
    } catch (Exception ex) {
        ex.printStackTrace();
        return null;
    }
}

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

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

public static void main(String[] args) {
    String sql = "SELECT orderid, employeeid, adddays(orderdate,10) day10,amount "
    + "FROM orders WHERE employeeid > ? AND amount > ?";
    String dataSourceName = "mysqlds"; 
    String translatedSQL = translateSQL(sql, dataSourceName);
    System.out.println("Translated SQL: " + translatedSQL);
    ……
}

Использование 2 – Переведите SPL-скрипт и выполните SQL

Ранее мы обсуждали преимущества и недостатки использования только перевода. Теперь давайте рассмотрим использование SPL gateway для перевода и выполнения SQL-инструкций.

public static void main(String[] args) {
    String driver = "com.esproc.jdbc.InternalDriver";
    String url = "jdbc:esproc:local://";
    try {
        Class.forName(driver);
        String mysqlDsName = "mysqlds";
        Connection mysqlConn = DriverManager.getConnection(url);
        String setDS = "setds "+ mysqlDsName;
        PreparedStatement setst = mysqlConn.prepareStatement(setDS);
        setst.execute();
        String sql = "SELECT orderid, employeeid, adddays(orderdate,10) day10,amount "
            + "FROM orders WHERE employeeid > ? AND amount > ?";
        PreparedStatement st = mysqlConn.prepareStatement(sql);
        st.setObject(1, "506");
        st.setObject(2, 9900);
        ResultSet rs = st.executeQuery();
        while (rs.next()) {
            String employeeid = rs.getString("employeeid");
            System.out.print(employeeid + ",");
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

Здесь мы устанавливаем соединения с разными источниками данных по отдельности, с дополнительным шагом для задания имени источника данных. Синтаксический анализ источника данных выполняется в скрипте gateway, в то время как остальная часть выполнения SQL полностью соответствует исходной программе.

Параметры в скрипте gateway полностью совпадают с параметрами в предыдущем скрипте single database gateway, где параметр sql получает инструкции SQL, а параметр args получает параметры SQL.

Ниже приведено содержимое скрипта шлюза gateway.splx:

Сценарий шлюза для работы с несколькими базами данных также включает в себя процесс настройки имени источника данных.

Сценарий инициализации initGlobalVars Multi.splx, вызываемый B1, считывает файл конфигурации:

И получает следующий результат:

A3 получает от программы параметр для установки источника данных, который задается как setds mysqld. Если он начинается с setds, B3 присвоит имя источника данных переменной задания dsName. Область действия переменной задания - то же соединение. Затем все инструкции SQL в этом источнике данных могут быть запущены напрямую. Аналогично, B4 выполняет поиск типа базы данных в соответствии со списком источников данных dbs и присваивает его переменной задания DbType.

Начиная с версии A6, скрипт работает так же, как и в версии с единой базой данных, и мы не будем объяснять это снова.

Этот скрипт шлюза по-прежнему может обрабатывать все инструкции SQL и обеспечивает плавную миграцию.

Итоги

Выше описано все, что нам нужно знать о миграции SQL в SPL. С SPL переключение базы данных больше не требует изменения кода, и может быть достигнута плавная миграция.

Конечно, возможности SPL значительно расширяются. Он также поддерживает параллельное выполнение поиска данных SQL, упрощает запросы к разным базам данных, позволяет выполнять смешанные вычисления между базами данных и источниками, не относящимися к базам данных, и использует свои вычислительные мощности для оптимизации производительности SQL. Мы будем рассматривать эти темы в будущих обсуждениях по очереди.

SPL теперь доступен с открытым исходным кодом. Вы можете получить исходный код на GitHub в разделе https://github.com/SPLWare/esProc.

Источник:

#Oracle #SQL #MySQL #Data Science
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу