342  /  382
Справочник

Миграция сторонних модулей

Просмотров: 9335
Дата последнего изменения: 12.07.2024
Роберт Басыров
Сложность урока:
4 уровень - сложно, требуется сосредоточиться, внимание деталям и точному следованию инструкции.
1
2
3
4
5
Внимание! Мигратор, описанный выше, создан для продукта, не для сторонних решений! Если сторонний модуль не будет поддерживать PostgreSQL, то мастер миграции отключит это модуль.

Миграция сторонних модулей

Модули из Битрикс24.Маркет будут работать. В этом маркетплейсе размещаются модули, работающие на REST методах и напрямую к БД не обращаются.

Модули из Маркетплейс 1С-Битрикс могут не заработать.

Если модуль сделан на Bitrix ORM то он не потребует доработок и заработает сразу. Если используемый модуль старый, либо написан не на ядре D7, то вероятность что он отвалится высока.

Если модуль написан на D7, но в нём используются прямые запросы к СУБД, какие-то специфичные функции, то он не заработает. В этом случае рекомендуется обратиться к разработчикам модуля для внесения ими изменений в код.

Разработчикам модулей следует адаптировать свои модули по рекомендациям ниже.

Адаптация модуля

Схема данных

Для квотирования идентификаторов в Postgres используются двойные кавычки.

Все неквотированные идентификаторы сначала преобразуются к НИЖНЕМУ регистру, а потом ищутся в метаданных базы.

Для совместимости к кодом «1С-Битрикс: Управление сайтом» в методе fetch ВСЕ колонки преобразуются к верхнему регистру.

Поэтому, если создана табличка:

create table test (ID int)
то по факту будет создана табличка test с колонкой id в нижнем регистре.

При выборке «из 1С-Битрикс: Управление сайтом»:

$rs = $DB->Query('SELECT ID from test');

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

Вот пример неправильного запроса:
$rs = $DB->Query('SELECT "ID" from test');

При выборке данных:

$ar = $rs->Fetch();

Метод Fetch автоматически преобразует колонку id в ID.

Если идентификатор нужно квотировать, то воспользуйтесь методом quote (SqlHelper или CDatabase). Для MySQL с регистром ничего делаться не будет, а вот для PostgresSQL идентификатор сначала будет приведён к нижнему регистру, а потом обрамлён двойными кавычками. Например:

Код
$sql = 'SELECT ' . $DB->quote('ID') . ' from ' . $DB->quote('test');

для MySql сформирует строку:

SELECT `ID` from `test`

а для Postgres:

SELECT "id" from "test"

Это будет рабочим кодом для обеих баз данных.

Типы колонок

Типы колонок
timestamp Замените на datetime с переделкой логики на PHP.
enum Замените на char т.к. в PostgreSQL будет создаваться лишний тип для колонки.
unsigned В PostgreSQL отсутствует такой модификатор типа поэтому для него будет увеличено количество разрядов хранения.

Примеры соответствия типов:

MySqlPostgreSQL
smallintsmallint
unsigned smallintint
intint
unsigned intint8

unsigned чаще всего используется не для ограничения хранения только положительных чисел (например результат ip2long), а как «дешёвый» способ удвоить диапазон хранимых данных. В таких случаях для упрощения поддержки и унификации типов в разных базах откажитесь от использования unsigned в MySql (с возможным увеличением разрядности хранения).

char Учтите, что PostgreSQL возвращает значение дополненное пробелами справа до указанной длины поля. Для char(1) это не составляет проблемы, однако если в char колонке вы храните данные переменной длины, то необходимо изменить тип хранения на varchar.

Имена индексов

В MySql имена индексов «локальные» по отношению к таблице и вполне возможна ситуация когда разные индексы у разных таблиц называются одинаково.

CREATE TABLE author (id int, name varchar(50), primary key (id), key ix_search(name));
CREATE TABLE book (id int, title varchar(50), primary key (id), key ix_search(title));

В PostgreSQL имена индексов должны быть уникальны в рамках схемы данных. Поэтому рекомендуется «составлять» имена индексов для удобства последующих манипуляций из:

  • префикса (ux_ - для уникальных, tx_ - для полнотекстовых и ix_ для остальных),
  • далее имя таблицы и разделитель «_»
  • далее имена столбцов разделённых «_»

Примечание: Надо учитывать, что максимальная длина имени индекса в PostgreSQL - 63 символа. Для таких случаев необходимо обрезать имя индекса и если оно будет конфликтовать с другим, то добавить числовой суффикс через «_».

Конвертация install.sql

