Кое-что об импорте

Этот текст не является готовым рецептом. Мы рассмотрим несколько аспектов, связанных с загрузкой данных в modx. Принципиально нового ничего не изобретено, просто собран в одном тексте некоторый опыт, полученный при работе над различными проектами.

1. Постановка задачи


Есть сайт, разработка которого ведется на modx. Есть какие-то данные, которые требуется загрузить в виде каталога или набора ресурсов в БД сайта. Эти данные могут находиться в файле, в базе 1С, на другом сайте. Выглядеть эти данные могут как html, xml, csv, xls(x) etc.

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

Замечание: мы не будем рассматривать готовые решения для импорта товаров в магазины, сделанные на modx. Shopkeeper и Minishop предоставляют такие инструменты, которые позволяют решить задачу для небольших наборов данных. Если вам достаточно этих инструментов, нет никакой необходимости что-то уложнять.



2. Подготовка modx


Итак, у нас есть сайт на modx, произвольной структуры и вложенности. Для определенности я буду называть ресурсы, которые нужно импортировать на сайт, товарами и категориями. Но это могут быть, например, рубрики и статьи или жанры и книги.

Первое, с чем будет необходимо определиться — структура категорий и товаров в дереве ресурсов. Второе — в сторонних данных (которые будем называть данными донора) нам очень важно найти какое-то уникальное поле (или два поля), которое однозначно позволит отличить товар от категории и от другого товара.

Напомню, нам потребуется делать импорт периодически. А следовательно, глядя на одну строку в данных, полученных от донора, мы должны сразу понять, есть ли у нас в БД уже сохраненные ранее товар или категория, которые представлены в этой строке. Если нет — создаем новую запись. Если уже есть — обновляем данные таким способом, который соответствует поставленной задаче (например, обновляется только цена товара или дата последнего обновления)

Поскольку прозреть умственным усилием все возможные подводные камни при сохранении данных в БД невозможно, даже при наличии большого опыта легко что-то забыть или проморгать, можно предложить такую схему работы: вручную создаются в дереве ресурсов 2-3 категории и в каждой 1-2 товара. Если вы работаете с незнакомым форматом хранения данных (например, вы новичок в modx, или впервые работаете с конкретным «магазинным» расширением modx), очень полезным будет поменять данные в БД и посмотреть, как при этом будут меняться данные товаров и категорий в админке modx. Из личного опыта: при работе над импортом товаров в minishop не имел представления, как работает поле uri в таблице site_content для Revo. Поведение админки при неверном значении этого поля было не слишком адекватным.

Итак, мы точно знаем как именно нужно записать данные в таблицы modx. Мы определились, что пишется в таблицу site_content и с какими значениями parent/template (а для Рево еще и type/class_key/context_key) у нас будут писаться товары и категории. Мы точно знаем, какие TV и в каком виде должны сохраняться в site_tmplvar_contentvalues. Если у нас стоят еще какие-то расширения, которые хранят данные в БД, формат хранения их данных мы тоже должны изучить и понимать.

3. Подготовка данных донора


Параллельно с изучением структуры хранения данных в БД мы должны также уделить время анализу данных донора. В идеале, мы должны иметь дело с файлом с данными в виде строк, одна строка — один ресурс в modx (если структура сайта не требует иного подхода). На что имеет смысл обратить внимание при анализе:

  • — формат файла
  • — наличие уникального поля или уникального сочетания полей
  • — кодировка
  • — полнота данных
  • — «типографичность» (формат «ячеек»)
  • — размер (кол-во строк)
  • — источник/место хранения файла
  • — необходимость и наличие возможности для импорта изображений

Слишком подробно расписывать методику работы с данными донора нет смысла, вариантов может быть очень много. Главное: для импорта нам нужно получить данные из каждой строки файла в виде какого-то массива, в котором мы гарантированно будем иметь возможность получить из определенного элемента массива определенную строку для записи в какое-то поле в modx. В некоторых случаях в файлах донора могут храниться не текстовые значения, а какие-то отдельные id (например, id производителя или id страны). Это означает, что перед началом импорта нам необходимо будет иметь под рукой готовые списки id-название для тех данных, которые мы хотим записывать в виде названий.

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

Как уже упоминалось выше, необходимо (без этого импорт будет сделать практически невозможно) однозначно идентифицировать каждую строку. Имейте в виду, что большая часть данных при повторном импорте может измениться. Если донор предоставляет в своих данных уникальное поле, например id — не поленитесь убедиться, что этот id не поменяется со временем для этого товара. Если уникального поля в данных нет, можно использовать сочетание нескольких полей для проверки; например название + размер + цвет + производитель + артикул. Заранее определимся, в каком поле в БД modx мы будем хранить это уникальное поле из файла (обычно используется что-то, не очень нужное для вывода на фронте, например description или longtitle)

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

