В 2016 году я опубликовал сборник рецептов и секретов ImpEx. Он писался под hybris 6.x и был в основном практическим: как удалить все записи типа, как написать условный блок, как подтянуть данные из чужой базы. Про то, почему ImpEx ведёт себя так, а не иначе, там почти ничего не было.
С тех пор ImpEx пережил переход на ServiceLayer Direct, обзавёлся распределённым режимом, многопоточностью по умолчанию и десятком новых свойств, которые описаны только комментариями в project.properties. При этом снаружи он выглядит ровно так же, как десять лет назад: заголовок, точки с запятой, квадратные скобки.
Поэтому я повторил с ImpEx то же, что с FlexibleSearch: проверил на 2211, что из известного про него верно и что движок делает с файлом.
Большинство рецептов 2016 года работает до сих пор, но не все, а некоторые работают не так, как я тогда думал.
ImpEx используют на каждом проекте, но в его код заглядывают редко. Документация описывает синтаксис заголовков и десяток модификаторов. На практике модификаторов больше трёх десятков, у CSV-слоя свои правила, ссылки разрешаются не так, как обычно думают, а от галки Enable code execution в HAC зависит, какие строки файла импортируются.
Общеизвестного здесь не будет. Синтаксис заголовков, режимы и основные модификаторы хорошо описаны в документации SAP, и пересказывать её я не стану. Почти всё, что ниже, — дополнения: то, чего в документации нет или что упомянуто одной строкой комментария в project.properties.
Про базы данных. SAP Commerce Cloud давно работает только в облаке SAP, и база там одна — Microsoft SQL Server. Платформа по-прежнему поддерживает HANA, Oracle, MySQL и HSQLDB, и код до сих пор различает их, поэтому они встречаются в тексте. Но на практике из этих различий важно одно: локальная HSQLDB ведёт себя не так, как SQL Server на проде.
И многие возможности, описанные ниже, исторические. Пользоваться ими я не предлагаю. Описываю их, чтобы на них случайно не наступить, а если уже наступили, быстро понять, во что вляпались.
О коде в этой статье
Все фрагменты Java ниже — иллюстративная реконструкция, а не исходный код платформы и не декомпиляция. Это мой пересказ поведения, восстановленного по наблюдениям: SQL в JDBC-логе, сообщениям в консольном логе, текстам исключений и экспериментам на работающей установке. Они показывают, что делает движок, а не то, как это реализовано. Имена, структура и детали будут отличаться от настоящей реализации, а в версиях новее той, что я тестировал (2211, JDK 21, HSQLDB), поведение могло измениться. Считайте каждый фрагмент схемой.А вот ImpEx-примеры, SQL и сообщения об ошибках — настоящие: каждый из них я прогнал через HAC и привожу вывод как есть.
Песочница, которая есть в каждой установке
Чтобы проверять поведение ImpEx, нужен тип, на котором не жалко экспериментировать: без интерцепторов, без обязательных атрибутов, без уникального ключа, который помешает создать дубликат. Такой тип есть в любой установке SAP Commerce — он объявлен прямо в core-items.xml:
TestItemType2 (extends TestItem)
foo, bar, fooBar : String
localizedFooBar : localized String
gender : Gender, не writable
+ всё, что унаследовано от TestItem: string, integer, date, …Все примеры ниже используют foo как ключ и bar как поле данных. У TestItem есть Jalo-логика, которая подменяет значения string и integer при чтении, поэтому проверять имеет смысл только foo и bar. Поле fooBar вычисляемое, так что foo,bar в нём — это нормально.
Читать результат удобнее всего из Groovy-консоли HAC:
import de.hybris.platform.servicelayer.search.FlexibleSearchQuery
def q = new FlexibleSearchQuery(
"SELECT {pk} FROM {TestItemType2} WHERE {foo} LIKE 'ih-%' ORDER BY {foo}")
flexibleSearchService.search(q).result.each {
println "[" + it.foo + "] bar=[" + it.bar?.replace("\n", "\\n") + "]"
}А убирать за собой — рецептом из статьи 2016 года, который по-прежнему работает:
$item=TestItemType2
REMOVE $item[batchmode=true];itemtype(code)[unique=true]
;$itemТогда я не написал, как это выполняется. В JDBC-логе один SELECT по типу, а затем удаление каждого объекта по отдельности, со всеми сопутствующими таблицами. На 47 объектах:
47 SELECT * FROM testitem WHERE PK=?
47 DELETE FROM testitem WHERE PK = ?
94 DELETE FROM testitemlp WHERE ITEMPK=?
96 DELETE FROM props WHERE ITEMPK=?
50 DELETE FROM aclentries WHERE ItemPK=?Для тысячи строк это терпимо, для миллиона — несколько миллионов запросов; тут ImpEx уже не подходит.
Для экспериментов с кодом (всё, что начинается с #%) в форме импорта HAC нужно включить галку Enable code execution. По умолчанию она выключена. Что от этого меняется для блоков if:, описано в разделе про код.
Собственный CSV-парсер ImpEx
ImpEx читает файл собственным CSVReader. В 2016 году я ссылался на RFC 4180 как на спецификацию. Это была ошибка: у этого парсера свои правила.
Кавычки не защищают пробелы
Значение в кавычках обрезается по краям точно так же, как без кавычек:
// иллюстрация
String value = quoted
? unescape(line.substring(start + 1, end).trim()) // ← trim и внутри кавычек
: line.substring(start, end).trim();Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Пример.
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-a1;" padded "[ih-a1] bar=[padded]Импортировать через ImpEx значение с ведущими или хвостовыми пробелами нельзя никак — только cell decorator'ом или транслятором, который допишет их обратно. Пробелы внутри многострочного значения при этом сохраняются: обрезаются только края всего значения.
Кавычка открывает цитирование только в начале ячейки
Посреди значения кавычка — обычный символ:
;ih-a2;abc"def[ih-a2] bar=[abc"def]А вот непробельный символ после закрывающей кавычки — ошибка:
;ih-a3;"abc"defunexpected char d after cell end at 12 in line 1 : ';ih-a3;"abc"def'На этом часто спотыкаются с дюймами. Обратный слэш в ImpEx не экранирует кавычку, поэтому "5\" monitor" — это ячейка 5\, закрытая кавычкой, за которой идёт monitor:
unexpected char m after cell end at 12 in line 2 : ';ih-b2;"5\" monitor'Кавычка внутри кавычек удваивается: "5"" monitor".
Непарная кавычка и ошибка не на той строке
Если строка заканчивается «внутри кавычек», парсер считает, что значение продолжается, и присоединяет следующую строку. Потом ещё одну — пока кавычки не сбалансируются или не кончится файл.
// иллюстрация
boolean isWrappedLine(String line, boolean alreadyQuoted) {
boolean quoted = alreadyQuoted, cellStarted = alreadyQuoted, cellEnded = false;
for (char c : line) {
if (quoted) {
if (c == '"') { if (next == '"') skip(); else { quoted = false; cellEnded = true; } }
} else if (c == '"') {
if (cellEnded) throw new IllegalStateException("unexpected text separator ...");
if (!cellStarted) { quoted = true; cellStarted = true; } // только в начале ячейки
} else if (c == ';') {
cellStarted = cellEnded = false;
} else if (!isWhitespace(c)) {
if (cellEnded) throw new IllegalStateException("unexpected char " + c + " after cell end ...");
cellStarted = true;
}
}
return quoted; // true → приклеить следующую строку
}Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Пример. Здесь виновата вторая строка данных — открывающая кавычка без закрывающей:
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-e1;ok
;ih-e2;"27 inch monitor
;ih-e3;fine
;ih-e4;"bold";x
;ih-e5;lastРезультат:
[ih-e1] bar=[ok]
[ih-e5] bar=[last]Три строки пропали, а в логе:
unexpected char b after cell end at 8 in line 2 : ';ih-e4;"bold";x'Строка из сообщения, ;ih-e4;"bold";x, корректна. Ошибка возникла, когда парсер, склеивая строки после ih-e2, дошёл до кавычки в ih-e4 и принял её за закрывающую. Номер line 2 указывает на настоящую причину (ih-e2 — строка 2, если считать с нуля и вместе с заголовком), а текст — на ih-e4. И ih-e3, которой в сообщении нет вообще, тоже не импортирована.
Если до конца файла кавычек больше нет, сообщение точнее:
reached EOF and got odd number of '"'! File is corrupt. Error starts in line:
";ih-c2;"27 inch monitor" and occurs somewhere in the following lines.Если склеивается больше csv.maxmultilines строк (по умолчанию 10 000), парсер останавливается с отдельным сообщением.
Ни одна из этих ошибок не видна в HAC. Блок результата пишет «Import has encountered problems.», а под ним ничего нет. Текст ошибки есть только в консольном логе сервера.
Перевод строки и обратный слэш — два разных механизма
Кавычки сохраняют перевод строки внутри значения:
;ih-a4;"line1
line2"[ih-a4] bar=[line1\nline2]Обратный слэш в конце строки склеивает её со следующей — встык, без пробела и без перевода строки:
;ih-a5;part1\
part2[ih-a5] bar=[part1part2]Здесь поправка к статье 2016 года. Тогда я писал, что \ нельзя использовать как разделитель многострочности в блоке данных. На 2211 это не так: в примере выше \ стоит именно в строке данных, и склейка работает.
Комментарий определяется по первой ячейке
Комментарием считается строка, у которой первая ячейка начинается с #. Поэтому # в середине строки — обычные данные:
;ih-a7;value # not a comment[ih-a7] bar=[value # not a comment]А вот это — комментарий, хотя выглядит как данные:
"# quoted comment?";ih-a7b;xПроверка выполняется уже после того, как ячейка извлечена из кавычек. Поэтому первая ячейка в кавычках, начинающаяся с #, — тоже комментарий. Именно благодаря этому работают строки кода в кавычках вида "#% ...", о которых ниже.
Пустая ячейка, <ignore> и <null>
Как ведут себя три варианта на 2211:
| В ячейке | Строковый атрибут | Ссылка |
|---|---|---|
| пусто | записывается null |
записывается null |
<ignore> |
атрибут не трогается | атрибут не трогается |
<null> |
записывается строка <null> |
ищется объект, у которого ключ равен null |
Пример.
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-m1;start
;ih-m2;start
;ih-m3;start
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-m1;
;ih-m2;<ignore>
;ih-m3;<null>[ih-m1] bar=[null]
[ih-m2] bar=[start]
[ih-m3] bar=[<null>]С <ignore> всё ожидаемо. А <null> используется для поиска по ссылке. Попробуйте обнулить ссылку:
INSERT_UPDATE Employee;uid[unique=true];sessionLanguage(isocode)
;ih-emp2;<null>column 2: cannot resolve value '<null>' for attribute 'sessionLanguage'ImpEx искал язык с isocode IS NULL и не нашёл его. Чтобы обнулить ссылку, нужна пустая ячейка.
<null> нужен для составных ключей, у которых одна из частей пустая. Например, адрес, у которого нет города:
INSERT Address;owner(Employee.uid);streetname;town
;ih-emp1;ih-street;
;ih-emp1;ih-street;Berlin
INSERT_UPDATE Employee;uid[unique=true];defaultShipmentAddress(streetname,town)
;ih-emp1;ih-street:<null>
;ih-emp2;ih-street:Berlinih-emp1 -> ih-street/null
ih-emp2 -> ih-street/BerlinПод капотом движок просто переписывает текст запроса: каждое =?параметр, значение которого оказалось null, заменяется на IS NULL.
// иллюстрация
for (Map.Entry<String, Object> e : values.entrySet()) {
if (e.getValue() == null) {
query = query.replaceAll("=\\?" + e.getKey(), " IS NULL ");
}
}Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Регистр не важен: <NULL> работает так же.
Разрешение ссылок одним UNION ALL на всю строку
Если в строке больше одной колонки-ссылки (catalogVersion(...), unit(code), supercategories(code)), ImpEx разрешает их все одним запросом. Вот что в JDBC-логе для такой строки:
INSERT_UPDATE Employee;uid[unique=true];sessionLanguage(isocode);sessionCurrency(isocode);defaultShipmentAddress(streetname,town)
;ih-emp6;fr;USD;ih-street-q3:ParisSELECT x.a, x.b FROM (
SELECT item_t0.PK AS a, 0 AS b FROM languages item_t0
WHERE ( item_t0.p_isocode =?) AND (item_t0.TypePkString=? )
UNION ALL
SELECT item_t1.PK AS a, 1 AS b FROM currencies item_t1
WHERE ( item_t1.p_isocode =?) AND (item_t1.TypePkString=? )
UNION ALL
SELECT item_t2.PK AS a, 2 AS b FROM addresses item_t2
WHERE ( item_t2.p_streetname =? AND item_t2.p_town =?) AND (item_t2.TypePkString=? )
) xКолонка b — номер транслятора. По ней результаты раскладываются обратно по колонкам. Параметры каждого подзапроса переименовываются (?v0 → ?t0v0, ?t1v0, …), чтобы не столкнуться между собой.
// иллюстрация
sb.append("SELECT x.a, x.b FROM (");
for (int i = 0; i < translators.size(); i++) {
if (i > 0) sb.append(" UNION ALL ");
sb.append("{{").append(t.select()).append(" AS a, ").append(i).append(" AS b ")
.append(t.body().replaceAll("\\?v", "?t" + i + "v")).append("}}");
}
sb.append(") x");Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Тот же механизм работает для элементов коллекции в одной ячейке. Три группы в groups(uid) — тоже один запрос:
SELECT x.a, x.b FROM (
SELECT item_t0.PK AS a, 0 AS b FROM usergroups item_t0 WHERE ( item_t0.p_uid =?) AND (...)
UNION ALL
SELECT item_t1.PK AS a, 1 AS b FROM usergroups item_t1 WHERE ( item_t1.p_uid =?) AND (...)
UNION ALL
SELECT item_t2.PK AS a, 2 AS b FROM usergroups item_t2 WHERE ( item_t2.p_uid =?) AND (...)
) xЗапрос идёт через кэш FlexibleSearch
Это обычный FlexibleSearch-запрос с обычным ключом кэша. Когда я повторил импорт с той же комбинацией языка, валюты и адреса, но с другим uid, в JDBC-логе этого запроса не оказалось: результат взят из кэша. Если тысячи строк ссылаются на одну версию каталога и одну единицу измерения, это экономит запросы. Если комбинация значений в каждой строке уникальна, кэш забивается одноразовыми записями. И это ещё одна причина, по которой проблема с NOW() из статьи про FlexibleSearch касается и ImpEx.
Чанки
Объединённый запрос режут на чанки, но не на всех базах. Размер чанка по умолчанию зависит от базы:
// иллюстрация
int defaultChunkSize() {
boolean bigQueriesAreAProblem = isHana() || isOracle() || isSqlServer() || isMySQL8();
return config.getInt("impex.query.chunk.size", bigQueriesAreAProblem ? 400 : 0);
}Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
0 значит «не резать». На HANA, Oracle, SQL Server и MySQL 8 запрос режется по 400 подзапросов, на остальных — то есть на практике на локальной HSQLDB — собирается целиком. Значит, локально на HSQLDB вы проверяете не тот запрос, который пойдёт на прод в SAP Commerce Cloud на SQL Server. Модификатор колонки query-chunk-size переопределяет это значение, но только для элементов коллекции в этой колонке. Объединённый запрос по колонкам строки всегда использует глобальное значение. Если импорт падает с «too many tables in query» на строке с большой коллекцией — помогает модификатор. Если на строке с большим числом колонок-ссылок — только impex.query.chunk.size.
Как это выключить
impex.query.combined=false (по умолчанию true) возвращает по запросу на ячейку. Иногда это единственный способ заставить импорт работать на базе, которая давится длинными UNION.
Многопроходный импорт и условие остановки
Строки, ссылки которых не разрешились, не считаются ошибкой сразу. Они сбрасываются в dump, и dump импортируется ещё раз. Классический случай — ссылка вперёд:
INSERT_UPDATE Category;code[unique=true];catalogVersion(catalog(id),version)[unique=true]
;ih-cat-root;ih-catalog:Staged
INSERT_UPDATE CatalogVersion;catalog(id)[unique=true];version[unique=true]
;ih-catalog;Staged
INSERT_UPDATE Catalog;id[unique=true]
;ih-catalogКатегория ссылается на версию каталога, которая ссылается на каталог, который объявлен последним. В логе:
Finished 1 pass ... - processed: 3, dumped: 2 (last pass: 0)
Starting pass 2
Finished 2 pass ... - processed: 2, dumped: 1 (last pass: 2)
Starting pass 3
Finished 3 pass ... - processed: 1, no lines dumped (last pass 1)Импорт прошёл за три прохода, по одному на звено цепочки.
А теперь ссылка, которая не разрешится никогда:
INSERT_UPDATE Employee;uid[unique=true];sessionLanguage(isocode)
;ih-emp9;no_such_langFinished 1 pass ... - processed: 1, dumped: 1 (last pass: 0)
Starting pass 2
Finished 2 pass ... - processed: 1, dumped: 1 (last pass: 1)
Can not resolve any more lines ... Aborting further passes (at pass 2).
Finally could not import 1 lines!Условие продолжения выглядит так:
// иллюстрация
boolean anotherPass =
(maxPass < 0 || currentPass <= maxPass) // maxPass по умолчанию -1: без лимита
&& (currentPass == 1 || dumpedNow < dumpedLastPass); // строго меньше, чем в прошлый разИллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Второй проход выполняется всегда, если после первого что-то осталось. Третий и дальше — только если число отложенных строк строго уменьшилось. Что из этого следует на практике:
- Циклическая зависимость, в которой ни один объект не создаётся без другого, не разрешится никогда. Импорт остановится на втором проходе.
- Сообщение
Aborting further passesозначает, что число отложенных строк перестало уменьшаться. Лимита попыток по умолчанию нет вообще. - Длинная цепочка зависимостей в файле стоит прохода на каждое звено, и каждый проход перечитывает весь dump. Поэтому от порядка блоков зависит и скорость импорта.
Неимпортированная строка может оставить объект в базе
Посмотрим, что лежит в dump'е для неразрешимой строки (HAC показывает его в блоке результата):
INSERT_UPDATE Employee;uid[unique=true];sessionLanguage(isocode)
Employee,8809954312196,,,column 2: cannot resolve value 'no_such_lang' for attribute 'sessionLanguage';<ignore>ih-emp9;no_such_langВ первой ячейке — тип и PK. Ключевая колонка помечена <ignore>. Это значит, что сотрудник ih-emp9 уже создан в первом проходе со всеми атрибутами, которые удалось разрешить. Отложена только неразрешённая часть, которая будет дописана по PK. И если импорт в итоге «не удался», объект остаётся в базе — без языка, но существующий:
userService.getUserForUID("ih-emp9") → EmployeeModel (8809954312196@1)Импорт не откатывается: после «Finally could not import 1 lines!» в базе может остаться недоделанный объект.
Ошибки валидации тоже идут на второй проход
Если сохранение упало с ModelSavingException — валидатор, интерцептор, нарушение уникальности, — строка не считается окончательно провалившейся. Она уходит в dump с сообщением Failed to process item creation due to ModelSavingException. Retrying in the next turn. и выполняется ещё раз на следующем проходе. Поэтому сообщения об ошибках валидации в ImpEx часто выглядят вот так, с двумя номерами строки:
Exception : line 4: cannot create Title ... found 1 item(s) using the same keys,
Exception : line 3: cannot create Title ... found 1 item(s) using the same keysЭто одна и та же строка: номер из основного файла и номер из dump'а второго прохода. Интерцепторы с побочными эффектами на такой строке выполнятся дважды.
Есть недокументированный ключ impex.retry.on.interceptorexceptions.only (по умолчанию false, ни в одном .properties поставки его нет), который сужает повтор до случаев, когда причиной был именно InterceptorException.
Модификаторы
Документация описывает около полутора десятков модификаторов. В платформе их 29 в одном списке констант, ещё три заголовок читает отдельно, и есть testMode, который читается прямо в коде и нигде не объявлен.
| Модификатор | Где | Что делает |
|---|---|---|
unique |
колонка | входит в ключ поиска существующего объекта |
allownull |
колонка | разрешить null для обязательного атрибута |
ignorenull |
колонка | пропускать null-элементы коллекции |
default |
колонка | значение для пустой ячейки |
virtual |
колонка | колонки нет в данных, только default |
lang |
колонка | язык локализованного атрибута |
translator |
колонка | свой транслятор значения |
cellDecorator |
колонка | свой декоратор ячейки |
dateformat, numberformat |
колонка | форматы разбора |
collection-delimiter |
колонка | по умолчанию , |
path-delimiter |
колонка | по умолчанию : |
key2value-delimiter |
колонка | по умолчанию -> |
map-delimiter |
колонка | по умолчанию ; |
mode |
колонка | append / remove / merge для коллекций |
query-chunk-size |
колонка | чанк объединённого запроса — только для элементов коллекции |
forceWrite |
колонка | писать в не-writable атрибут |
pos |
колонка | явная позиция колонки |
alias |
колонка | имя колонки при экспорте |
sourceSystemId, pkFromExternalImportKey |
колонка | внешние ключи импорта |
batchmode |
заголовок | UPDATE/REMOVE по неуникальному ключу, много объектов |
cacheUnique |
заголовок | кэш ключ → PK на весь тип |
processor |
заголовок | свой ImportProcessor |
parallel |
заголовок | false — этот блок в один поток |
ignoreKeyCase |
заголовок | регистронезависимый поиск по ключу |
unlessExisting |
заголовок | пропускать существующие объекты в INSERT |
impex.legacy.mode |
заголовок | Jalo вместо ServiceLayer для этого блока |
sld.enabled |
заголовок | читается только распределённым ImpEx |
disable.interceptor.types |
заголовок | отключить интерцепторы по типу (validate, prepare, …) |
disable.interceptor.beans |
заголовок | отключить интерцепторы по имени бина |
disable.UniqueAttributesValidator.for.types |
заголовок | отключить проверку уникальности |
testMode |
заголовок | с cacheUnique: сверять кэш с базой на каждой строке |
Последний, testMode, — отладочный режим cacheUnique. Он перепроверяет каждый ответ кэша обычным запросом и падает при расхождении. В списке констант его нет.
Дальше — те, что ведут себя неочевидно.
Без forceWrite запись молча пропускается
Атрибут sealed есть у любого объекта и не writable. Попробуем его записать с модификатором и без:
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-s1;x
;ih-s2;x
UPDATE TestItemType2;foo[unique=true];sealed
;ih-s1;true
UPDATE TestItemType2;foo[unique=true];sealed[forceWrite=true]
;ih-s2;true[ih-s1, null, ...]
[ih-s2, true, ...]HAC пишет «Import finished successfully». Строка ih-s1 не изменилась, и единственный след этого — WARN в консольном логе:
column sealed of type TestItemType2 is not writable, but you want to update it.
If you want to write this non writable attribute explicit, you have to use the
forceWrite=true modifier. This warning is printed only once for header.
Please check subsequent lines for similar problem.«Printed only once for header» объясняет, почему в большом импорте вы видите одно предупреждение и думаете, что проблема единичная.
Атрибут, который можно задать только при создании (initial), forceWrite обновить не даст:
UPDATE TestItemType2;foo[unique=true];creationtime[dateformat=yyyy-MM-dd,forceWrite=true]
;ih-a4;2001-01-01attribute de.hybris.platform.jalo.test.TestItem.creationtime cannot be changed except during item creationunlessExisting работает только в INSERT и только со значением true
INSERT TestItemType2[unlessExisting=true];foo[unique=true];bar
;ih-a1;overwritten?
;ih-u1;new
INSERT TestItemType2[unlessExisting=yes];foo[unique=true];bar
;ih-a2;overwritten?
INSERT_UPDATE TestItemType2[unlessExisting=true];foo[unique=true];bar
;ih-a4;overwritten-by-insert-updateРезультат:
ih-a1не тронут,ih-u1создан — модификатор работает.unlessExisting=yes— этоfalse:Cannot insert. Item exists: [8796093153305]. Значение разбирается черезBoolean.valueOf, так что всё, кромеtrueв любом регистре, считаетсяfalse, и в лог ничего не пишется.- В
INSERT_UPDATEмодификатор молча игнорируется:ih-a4перезаписан.
ignoreKeyCase работает только на заголовке
Модификатор выглядит как колоночный — он про сравнение значения ключа. Но поиск существующего объекта читает его с заголовка:
INSERT_UPDATE TestItemType2;foo[unique=true,ignoreKeyCase=true];bar
;IH-A2;matched-case-insensitively?
INSERT_UPDATE TestItemType2[ignoreKeyCase=true];foo[unique=true];bar
;IH-A4;header-levelih-a2 -> abc"def ← не найден
IH-A2 -> matched-case-insensitively? ← создан новый объект
ih-a4 -> header-level ← найден и обновлёнНа колонке модификатор игнорируется, предупреждения нет.
Раз уж речь о регистре: имена атрибутов в заголовке регистронезависимы. INSERT_UPDATE TestItemType2;foo[unique=true];BAR пишет в bar.
cacheUnique загружает ключи всего типа
При старте заголовка движок выгружает ключи всех объектов типа:
INSERT_UPDATE Employee[cacheUnique=true];uid[unique=true];name
;ih-emp6;CachedSELECT item_t0.PK, item_t0.p_uid FROM users item_t0 WHERE (item_t0.TypePkString=? )Без WHERE по значениям. Для большого обновления, которое затронет большую часть таблицы, это выгодно: один запрос вместо тысяч. Для файла из десяти строк против таблицы из десяти миллионов это десять миллионов прочитанных строк ради десяти.
В INSERT без уникальной колонки модификатор запрещён:
You can not use cacheUnique modifier in INSERT mode when no column is unique,
use UPDATE / INSERT_UPDATE mode or declare at least one column as uniqueА если в базе уже есть два объекта с одинаковым значением ключа, заголовок падает целиком: found more than one item for unique key values .... Поэтому cacheUnique заодно находит дубли ключа, которые уже есть в базе.
Префиксы коллекций (+), (-) и недокументированный (+?)
В статье 2016 года были (+) и (-). Есть и третий префикс:
| Префикс | Действие |
|---|---|
(+) |
добавить элемент — всегда |
(-) |
удалить элемент |
(+?) |
добавить, если его ещё нет |
Разница между (+) и (+?) видна на атрибуте-списке. fallbackLanguages у Language — список:
UPDATE Language;isocode[unique=true];fallbackLanguages(isocode)
;xx_test;en
;xx_test;(+) de
;xx_test;(+) de
;xx_test;(+?) en
;xx_test;(+?) fr[en, de, de, fr](+) добавил de дважды. (+?) не стал добавлять существующий en. На множествах (Set, например groups у пользователя) дубликатов не будет, поэтому разница проявляется только на списках.
Префиксы нельзя смешивать с «голыми» значениями в одной ячейке:
;ih-emp7;ih-g2,(+) ih-g3Invalid syntax! The assigned collectionvalues must either have one of the prefixes
'(+)' or '(-)' or modifier mode must be setМодификатор mode=append|remove|merge на колонке задаёт действие сразу для всех значений колонки. Без него префикс нужен у каждого значения в ячейке, а не только у первого.
Заголовок, который выключает бизнес-логику
Три модификатора заголовка не входят в общий список, но читаются отдельно и кладутся в параметры сессии, в которой выполняется сохранение:
disable.interceptor.types=validate,prepare
disable.interceptor.beans='beanName1,beanName2'
disable.UniqueAttributesValidator.for.types='Title,Product'Эти модификаторы выключают валидацию и бизнес-логику для одного блока файла. Пример — интерцептор, который выключает вход пользователям.
Как ImpEx отключает пользователям вход
В расширении impex зарегистрирован интерцептор disableLoginForImportedUserInterceptor. Он срабатывает на любого User, которого создаёт или изменяет ImpEx, и выставляет loginDisabled=true, если у пользователя нет пароля.
INSERT_UPDATE Employee;uid[unique=true];name
;ih-emp10;plain
INSERT_UPDATE Employee[disable.interceptor.beans='disableLoginForImportedUserInterceptor'];uid[unique=true];name
;ih-emp11;interceptor off
UPDATE Employee;uid[unique=true];name
;ih-emp-sl;just renamedПользователь ih-emp-sl перед этим был создан через ServiceLayer, без пароля, с loginDisabled=false. Результат:
ih-emp10 loginDisabled=true name=plain
ih-emp11 loginDisabled=false name=interceptor off
ih-emp-sl loginDisabled=true name=just renamedОбычный UPDATE имени через ImpEx заблокировал пользователю вход. Если у вас есть пользователи без локального пароля (SSO, технические учётки), любой ImpEx, который их трогает, молча выключит им вход.
В логе при этом:
Disabling login for 'null' because of empty password.null — потому что сообщение печатает PK, а при создании его ещё нет.
Как интерцептор узнаёт, что работает ImpEx? По атрибутам сессии, которые ImpEx выставляет на время сохранения: impex.creation и impex.modification вместе с impex.modification.pk. Эти атрибуты может проверять и ваш интерцептор, чтобы вести себя иначе при импорте.
Интерцептор помечен @Deprecated(since = "2211", forRemoval = true), но включён по умолчанию. Глобально его выключает свойство disable.login.for.imported.user.interceptor.enabled=false, которое в project.properties поставки есть только в закомментированном виде.
disable.UniqueAttributesValidator.for.types
Модификатор действительно выключает валидатор — это видно по тому, как меняется ошибка:
INSERT Title;code
;ih-title
INSERT Title[disable.UniqueAttributesValidator.for.types='Title'];code
;ih-title... due to [UniqueAttributesInterceptor]:ambiguous unique keys {code=ih-title} ... found 1 item(s) using the same keys
... due to Duplicate code 'ih-title' for type TitleПервую строку остановил валидатор уникальности, вторую — проверка ниже по стеку. У Title она есть. Отключая валидатор, вы полагаетесь на то, что у вашего типа она тоже есть. Стоит проверить это на конкретном типе, прежде чем оставлять модификатор в файле.
Практическое правило: grep -rn "disable\.\(interceptor\|UniqueAttributesValidator\)" --include=*.impex . по всему проекту. Эти модификаторы обычно появляются «временно».
Переписывание заголовков через impex.header.replacement
В документации этого механизма нет, хотя сокращения E1p, E1t, E1d и C@ используются в любом проекте с ценами и классификацией. Это не синтаксис ImpEx, а правила из advanced.properties:
impex.header.replacement.1=C@(\\w+) ... @$1[system='\\$systemName',version='\\$systemVersion',translator='...ClassificationAttributeTranslator']
impex.header.replacement.2=E1p ... europe1Prices[translator='de.hybris.platform.europe1.jalo.impex.Europe1PricesTranslator']
impex.header.replacement.3=E1t ... europe1Taxes[translator='...Europe1TaxesTranslator']
impex.header.replacement.4=E1d ... europe1Discounts[translator='...Europe1DiscountsTranslator']Формат: impex.header.replacement.<приоритет> = <регулярка> ... <замена>, разделитель — три точки. Правила применяются так:
// иллюстрация
for (int i = 1; i < columns.size(); i++) { // колонка 0 (режим и тип) не трогается
for (Rule r : rulesByPriorityDesc) {
String replaced = columns.get(i).replaceAll(r.pattern, r.replacement);
if (!replaced.equals(columns.get(i))) {
columns.set(i, expandMacros(replaced)); // макросы раскрываются после замены
break; // первое сработавшее правило — последнее
}
}
}Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Свойства читаются при создании каждого reader'а, так что правило можно добавить без перезапуска. Я добавил его из Groovy-консоли:
de.hybris.platform.util.Config.setParameter(
"impex.header.replacement.10", "barD ... bar[default='from-rule']")INSERT_UPDATE TestItemType2;foo[unique=true];barD
;ih-h3;
;ih-h4;explicit[ih-h3] bar=[from-rule]
[ih-h4] bar=[explicit]Вот почему в правиле для C@ стоит \\$systemName: замена вставляет в колонку ссылку на макрос, а макрос раскрывается следующим шагом.
Регулярка не заякорена: replaceAll ищет совпадение в любом месте имени колонки:
INSERT_UPDATE TestItemType2;foo[unique=true];oldbarDINSERT_UPDATE TestItemType2;foo[unique=true];oldbar[default='from-rule']
# unknown attribute 'oldbar' in header 'INSERT_UPDATE TestItemType2'Колонка oldbarD превратилась в oldbar[...]. Со стандартными правилами это значит, что атрибут, в имени которого встречается E1p, E1t или E1d, будет искалечен ещё до разбора заголовка. Свои правила стоит писать с якорями: ^MyAttr$ ... myRealAttribute[...].
Программно то же самое делает reader.addHeaderReplacementRule(src, tgt, prio).
Код внутри ImpEx
Строки, начинающиеся с #%, — это код. По умолчанию (impex.legacy.scripting=true) его исполняет BeanShell. В статье 2016 года этому было посвящено полстатьи. Почти всё из неё работает, но есть несколько отличий, о которых я тогда не писал.
Без Enable code execution блок if: всегда истинен
В форме импорта HAC галка Enable code execution по умолчанию снята. Код в этом случае пропускается с WARN в логе. С блоком if: всё интереснее:
INSERT_UPDATE TestItemType2;foo[unique=true];bar
"#% if: ""a"".equals(""b"")"
;ih-nocode-inside-false-if;imported anyway?
#% endif:Без Enable code execution:
[ih-nocode-inside-false-if] bar=[imported anyway?]HAC: «Import finished successfully». Строка импортирована, хотя условие ложное: когда выполнение кода выключено, условие не вычисляется, и движок считает его истинным:
// иллюстрация
if (!(ifLine.getResult() instanceof Boolean)) {
result = codeExecutionEnabled
? Boolean.FALSE // невалидное выражение — «ложь», с WARN
: Boolean.TRUE; // код выключен — всё внутри if: импортируется
}Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Если вы используете if: для «этот блок только для dev» или «только если расширение установлено», то файл, импортированный без галки, импортирует всё.
Точка с запятой внутри кода исчезает
Строка кода — это тоже CSV-строка. ; в ней — разделитель ячеек. В режиме BeanShell по умолчанию движок собирает код, склеивая все ячейки без разделителя:
#% impex.info("IH1 a;b;c");
"#% impex.info(""IH2 a;b;c"");"IH1 abc
IH2 a;b;cЕсли операторов два, получится синтаксическая ошибка, по которой трудно понять причину:
#% counter = counter + 1; impex.info("counter=" + counter);inline evaluation of: ``counter = counter + 1impex.info("IH3 counter=" + counter);''
Encountered "impex" at line 1, column 22.Отсюда правило, которое в 2016 году я соблюдал, не объясняя: код в ImpEx всегда пишется в кавычках, а кавычки внутри удваиваются.
Один интерпретатор на файл
Интерпретатор BeanShell создаётся один на reader и живёт до конца файла. Переменные переживают заголовки:
#% counter = 1;
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-bsh1;x
INSERT_UPDATE TestItemType2;foo[unique=true];bar
"#% impex.info(""counter still visible="" + counter);"counter still visible=1В контексте всегда есть:
| Переменная | Что это |
|---|---|
impex |
сам reader (ImpExImportJob$MyImpExImportReader при импорте из HAC) |
jaloSession |
текущая Jalo-сессия |
line |
Map<Integer,String> текущей строки данных |
currentLineNumber, currentLocation |
позиция в файле |
Плюс предымпортированные пакеты de.hybris.platform.core, ...core.model.user, ...util, ...impex.jalo, ...jalo, ...jalo.c2l, ...jalo.user, ...jalo.flexiblesearch. Поэтому в коде можно писать Config.getBoolean(...) без импорта.
beforeEach: может переписать или выбросить строку
Механизм из примера 2016 года с языками по-прежнему работает:
INSERT_UPDATE TestItemType2;foo[unique=true];bar
"#% beforeEach: if (line.get(Integer.valueOf(2)).startsWith(""skip"")) line.clear(); else line.put(Integer.valueOf(2), line.get(Integer.valueOf(2)).toUpperCase());"
;ih-be1;make me loud
;ih-be2;skip me
#% beforeEach:end
;ih-be3;untouched[ih-be1] bar=[MAKE ME LOUD]
[ih-be3] bar=[untouched]line.clear() — недокументированный способ отфильтровать строку по условию: пустая строка просто не импортируется. afterEach: работает так же, но после сохранения объекта, и вызывается на каждой строке (forEach: — его устаревшее имя, при использовании пишется WARN).
getLastImportedItem() возвращает Jalo-объект
Даже в режиме ServiceLayer Direct:
"#% impex.info(""IH-LAST "" + impex.getLastImportedItem().getClass().getName());"IH-LAST de.hybris.platform.jalo.test.TestItemИменно поэтому в примере 2016 года с заказом работал impex.getLastImportedItem().calculate(): это Jalo-метод Jalo-объекта. Чтобы работать с моделью, нужен modelService.get(impex.getLastImportedItem()).
Два рецепта 2016 года, которые больше не работают
addDefinition из кода. В 2016 году макрос можно было задать из BeanShell. Сейчас метод protected, и BeanShell его не видит:
#% impex.addDefinition("$dyn=from-code");Error in method invocation: Method addDefinition ...При этом неизвестный макрос $dyn ниже по файлу не вызывает ошибку, а остаётся в данных буквально: bar=[$dyn]. Рабочий обход — вставить строку определения в поток:
"#% m = new HashMap(); m.put(Integer.valueOf(0), ""$dyn=from-code-"" + (6 * 7)); impex.insertLine(m);"
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-def3;$dyn[ih-def3] bar=[from-code-42]includeSQLData с тремя аргументами. Сигнатуры includeSQLData(url, driver, sql) из статьи 2016 года нет. Есть (url, user, password, driverClass, sql) — о ней ниже.
А $config- по-прежнему работает, если установлен commerceservices:
UPDATE GenericItem[processor=de.hybris.platform.commerceservices.impex.impl.ConfigPropertyImportProcessor];pk[unique=true]
$workers=$config-impex.import.workers
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-cfg1;$workers
;ih-cfg2;$config-impex.legacy.scripting[ih-cfg1] bar=[4]
[ih-cfg2] bar=[true]#%groovy% при impex.legacy.scripting=true
Есть и «современный» синтаксис с указанием движка: #%groovy% ..., #%javascript% .... Но он работает только при impex.legacy.scripting=false. При значении по умолчанию строка целиком, вместе с groovy%, уходит в BeanShell:
#%groovy% impex.info("IH-GROOVY engine=" + this.getClass().getName())IH-GROOVY engine=bsh.XThis
... inline evaluation of: ``groovy% impex.info(...)'' : illegal use of undefined variable, class, or 'void' literalСообщение в логе появилось раньше ошибки. BeanShell прочитал groovy% impex.info(...) как выражение «groovy по модулю impex.info(...)», вычислил правый операнд — то есть вызвал метод — и упал на левом. Если в такой строке стоит что-то с побочным эффектом, побочный эффект случится, а импорт сообщит об ошибке.
При impex.legacy.scripting=false та же строка выполняется Groovy (engine=Script73). Но и у этого режима свои отличия: код берётся только из первой ячейки, то есть обрезается на первой ; без кавычек, а не склеивается. Старые файлы с BeanShell-кодом, у которого точка с запятой внутри, после переключения сломаются по-новому.
Строка кода останавливает параллельную обработку
Импорт по умолчанию многопоточный: читатель раскладывает строки данных по очереди, воркеры их сохраняют. Если в файле стоит строка кода, она должна увидеть результат всех строк выше. Поэтому перед каждой отдельной #%-строкой и перед каждым новым заголовком читатель ждёт, пока очередь опустеет:
// иллюстрация
@Override
protected void execute(CodeLine line, Map csvLine, boolean perLineHook) {
if (!perLineHook) {
waitForEmptyValueLineQueue(); // барьер: все воркеры должны доделать свою работу
}
super.execute(line, csvLine, perLineHook);
}Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Строка #% impex.info("progress") после каждой сотни строк данных превращает многопоточный импорт в последовательность коротких параллельных участков. Сам интерпретатор при этом защищён synchronized, так что beforeEach: / afterEach: тоже выполняются строго по одному.
Модификатор заголовка parallel=false переводит конкретный блок в один поток, не трогая остальные.
Файлы, classpath и чужие базы данных
Чтение файлов сервера через file:, zip: и jar:
Колонка @media с MediaDataTranslator принимает путь с префиксом:
INSERT_UPDATE Media;code[unique=true];catalogVersion(catalog(id),version)[unique=true];mime;@media[translator=de.hybris.platform.impex.jalo.media.MediaDataTranslator]
;ih-m-file;ih-catalog:Staged;text/plain;file:/path/on/server/srvfile.txt
;ih-m-zip;ih-catalog:Staged;text/plain;zip:/path/on/server/arch.zip&inner/note.txt
;ih-m-jar;ih-catalog:Staged;application/octet-stream;jar:de.hybris.platform.impex.jalo.ImpExManager&ImpExManager.classih-m-file size=37 data=hello from the app server filesystem
ih-m-zip size=24 data=hello from inside a zip
ih-m-jar size=29123 data=����...de/hybris/platform/impex/jalo/GeneratedImpExMan...file:читает любой файл, доступный процессу сервера приложений.zip:читает запись из архива на сервере. Разделитель между архивом и записью —&.jar:Класс&ресурсзагружает класс, имя которого указано в ячейке, и читает ресурс через его загрузчик. В примере выше в медиа попал байт-код самогоImpExManager.
http: сломан, а https: не поддерживается
Префикс http: не скачивает файл, а записывает URL в медиа. Точнее, должен записывать:
;ih-m-http;ih-catalog:Staged;image/png;http://example.com/x.png
;ih-m-https;ih-catalog:Staged;image/png;https://example.com/x.pngException : Error while setting URL to media
Path 'https://example.com/x.png' couldn't be resolved!Префикса https: нет. А http: ломается потому, что движок отрезает префикс, прежде чем записать URL:
// иллюстрация
if (path.toLowerCase().startsWith("http:")) {
media.setURL(path.substring("http:".length())); // → "//example.com/x.png"
}Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Остаётся //example.com/x.png, и его отвергает MediaExternalUrlSourcesValidator, который в текстах ImpEx не появляется. Если повторить запись из Groovy, видна настоящая причина:
Access protocol is not allowed for media URL //example.com/y.png.
Only: [https, http, fromjar] are allowed.В legacy-режиме (Jalo) — то же самое. Обход — продублировать префикс:
;ih-m-http3;ih-catalog:Staged;image/png;http:http://example.com/z.png
;ih-m-https3;ih-catalog:Staged;image/png;http:https://example.com/z.pngih-m-http3 url=http://example.com/z.png
ih-m-https3 url=https://example.com/z.pngВ неудачном случае объект Media всё равно создаётся, без URL и без данных. Это тот же эффект «создано, но не импортировано», что и в разделе о проходах.
Строки из чужой базы через includeSQLData
Переменная impex — это reader со всеми публичными методами. Один из них открывает JDBC-соединение и подставляет результат запроса как строки данных текущего заголовка:
INSERT_UPDATE TestItemType2;foo[unique=true];bar
"#% impex.includeSQLData(""jdbc:hsqldb:mem:ihdemo"", ""SA"", """", ""org.hsqldb.jdbcDriver"", ""SELECT 'ih-sql-' || TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'INFORMATION_SCHEMA' ORDER BY TABLE_NAME LIMIT 3"");"[ih-sql-ADMINISTRABLE_ROLE_AUTHORIZATIONS] bar=[VIEW]
[ih-sql-APPLICABLE_ROLES] bar=[VIEW]
[ih-sql-ASSERTIONS] bar=[VIEW]Первая колонка результата идёт в первую колонку заголовка после пустой ячейки, вторая — во вторую. Для ETL это вполне рабочий инструмент. Вместе с ним есть семейство includeExternalData(...) для файлов и потоков и initDatabase(...) для нескольких запросов к одной базе.
Безопасность
#% — произвольный код на сервере, file: и zip: — чтение файловой системы, jar: — загрузка любого класса по имени, includeSQLData — соединение с любой базой, до которой дотягивается сервер. Право запускать ImpEx-импорт с включённым выполнением кода эквивалентно праву выполнять код на сервере. Без выполнения кода остаётся чтение файлов через file:, zip: и jar:.
В HAC это и так администраторская операция. Но ImpEx часто дают контент-менеджерам через Backoffice, интеграциям через ImportService и пайплайнам загрузки данных. Там это стоит пересмотреть.
Число потоков задаётся выражением SpEL
Число воркеров берётся из свойства, и его значение — это выражение SpEL:
impex.import.workers=#cores * 2 # project.properties
impex.import.workers.hsqldb=1 # advanced.properties: override по имени базы// иллюстрация
int defaultMaxThreads(Tenant t) {
String expr = config("impex.import.workers." + t.databaseName().toLowerCase());
if (isBlank(expr)) expr = config("impex.import.workers");
return numberOfThreadsFromExpression(expr, 2); // переменная #cores, запасное значение 2
}Иллюстративная реконструкция, а не исходный код платформы. См. примечание выше.
Я прогнал несколько выражений через ту же функцию на машине с 14 ядрами:
| Выражение | Результат |
|---|---|
#cores * 2 |
28 |
#cores + 2 |
16 |
4 |
4 |
#core * 2 (опечатка) |
2, с WARN |
cores * 2 (без #) |
2, с WARN |
#cores / 0 |
2, с WARN |
T(java.lang.Runtime).getRuntime().availableProcessors() |
2, с WARN |
#cores * 2,5 |
исключение SpelParseException |
Контекст вычисления ограничен: доступна арифметика над #cores, но не обращение к классам — T(...) не работает. Опечатка тихо откатывает вас на два потока, и всё, что остаётся, — WARN Exception has been thrown during evaluation of '...'. Null will be returned. А вот синтаксическая ошибка не перехватывается и вылетает наружу.
На HSQLDB эта настройка не действует: impex.import.workers.hsqldb=1 переопределяет общее значение, и getDefaultMaxThreads возвращает 1. Если вы меряете скорость импорта локально на HSQLDB, вы меряете однопоточный импорт.
Распределённый ImpEx работает на другом движке
В HAC у формы импорта есть галка Distributed mode. Это отдельная реализация поверх task engine: файл режется на батчи (impex.distributed.batch.size=100), батчи сохраняются в базе как ImportBatch / ImportBatchContent и обрабатываются задачами, которые могут выполняться на любой ноде кластера.
Я прогнал через него файл с двумя обычными строками, строкой кода и неразрешимой ссылкой:
INSERT_UPDATE TestItemType2;foo[unique=true];bar
;ih-dist1;d1
;ih-dist2;d2
#% impex.info("IH-DIST code line");
INSERT_UPDATE Employee;uid[unique=true];sessionLanguage(isocode)
;ih-dist-emp;no_such_langВ логе видно, что строка кода выполнилась один раз, при нарезке файла: IH-DIST code line появилась в HTTP-потоке раньше, чем стартовала первая задача обработки батча. Поэтому всё, что зависит от порядка (getLastImportedItem(), счётчики, afterEach:), в распределённом режиме работает иначе, чем в обычном.
Модели сохраняются пакетами: Number of models saved in bulk: 1.
Повторы идут «ходами» по таймеру. Неразрешимая строка обрабатывалась четыре раза: в 20:33:33, 20:34:03, 20:34:34 и 20:35:04 — с шагом около 30 секунд на моей установке. Условие остановки — impex.distributed.noProgressionLimit=3 хода без прогресса. Импорт, который в обычном режиме упал бы за доли секунды, в распределённом занял больше трёх минут.
После неудачи батчи остаются в базе. До запуска ImportBatch и ImportBatchContent было по нулю, после — 12 и 3. Удаляются они только при успехе (impex.distributed.importbatchcontent.removeonsuccess=true). Если у вас растёт таблица ImportBatchContent, это следы провалившихся распределённых импортов.
Объединённый запрос для разрешения ссылок у распределённого режима свой, со своим выключателем: impex.distributed.query.for.each.line=false.
Справочник свойств, описанных только комментариями
Большинство ключей, описанных только комментариями, лежит в ext/impex/project.properties. Вместе с ключами из advanced.properties и теми, что читаются только кодом:
| Свойство | По умолчанию | Что делает |
|---|---|---|
impex.legacy.mode |
false |
Jalo вместо ServiceLayer для всех операций |
impex.legacy.scripting |
true |
BeanShell со склейкой ячеек; false — движки по #%groovy% и т. д. |
impex.query.combined |
true |
один UNION ALL на строку |
impex.query.chunk.size |
400 / 0 | 400 для HANA, Oracle, SQL Server, MySQL 8; 0 для остальных (на практике — HSQLDB) |
impex.import.workers |
#cores * 2 |
SpEL; .hsqldb=1 и т. п. — override по базе |
impex.header.replacement.N |
E1p, E1t, E1d, C@ |
правила переписывания заголовков |
impex.log.unresolved.lines |
true |
логировать отложенные строки |
impex.retry.on.interceptorexceptions.only |
false |
повтор на следующем проходе только для InterceptorException |
impex.clearing.model.context.after.processing.each.item.enabled |
true |
чистить model context после каждого объекта |
disable.login.for.imported.user.interceptor.enabled |
true (в коде) |
loginDisabled для импортированных пользователей без пароля |
documentidregistry.disallow.ignored.values |
true |
запрет <ignore>-значений в &-ссылках |
csv.maxmultilines |
10000 |
сколько строк можно склеить при непарной кавычке |
impex.distributed.batch.size |
100 |
размер батча |
impex.distributed.noProgressionLimit |
3 |
ходов без прогресса до остановки |
impex.distributed.importbatchcontent.removeonsuccess |
true |
удалять батчи только при успехе |
impex.distributed.query.for.each.line |
false |
свой выключатель объединённого запроса |
impex.import.service.distributed.enabled |
false |
распределённый режим для всего ImportService |
Про impex.clearing.model.context.after.processing.each.item.enabled комментарий в поставке сам признаёт, что включённое значение может давать «significant performance degradation». Если импорт заметно просел после апгрейда — это первый кандидат на проверку. Но выключение — осознанный размен: валидаторы перестанут гарантированно видеть свежие данные.
Чек-лист
- Без галки Enable code execution блоки
if:импортируются целиком. Проверьте, как ваши файлы импортируются в каждом из путей: HAC, Backoffice,ImportService, system setup. - Ошибки CSV видны только в консольном логе, не в HAC. При непарной кавычке номер строки в сообщении правильный, а текст взят из другой строки, и строки между ними пропадают.
<null>не обнуляет ссылку. Обнулить её можно только пустой ячейкой.- «Could not import N lines» не значит, что объектов нет. Строка создаётся в первом проходе, а отложенная часть может так и не дописаться.
Aborting further passes (at pass 2): число отложенных строк перестало уменьшаться, ищите циклы и опечатки в ключах. Лимита проходов нет.forceWrite,unlessExisting=yes,ignoreKeyCaseна колонке,(+)на списке, неизвестный$макросне дают ошибки, но результат отличается от написанного.cacheUniqueна маленьком файле против большой таблицы выгружает ключи всего типа.- ImpEx-
UPDATEпользователя без пароля выключает ему вход (disableLoginForImportedUserInterceptor). SSO и технические учётки — в первую очередь. disable.interceptor.*иdisable.UniqueAttributesValidator.for.typesнайдите grep'ом по всем.impexпроекта.- Код в ImpEx пишется в кавычках, иначе
;исчезает.#%groovy%при настройках по умолчанию выполняет BeanShell. #%-строки посреди данных ставят барьеры в многопоточном импорте.http:для медиа работает только с двойным префиксом,https:не поддерживается.- Доступ к импорту с кодом равен праву выполнять код на сервере; без кода остаётся чтение файлов сервера через
file:,zip:,jar:. - Растущая
ImportBatchContent— следы провалившихся распределённых импортов. - Скорость импорта на HSQLDB однопоточная, сравнивать её с продом бессмысленно.
Что изменилось с 2016 года
Из рецептов 2016 года не работают два: addDefinition из кода (метод стал protected) и трёхаргументный includeSQLData (такой сигнатуры нет). Одно утверждение было неверным с самого начала: обратный слэш работает и в строках данных. Всё остальное — удаление через batchmode, if:, beforeEach: с line, $config-, (+) и (-) — работает как раньше. Под каждым рецептом, правда, нашлась деталь, которую тогда стоило бы упомянуть.
Сильнее всего меня удивило, сколько в ImpEx отказов, о которых HAC не сообщает: «Import finished successfully», а в базе не то, что в файле. А ошибки, которые ImpEx всё-таки выдаёт, часто указывают не туда: на другую строку, на второй проход вместо первого, на null вместо идентификатора пользователя.
Как и в случае с FlexibleSearch, для компонента, спроектированного в начале 2000-х и совместимого с каждым когда-либо импортированным файлом, это объяснимо. Практическое правило отсюда одно: результат ImpEx нужно проверять по базе, например FlexibleSearch-запросом по импортированным ключам и сравнением количества объектов.
Discussion
Powered by GitHub Discussions — sign in with a GitHub account to join the conversation.