$cd mymodule
$mkdir install/db/pgsql
$cp install/mysql/uninstall.sql install/pgsql/uninstall.sql
$php -f ../perfmon/tools/mysql_to_pgsql.php -- install/mysql/install.sql > install/pgsql/install.sql

Использование специфичного квотирования

Использование обратных кавычек для экранирования идентификаторов в MySql не подходит для PostgreSQL.

Такие использования необходимо переписать с использованием методов CDatabase::quote или SqlHelper::quote или убрать как ненужное/избыточное. При этом необходимо помнить, что названия таблиц всегда в нижнем регистре, а названия столбцов в верхнем.

Строковые литералы

В отличие от MySql в PostgresQL не допускается использование двойных кавычек для строковых литералов. Необходимо заменить их на одиночные кавычки.

Также MySql и PostgreSQL имеют различие в интерпретации обратных слешей в строковых литералах.

Например, запрос:

select '\Bitrix'

в MySql вернет "Bitrix", а в PostgreSQL — "\Bitrix".

А запрос:

select '\\Bitrix'

в MySql вернет "\Bitrix", а в PostgreSQL — "\\Bitrix".

Чтобы избежать этого, используйте функцию ForSql (SqlHelper или CDatabase):

$rs = $DB->Query("SELECT '" . $DB->ForSql("\\Bitrix") . "'")

Для обеих баз данных запрос вернет "\Bitrix".

Функции

Функции
ifnull() Замените на coalesce. Например:
SELECT ID,ifnull(NAME, '') AS NAME FROM b_user

Замените на:

SELECT ID,coalesce(NAME, '') AS NAME FROM b_user
mid() Замените на substr.
if() Замените на оператор case when. Например:
SELECT ID, if(TIMESTAMP_X > now() , 'red', 'green') AS STATUS FROM b_user

Замените на:

SELECT ID, case when TIMESTAMP_X > now() then 'red' else 'green' end AS STATUS FROM b_user
YEAR(), MONTH(), DAY() Замените на extract(... from ...). Например:
SELECT ID, YEAR(TIMESTAMP_X) AS A_YEAR FROM b_user

Замените на

SELECT ID, extract(YEAR FROM TIMESTAMP_X) AS A_YEAR FROM b_user
LOCATE() Замените на position(... in ...). Например:
SELECT ID, LOCATE(',',NAME) AS A_POS FROM b_user

Замените на

SELECT ID, POSITION(',' IN NAME) AS A_POS FROM b_user
get_lock() и release_lock() Перепишите с прямых запросов на методы DatabaseConnection
$lockName = 'mylock';
$connection = \Bitrix\Main\Application::getConnection();
if ($connection->lock($lockName))
{
	//....
	$connection->unlock($lockName);
}
date_add() и date_sub() Перепишите с прямых запросов на методы SqlHelper
$sql = 'SELECT ID, date_add(TIMESTAMP_X, interval 60 second) AS EXPIRATION_TIME FROM b_user';

Меняем на:

$connection = \Bitrix\Main\Application::getConnection();
$helper = $connection->getSqlHelper();

$sql = 'SELECT ID, ' . $helper->addSecondsToDateTime(60, 'TIMESTAMP_X') . ' AS EXPIRATION_TIME FROM b_user';
Для date_sub вызовите с отрицательным значением. Для date_add(..., interval day) используйте метод addDaysToDateTime.
date_format() Перепишите с использованием метода formatDate из \Bitrix\Main\Application::getConnection()->getSqlHelper().
concat() Перепишите с использованием метода getConcatFunction из \Bitrix\Main\Application::getConnection()->getSqlHelper().
date() Перепишите с использованием метода getCurrentDateFunction из \Bitrix\Main\Application::getConnection()->getSqlHelper().
rand() Перепишите с использованием метода getRandomFunction из \Bitrix\Main\Application::getConnection()->getSqlHelper().
sha1() Перепишите с использованием метода getSha1Function из \Bitrix\Main\Application::getConnection()->getSqlHelper().
group_concat() Избавьтесь от использования этой функции так как она потенциально может вернуть «битые» данные >The result is truncated to the maximum length that is given by the group_concat_max_len system variable, which has a default value of 1024.

Подробнее...
.

Запросы

INSERT IGNORE

Перепишите с прямых запросов на методы SqlHelper.

$sql = 'INSERT IGNORE INTO b_user_group (USER_ID, GROUP_ID, DATE_ACTIVE_FROM, DATE_ACTIVE_TO) VALUES (...)';
$sql = 'INSERT IGNORE INTO b_user_access_check (USER_ID, PROVIDER_ID) SELECT ...';

Замените на:

$connection = \Bitrix\Main\Application::getConnection();
$helper = $connection->getSqlHelper();

