Синхронизация данных из внешних систем
Импорт данных из внешних систем и их последующая синхронизация – не самая приятная работа из-за большого количества нюансов. А если количество источников больше одного, становится ещё веселее.
В самом начале работы над Zenky (облачная eCommerce-платформа) было понятно, что наполнение каталога интернет-магазина только из админки – плохая идея. Большое количество магазинов работают с 1С, общепит использует решения вида Frontpad или Iiko, каждая из этих систем имеет свой каталог товаров или блюд, который крайне желательно переиспользовать на сайте и в мобильных приложениях заведений.
Помимо этих трёх систем есть ещё огромное разнообразие похожих, но с очень непохожими форматами каталогов.
Проблема форматов
Как только проект решает сделать API для импорта данных из внешних источников (будь это загрузка CSV файла или работа с внешними API), сразу же возникает очевидная проблема – у каждого источника своё видение на формат данных и с этим надо как-то работать.
1С использует CommerceML для выгрузки данных в тот же 1C-Bitrix – это XML файлы с описанием групп (категорий), товаров, товарных предложений (офферы), характеристик, складов, типов цен и т.д. Из-за широкого использования 1С в ритейле, интернет-магазинам (и тем более eCommerce платформам) крайне критично иметь поддержку этого формата.
Упомянутые выше Iiko и Frontpad – тоже популярные PoS (Point of Service) системы в общепите и не только. Свой стандарт они не придумывали, но их API разительно отличается друг от друга (как по формату, так и по количеству полезной информации).
Разумеется, в разнице форматов как таковой нет ничего плохого, но только до того момента, когда надо придумать как импортировать их в систему, формат данных которой тоже отличается.
Импорт всего в одном месте
Самое быстрое решение – сделать отдельные импортеры для каждого сервиса, доступные по разным URL. Внутри каждого импортера можно сделать обработку получаемых данных, конвертирование и последующее сохранение в базу данных.
Несмотря на очевидные неудобства, рабочее решение лучше нерабочего. Если все импортеры подчиняются какому-то общему контракту (интерфейсу), постоянно актуализируются и не мешают остальной кодовой базе – значит такая реализация подходит проекту.
Но если добавление новых внешних источников становится всё болезненней, можно попробовать другой подход.
Единый импорт в одном месте
Вместо импорта всего в одном месте (все внешние источники сидят в одной кодовой базе) можно сделать единый импорт в одном месте (все данные импортируются через единый API).
Сразу главное уточнение – это решение может потребовать выноса большого количества кода из одной кодовой базы в другую. Это подходит далеко не всем проектам по разным причинам.
Идея заключается в том, что в кодовой базе основного проекта можно сделать только один API импорта данных в одном формате, который подходит проекту. В этом случае импортеры переезжают в свои отдельные небольшие приложения и выступают в качестве обменников.
Обменники
Обменник – это отдельное приложение, которое работает только с одним источников данных. Он принимает исходные данные в формате источника (CommerceML, API сервиса), обрабатывает их, возможно, сохраняет в свою базу, а затем трансформирует их в тот формат, который принимает единый импортер главного проекта.
В случае двухсторонней синхронизации (получаем товары из сервиса, отправляем заказы в сервис) обменник может взять на себя обязанности по обратной отправке данных, что тоже снимает с основного проекта головную боль по поддержке кучи API-клиентов для разных сервисов.
Разумеется, такой подход имеет свои минусы – если импорт уже реализован, его нужно выносить в отдельное приложение. За такие обменники должен кто-то отвечать и постоянно поддерживать, увеличивается количество внутренних проектов.
Синхронизация данных
Допустим, с большим количеством источников разобрались – выносим всё в обменники. Что нужно учитывать при постоянной синхронизации данных?
Создавать или обновлять?
Во-первых, нужно устранить вопрос «создавать или обновлять?». Жизнь обменников очень сильно упростится, если им не надо будет постоянно решать этот вопрос и слать новые данные в один API, а обновлённые – в другой.
Импортер основного проекта может решить этот вопрос сопоставлением внешних идентификаторов с внутренними. К примеру, когда в импортер попадает новый товар, он может проверить, есть ли в таблице сопоставления переданный идентификатор товара. Если ID есть, значит товар существует и его нужно обновить (внутренний ID должен быть записан рядом с внешним), а если нет – создать товар и добавить сопоставление в таблицу:
CREATE TABLE `external_identifiers` (
`id` BIGINT(20) unsigned NOT NULL AUTO_INCREMENT,
`external_id` VARCHAR(255) NOT NULL,
`internal_id` BIGINT(20) unsigned NOT NULL,
`importer` VARCHAR(255) NOT NULL,
);
В этой таблице поле external_id
будет заполняться идентификатором из внешней системы (либо напрямую из
источника, либо из внутренней базы обменика), internal_id
– внутренним ID ресурса (товар, категория и т.д.),
а importer
– тип импортера, который создал запись (через это поле можно разрешить категориям и товарам иметь
одинаковые ID).
Этот подход решает ещё одну важную, но незаметную проблему – зависимость идентификаторов. К примеру, мы ждём
от обменника список товаров и у каждого товара есть поле category_id
, в котором должен быть передан ID
категории товара.
Импортер товаров не знает, была ли создана категория с переданным внешним ID или нет. Для него главное – получить внутренний ID категории и связать товар и категорию.
Допустим, список товаров был отправлен до того, как были импортированы категории. У всех category_id
нет
сопоставлений с внутренними ID категорий (т.к. их просто-напросто ещё нет в базе).
Импортер товаров, заметив, что сопоставления нет, может создать запись в этой таблице и присвоить новый внутренний ID внешней категории. Затем он использует этот внутренний ID, чтобы связать товар и категорию и закончит синхронизацию.
Когда придут категории, их импортер найдёт ранее сохранённый ID, проверит, что записи в таблице categories
с таким ID нет, и создаст категорию, используя его. После завершения импорта категорий все товары будут
связаны с категориями без дополнительных действий.
Конфликты ручных обновлений
Все импортированные данные, как правило, можно изменять вручную – через админку. Тут возникает проблема какие данные считать приоритетными – из внешней системы или отредактированные вручную.
У этой проблемы нет общего решения – всё зависит от каждого поля в каждом типе данных. Например, импорт товаров скорее всего должен перезаписывать цены (использовать цену из внешней системы, а не ту, которую изменили в админке), но импорт категорий по-идее не должен менять вложенность категорий на сайте.
Если у товаров есть опция «Скрыть товар на сайте» и во внешней системе есть похожее поле, скорее всего надо учитывать поле из админки, нежели значение из системы – магазин может захотеть отключить заказ конкретного товара на сайте, но продолжить продавать его в оффлайне.
В зависимости от реализации, такие поведения можно настраивать вручную (в конфиг файлах), либо давать возможность пользователю решать как лучше.