Перейти к содержимому

Внешняя база данных - второе соединение, запросы, ORM

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

Механика

Все подключения платформа описывает в одном месте - секции соединений файла настроек. Основное называется по умолчанию, дополнительные добавляют рядом под своими именами, и никакой разницы в способе обращения к ним нет.

Драйвер выбирается классом соединения. MySQL, PostgreSQL, MS SQL и Oracle - это разные классы ядра, а прикладной код работает с ними одинаково, через общий интерфейс соединения.

Соединение берут методом ядра, а не создают драйвером вручную. Так работают пул соединений, отложенное подключение и корректное закрытие, а конфигурация остаётся в одном месте.

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

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

Сущность ORM умеет жить на другом соединении, а не только на основном. Достаточно объявить имя соединения в классе таблицы - дальше выборки, фильтры и постраничная навигация работают привычным образом.

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

Шаги

  1. Получить доступ к чужой базе, желательно с правами только на чтение.
  2. Описать соединение в секции настроек рядом с основным подключением.
  3. Проверить соединение отдельным скриптом до написания прикладного кода.
  4. Завести сущность ORM на нужной таблице или писать запросы через соединение.
  5. Обернуть обращения кэшем и обработкой отказа чужой базы.
  6. Договориться о схеме и предупреждениях об её изменении.

Код

Описываем второе соединение:

/bitrix/.settings.php
'connections' => ['value' => [
'default' => [/* основное подключение сайта */],
'erp' => [
'className' => '\\Bitrix\\Main\\DB\\MysqliConnection',
'host' => '10.0.0.5', 'database' => 'erp',
'login' => 'site_ro', 'password' => '***',
'options' => 2, // отложенное: соединение при первом запросе
],
], 'readonly' => true], // настройки нельзя менять во время работы

Реквизиты чужой базы - такой же секрет, как пароль основной. Файл настроек не попадает в репозиторий, а пользователю выдают права только на чтение нужных таблиц: этого хватает почти всегда.

Берём соединение и делаем запрос:

$conn = \Bitrix\Main\Application::getConnection('erp');
$rows = $conn->query('SELECT id, code, amount FROM stock LIMIT 100')->fetchAll();
// имя соединения - единственное отличие от работы с основной базой

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

Экранируем значения запроса:

$helper = $conn->getSqlHelper();
$sql = "SELECT amount FROM stock WHERE code = '" . $helper->forSql($code) . "'";
$amount = $conn->queryScalar($sql);
// значения экранируют помощником этого соединения, а не основного

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

Заводим сущность ORM на чужой таблице:

class ErpStockTable extends \Bitrix\Main\ORM\Data\DataManager
{
public static function getConnectionName(): string { return 'erp'; }
public static function getTableName(): string { return 'stock'; }
public static function getMap(): array
{
return [
(new \Bitrix\Main\ORM\Fields\IntegerField('ID'))->configurePrimary(true),
new \Bitrix\Main\ORM\Fields\StringField('CODE'),
new \Bitrix\Main\ORM\Fields\FloatField('AMOUNT'),
];
}
}

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

Прямые запросы к чужой базе держат в одном классе или файле. Тогда переименование поля на стороне владельца системы правится в одном месте, а не по всем шаблонам и компонентам проекта.

Кэшируем ответ чужой базы:

$cache = \Bitrix\Main\Data\Cache::createInstance();
if ($cache->initCache(600, 'erp_stock_' . $code, '/vendor/erp')) {
$amount = $cache->getVars()['amount'];
} elseif ($cache->startDataCache()) {
$amount = ErpStockTable::getRow(['filter' => ['=CODE' => $code]])['AMOUNT'] ?? 0;
$cache->endDataCache(['amount' => $amount]);
}
// короткое время жизни: остатки устаревают быстрее, чем справочники

Переживаем отказ чужой базы:

try {
$amount = ErpStockTable::getRow(['filter' => ['=CODE' => $code]])['AMOUNT'] ?? null;
} catch (\Bitrix\Main\DB\ConnectionException $e) {
$amount = null; // страница обязана открыться и без этих данных
\Bitrix\Main\Diag\Debug::writeToFile($e->getMessage(), date('H:i:s'), 'erp.log');
}

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

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

Проверяем соединение отдельным скриптом:

// /local/tools/erp-check.php, запуск из консоли
$conn = \Bitrix\Main\Application::getConnection('erp');
var_dump($conn->queryScalar('SELECT 1'));
// проверка до прикладного кода экономит час на разборе «почему пусто»

Ограничения

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

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

Скорость ответа чужой базы вам не подконтрольна и меняется без предупреждения. Один медленный запрос к ней растягивает страницу целиком, поэтому обращения выносят в кэш, а тяжёлые - в фоновое задание.

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

Права только на чтение - не паранойя, а норма. Запись в чужую базу мимо её приложения ломает чужую бизнес-логику, и такие интеграции делают через её API, а не запросами.

Резервное копирование чужой базы - не ваша задача, но её отсутствие станет вашей проблемой. Об этом договариваются на берегу, вместе с тем, кто чинит доступ в выходные.

Типичные проблемы

Реквизиты чужой базы оказались в репозитории.

Соединение описали в файле, который версионируется. Настройки подключения держат в файле настроек вне репозитория.

Страница падает, когда чужой сервер недоступен.

Обращение к чужой базе не обёрнуто обработкой отказа соединения с ней. Блок с чужими данными должен исчезать, а не ронять страницу.

Витрина стала медленной без видимой причины.

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

Выборка возвращает мусор после обновления чужой системы.

Изменилась схема чужой базы: поле переименовано или сменило тип. Запросы к ней держат в одном месте, чтобы правка была одной.

Попытка соединить чужую таблицу с инфоблоком не работает.

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

Экранирование значений работает неверно.

Использован помощник основного соединения, а у чужой СУБД другой диалект. Помощника берут у того соединения, в которое идёт запрос.

Частые вопросы

Как подключить вторую базу к сайту?

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

Можно ли работать с чужой таблицей через ORM?

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

Что делать, если внешняя база недоступна?

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

Нужны ли права на запись в чужую базу?

Как правило нет: чтение закрывает большинство задач, а запись мимо чужого приложения ломает его логику. Если писать всё же нужно, это делают через API владельца системы.

Как не превратить интеграцию в тормоз?

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

Смежное

Первоисточник