$sql = $helper->getInsertIgnore("b_user_group", "(USER_ID, GROUP_ID, DATE_ACTIVE_FROM, DATE_ACTIVE_TO)", "VALUES (...)");
$sql = $helper->getInsertIgnore("b_user_access_check", "(USER_ID, PROVIDER_ID)", "SELECT ...");

Различия в CDatabase::PrepareInsert и SqlHelper::prepareInsert

UPDATE IGNORE

Аналогичного по логике выполнения запроса в PostgreSQL нет. В принципе идея обновления значений первичного или альтернативного ключа не очень хорошая. Но запрос можно заменить на два. Это не вполне соответствует исходной логике, но обе БД будут вести себя одинаково.

INSERT IGNORE INTO ... SELECT ... WHERE
DELETE FROM ... WHERE
REPLACE INTO

Перепишите с прямых запросов на методы SqlHelper

$sql = 'REPLACE INTO b_module_table (KEY_ID, DATA) VALUES (...)';

Измените на:

$connection = \Bitrix\Main\Application::getConnection();
$helper = $connection->getSqlHelper();

$update = [
    'KEY_ID' => 1,
    'DATA' => 'a',
];
$merge = $helper->prepareMerge('b_module_table', ['KEY_ID'], $update, $update);
if ($merge[0])
{
    $connection->query($merge[0]);
}
//or another helper method
//$update - is a row in a rows array
foreach ($helper->prepareMergeMultiple('b_module_table', ['KEY_ID'], [$update]) as $sql)
{
    $connection->query($sql);
}

Различия в CDatabase::PrepareInsert и SqlHelper::prepareInsert

DELETE ... LIMIT

Перепишите с прямых запросов на методы SqlHelper:

$sql = 'DELETE FROM b_test WHERE ACTIVE = 'N' ORDER BY TIMESTAMP_X ASC LIMIT 50';

Измените на:

$connection = \Bitrix\Main\Application::getConnection();
$helper = $connection->getSqlHelper();

$sql = $helper->prepareDeleteLimit('b_test', ['ID'], "ACTIVE = 'N'", ['TIMESTAMP_X' => 'ASC'], 50);
  • Коррелированный UPDATE
  • Коррелированный DELETE

С этими запросами нужно быть осторожными, они очень специфичны. Рекомендуется их переписать.

Использование классов

  • MysqlCommonConnection
  • MssqlConnection
  • OracleConnection

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

Некоторые особенности

CDatabase::Add в табличку без автоинкремента:

$DB->Add("b_iblock_fields", $arAdd, array("DEFAULT_VALUE"));

Перепишите на прямой запрос:

$arInsert = $DB->PrepareInsert("b_iblock_fields", $arAdd);
$DB->Query("INSERT INTO b_iblock_fields (".$arInsert[0].") VALUES (".$arInsert[1].")");

Различия в CDatabase::PrepareInsert и SqlHelper::prepareInsert (и update тоже).

  • По разному обрабатываются поля типа дата/время.
  • CDatabase учитывает настройку часовых поясов, а SqlHelper - нет

Поэтому в методы хеспера передавайте объект даты/времени созданный методом \Bitrix\Main\Type\DateTime::createFromUserTime. Например:

$fields = [
	'DATE_REGISTER' => '01.01.2023 00:00:00',
];
print_r($DB->PrepareInsert('b_user', $fields));

$fields = [
	'DATE_REGISTER' => new \Bitrix\Main\Type\DateTime('01.01.2023 00:00:00'),
];
print_r(\Bitrix\Main\Application::getConnection()->getSqlHelper()->prepareInsert('b_user', $fields));

$fields = [
	'DATE_REGISTER' => \Bitrix\Main\Type\DateTime::createFromUserTime('01.01.2023 00:00:00'),
];
print_r(\Bitrix\Main\Application::getConnection()->getSqlHelper()->prepareInsert('b_user', $fields));

Выведет

Array
(
    [0] => `DATE_REGISTER`
    [1] => DATE_ADD('2023-01-01 00:00:00', INTERVAL 3600 SECOND)
)
Array
(
    [0] => `DATE_REGISTER`
    [1] => '2023-01-01 00:00:00'
    [2] => Array
        (
        )
)
Array
(
    [0] => `DATE_REGISTER`
    [1] => '2023-01-01 01:00:00'
    [2] => Array
        (
        )
+
−)

Использование двойных кавычек

В PostgreSQL не допускается для строковых литералов. Необходимо менять на одинарные кавычки.

6
Курсы разработаны в компании «1С-Битрикс»

Если вы нашли неточность в тексте, непонятное объяснение, пожалуйста, сообщите нам об этом в комментариях.
Развернуть комментарии