Внешняя база данных - второе соединение, запросы, ORM
Подключаем чужую базу рядом с основной: соединение в настройках, запросы, своя сущность ORM, кэш и поведение при отказе.
Механика
Все подключения платформа описывает в одном месте - секции соединений файла настроек. Основное называется по умолчанию, дополнительные добавляют рядом под своими именами, и никакой разницы в способе обращения к ним нет.
Драйвер выбирается классом соединения. MySQL, PostgreSQL, MS SQL и Oracle - это разные классы ядра, а прикладной код работает с ними одинаково, через общий интерфейс соединения.
Соединение берут методом ядра, а не создают драйвером вручную. Так работают пул соединений, отложенное подключение и корректное закрытие, а конфигурация остаётся в одном месте.
Режим соединения задают числом, и для внешней базы он почти всегда один. Отложенное подключение устанавливается только при первом запросе - это разумное значение по умолчанию для внешней базы, к которой обращается не каждая страница.
Секцию соединений закрывают признаком только для чтения. Он запрещает менять настройки подключения во время работы скрипта, и это единственная защита от случайной подмены реквизитов чужим кодом.
Сущность ORM умеет жить на другом соединении, а не только на основном. Достаточно объявить имя соединения в классе таблицы - дальше выборки, фильтры и постраничная навигация работают привычным образом.
Внешняя база - чужая ответственность, и это меняет требования к коду. Схема поменяется без предупреждения, доступ пропадёт в неудобный момент, а скорость ответа вам не подконтрольна.
Шаги
- Получить доступ к чужой базе, желательно с правами только на чтение.
- Описать соединение в секции настроек рядом с основным подключением.
- Проверить соединение отдельным скриптом до написания прикладного кода.
- Завести сущность ORM на нужной таблице или писать запросы через соединение.
- Обернуть обращения кэшем и обработкой отказа чужой базы.
- Договориться о схеме и предупреждениях об её изменении.
Код
Описываем второе соединение:
'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 владельца системы.
Как не превратить интеграцию в тормоз?
Забирать данные одной выборкой вместо запроса в цикле, кэшировать результат на минуты и выносить тяжёлые выгрузки в фоновое задание. Скорость чужой базы вам не подконтрольна.
Смежное
- Свои таблицы на ORM - оглавление подтемы
- Код, совместимый с PostgreSQL: кавычки, функции, индексы - когда базы разного типа рядом
- Своя таблица на ORM: сущность, запросы, изменение структуры - та же сущность на своей базе
- Связи между таблицами ORM: ссылки, выборка, удаление - почему связи не работают между соединениями
- Вызов внешнего сервиса из кода: таймауты, повторы, журнал - альтернатива прямому доступу к базе
- База данных: продвинутое - устройство соединений и бэкендов
- Ядро D7 - устройство ядра целиком