Синхронизация данных из внешних систем

Импорт данных из внешних систем и их последующая синхронизация – не самая приятная работа из-за большого количества нюансов. А если количество источников больше одного, становится ещё веселее.

В самом начале работы над 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 нет, и создаст категорию, используя его. После завершения импорта категорий все товары будут связаны с категориями без дополнительных действий.

Конфликты ручных обновлений

Все импортированные данные, как правило, можно изменять вручную – через админку. Тут возникает проблема какие данные считать приоритетными – из внешней системы или отредактированные вручную.

У этой проблемы нет общего решения – всё зависит от каждого поля в каждом типе данных. Например, импорт товаров скорее всего должен перезаписывать цены (использовать цену из внешней системы, а не ту, которую изменили в админке), но импорт категорий по-идее не должен менять вложенность категорий на сайте.

Если у товаров есть опция «Скрыть товар на сайте» и во внешней системе есть похожее поле, скорее всего надо учитывать поле из админки, нежели значение из системы – магазин может захотеть отключить заказ конкретного товара на сайте, но продолжить продавать его в оффлайне.

В зависимости от реализации, такие поведения можно настраивать вручную (в конфиг файлах), либо давать возможность пользователю решать как лучше.