Особняком стоит тема импорта данных со сторонних сайтов, то, что принято называть парсерами. Та часть парсинга, которая касается записи данных в modx вполне укладывается в тему данной заметки. Сам же парсинг html тут рассматриваться не будет.

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

4. Резервное копирование


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

Тема резервного копирования с одной стороны простая — бекапы надо делать всегда. С другой стороны, при проектировании работы импорта сложно предугадать все возможные последствия сбоя (иногда просто сложно даже вспомнить о том, что нужно их предугадывать). Дело в том, что в идеальном случае требуется хранить бекапы разной «свежести» на тот случай, если через неделю после «плохого» импорта все бекапы уже перезаписались плохими данными. Мы, пожалуй удержимся от соблазна расписать тут теорию и ограничимся банальным «все зависит от проекта». Иногда бывает так, что проектирование и написание механизма хранения/восстановления бекапов просто не вписываются в сроки и бюджеты. А иногда проще в случае сбоя быстро исправить код в импорте и провести его повторно.

Итак, пишем сам импорт.

5. Способ «в лоб»


Первое, что нужно будет решить — будет ли наш импорт работать как часть админки (модуль/компонент) или как отдельный скрипт. У обоих вариантов есть достоинства и неудобства. С одной стороны, нам хорошо будет работать в рамках админки modx, имея под рукой уже подключенные API. С другой стороны, если потребуется запускать импорт по расписанию — modx не сможет нам предоставить необходимых инструментов.

Если нет задачи запуска импорта по расписанию, но есть необходимость периодически возвращаться к импортированию/обновлению данных — лучше использовать встроенное в админку решение. Небольшая форма с полем загрузки файла либо с полем для указания URL, где этот файл размещен и большая кнопка ИМПОРТИРОВАТЬ — вот и весь интерфейс.

На стороне сервера наш скрипт получает файл, разбирает его построчно в массивы с данными и используя API modx и какие-то вспомогательные библиотеки (modxAPI для Evo) сохраняет данные в БД. Логика следующая:

  • — вначале импортируем категории, если они есть в файле; список категорий и их id в БД modx храним в каком-то массиве, чтобы можно было быстро прописывать товарам/категориям parent
  • — когда мы получили очередную строку данных донора, проверяем в modx наличие товара/категории с таким значением уникального поля (или сочетанием полей), как в текущей строке; например мы решили хранить уникальное поле донора Артикул в поле longtitle — ищем в БД modx ресурс с шаблоном товара/категории, у которого longtitle равен значению Артикула из файла
  • — если товар/категория в предыдущем шаге найдены, делаем update для этой записи; если товар/категория не найдены, делаем insert
  • — из предыдущего шага получаем id ресурса modx и используем его для добавления/обновления значений TV

Два последних шага немного изменятся, если вы будете использовать библиотеки для сохранения ресурсов modx, которые (библиотеки) позволяют одним заходом сохранить и поля ресурса и значения TV для этого ресурса.

Как нетрудно заметить, способ «в лоб» не представляет никакой сложности. Импорт пишется быстро, главное прописать в коде соответствие полей из файла донора полям в БД modx. Если количество импортируемых товаров сравнительно большое и скрипт отваливается по таймауту — добавляем set_time_limit(). Если не хватает памяти, добавляем памяти в настройках хостинга. В конце кода вызываем при необходимости очистку кеша modx и можно заниматься тестированием и отладкой.

6. Способ «бесконечной подгрузки»


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

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

Рассмотрим случай, когда нам требуется импортировать ощутимое кол-во товаров (несколько десятков тысяч), которые требуется «разложить» по размерам и цветам. Сама логика импорта по сравнению со способом «в лоб» усложнится, но проблема не только в этом. Мы хотим так же загрузить файл с данными донора и увидеть процесс выполнения импорта с какой-то информацией вида "Товар такой-то сохранен в БД с ID=123". При этом нам не хочется, чтобы скрипт отвалился по таймауту или чтобы в случае зависания нам пришлось начинать все с самого начала.

Логика работы импорта в этом случае может быть следующей:

  • — загружаем файл с данными донора
  • — в отдельной таблице import в БД записываем каждую строку из этого файла в качестве набора данных, подготовленных к записи в БД modx
  • — еще одна таблица в БД (next) или же отдельный файлик на сервере (next.txt) будет содержать id строки, которую мы должны в порядке очереди сохранить в modx из таблицы import; после записи данных мы указываем следующий id строки в next или next.txt
  • — в браузере у нас работает js скрипт, который отправляет запросы на сервер аяксом; при получении ответа от сервера скрипт проверяет, есть ли в ответе указание на то, что больше нечего импортировать; если такого указания нет — на сервер отправляется следующий запрос; кроме этого ответ сервера содержит лог работы импорта, который выводится в браузер

