У 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-кодом, у якому є крапка з комою, після перемикання зламаються по-новому.
Рядок коду зупиняє паралельну обробку
Імпорт за замовчуванням багатопотоковий: reader розкладає рядки даних у чергу, воркери їх зберігають. Якщо у файлі є рядок коду, він має побачити результат усіх рядків вище. Тому перед кожним окремим #%-рядком і перед кожним новим заголовком reader чекає, доки черга спорожніє:
// ілюстрація
@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.