По идее, в этом месте стоило бы привести пример кода, но на его написание не очень хочется тратить время. Если у кого-то будет необходимость изучить пример такого кода, пишите, что-нибудь придумаем.

По сравнению со способом «в лоб» у данного способа есть дополнительные плюсы — возможность проводить довольно длительные импорты, при этом импорт можно возобновить в случае сбоя или еще как-то управлять процессом, если что-то пошло не так. Минус у этого способа тот же, что и в предыдущем способе — долго и не очень эффективно; кроме того много времени тратится на передачу данных по сети. На не самом плохом сервере импорт 30 тысяч строк занимал в зависимости от загруженности сервера от 30 минут до 2 часов.

7. Уровнем ниже


Теперь мы добрались до самой поставленной задачи. У нас есть донор, который по JSON API отдает 1300000 товаров. Не слишком много для заполненной БД, но для периодического импорта (данные обновляются дважды в сутки) — изрядная нагрузка на сервер. Попытка загрузить данные в БД вылилась в несколько дней наблюдения за выводом в консоли. Скорость работы совершенно неприемлемая, поэтому включаем тяжелую технику.

Сам импорт будет делаться консольным скриптом по расписанию. Всякие API modx откладываем в сторону и пишем в БД прямыми запросами (это решение спорное, возможно где-то можно и обойтись без этого). Чтобы не начудить с экранированием строк, была взята первая попавшаяся библиотека-обертка для mysqli, но это необязательно.

Логика работы следующая:

  • — в site_content оставляем только те ресурсы, которые будут отвечать за обычные страницы на сайте, не связанные напрямую с выводом товаров и каталога; после настройки этих ресурсов делаем копию таблицы в site_content_clean
  • — те же операции проделываем для site_tmplvar_contentvalues и делаем копию в site_tmplvar_contentvalues_clean
  • — очищаем таблицу ms2_products (работаем в Revo + minishop2; для Evo + Shopkeeper очищаем таблицы, связанные с хранением товаров «не в дереве»)
  • — импорт переписываем таким образом, чтобы он формировал локальный файл csv формата, в котором все данные уже разложены в нужном виде по нужным колонкам; строго говоря таких файлов будет несколько, по одному на каждую из таблиц в БД (modx_site_content_new.txt, modx_site_tmplvar_contentvalues_new.txt, ...)
  • — сам импорт начинается с того, что таблицы с суффиксом _clean копируются в таблицы с теми же названиями, но с суффиксом _new; в новых таблицах отключается (!) создание ключей командой вида (нужны права root)
  • myisamchk -r -q -k 0 /var/lib/mysql/DB_NAME/modx_site_content_new


Тут есть одна неприятная вещь — для корректного импорта нам надо заранее знать, какие id будут у товаров в modx_site_content, чтобы можно было заполнять файлы записями для TV и ms2. Когда мы просто импортируем данные, мы не указываем id и срабатывает auto increment в БД, мы можем получить id последней вставленной записи или заранее знать id записи для обновления. В случае, когда мы пишем данные в файл, нам придется самим создавать эти id, а значит важно не получить конфликт с уже существующими id в modx_site_content и остальных таблицах. Поэтому добавим еще один шаг:

  • — получаем максимальное значение id из modx_site_content_new и добавляем к нему 1
  • — далее происходит сам импорт данных донора в соответствующие файлы; файлы должны быть расположены в директории, которая доступна для чтения всем пользователям сервера
  • — когда файлы записаны, делаем следующее:

LOAD DATA INFILE "/path/to/modx_site_content_new.txt" REPLACE INTO TABLE modx_site_content_new FIELDS TERMINATED BY "," LINES TERMINATED BY "\n";


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

myisamchk -r -q -sort_buffer_size=50M /var/lib/mysql/DB_NAME/modx_site_content_new


  • — если все выглядит нормально, переносим таблицы, которые работали на сайте ранее в таблицы с суффиксом _old, а таблицы с суффиксом _new переносим в таблицы без суффикса
  • — чистим кеш при необходимости


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

Напоследок стоит сказать, что скорость такого импорта для 1300000 товаров + соответствующих TV повысилась не просто в разы, весь импорт занял 40 минут (вместо нескольких суток для обычного импорта по одному товару).

Пожалуй, это все, что хотелось сказать по данной теме.

0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.