Импорт данных
Материал из Eludia.
Содержание |
Практически каждая разветвлённая и долгоживущая информационная система получает данные не только от операторов с клавиатуры, но и в пакетном режиме из различных внешних источников. А на старте нового проекта зачастую приходится наследовать некий массив информации, накопленный в течение прошлой деятельности клиента.
Примерная постановка задачи
Допустим, вашему приложению так или иначе доступны 2 внешние таблицы: "организации" и "документы". Каждая строка таблицы "организации" содержит её уникальный код и наименование:
0001 1-я ГКБ 0002 2-я ГКБ
Таблица "документы" содержит уникальный код документа, код организации, регистрационный номер и дату:
000001 0001 №13/286 01.02.2006 000002 0001 №13/253 03.02.2006 000003 0002 №0-У\йеа 01.01.1980
Две внешние таблицы естественным образом отображаются в 2 таблицы в вашей БД. Назовём их organisations и docs. Какую структуру должна иметь таблица organisations? Символьное поле label – это понятно. А нужно ли ещё что-то? В принципе вы можете считать приравнять код организации обязательному полю id и использовать в импорте MySQL-инструкцию REPLACE INTO. Однако поступать так можно только в том случае, если внешнюю БД поддерживаете вы лично. Иначе в любой момент может обнаружиться, что после 0009 идёт 000A или просто вам объявят, что с такого-то числа код стал составным, поскольку во внешнюю БД добавили данные из удалённого филиала.
Чтобы заранее обезопасить себя от подобных неприятностей, лучше считать, что вам доступен натуральный ключ, который в будущем может сменить тип на символьный, и завести под него отдельное поле, скажем, external_id, не забыв построить соответствующий индекс.
Использование sql_select_id
Теперь займёмся импортом организаций. Допустим, мы прочитали запись с кодом $external_id и наименованием $label. Требуется добавить запись в наш справочник, но только если там нет строки с данным кодом. Для этого предназначена API-функция sql_select_id:
my $id = sql_select_id (organisations => { fake => 0, label => $label, external_id => $external_id, }, ['external_id']);
Поле fake упомянуто для того, чтобы новая запись была создана как актуальная, а external_id в последнем аргументе фигурирует в качестве ключевого поля. Если бы ключ был составным, скажем, включал бы в себя код внешнего подразделения external_id_department, то вызов функции принял бы вид:
my $id = sql_select_id (organisations => { fake => 0, label => $label, external_id => $external_id, external_id_department => $external_id_department, }, ['external_id', 'external_id_department']);
Составные ключи
Возможен также поиск по более чем одному натуральному ключу. Например, если для некоторых записей указан ОГРН, а для других – пара ИНН/КПП, то можно вызвать sql_select_id с двумя описаниями ключей:
my $id = sql_select_id (organisations => { fake => 0, label => $label, inn => $inn, kpp => $kpp, ogrn => $ogrn, }, ['inn', 'kpp'], ['ogrn']);
Актуализация найденных записей
Теперь предположим, что вы периодически, скажем, ежедневно, синхронизируете справочник организаций. Рано или поздно какая-либо из его строк модифицируется. Рассмотрим только возможность смены наименования организации при постоянных ИНН/КПП и ОГРН. Итак, чтобы быть уверенным в актуальности справочника, нужно обновлять значение поля label. Для этого можно организовать отдельный вызов sql_do, однако гораздо проще и нагляднее воспользоваться специальной возможностью всё той же sql_select_id: указанием "форсированных" аргументов при помощи префикса '-':
my $id = sql_select_id (organisations => { fake => 0, -label => $label, inn => $inn, kpp => $kpp, ogrn => $ogrn, }, ['inn', 'kpp'], ['ogrn']);
Форсированное поле можно использовать в том случае, когда имеется абсолютное доверие к источнику данных. Однако в жизни возможно, например, такое: во внешней БД ОГРН является ключевым полем, а ИНН/КПП заполняются от случая к случаю. Тем не менее, даже такая информация может оказаться нелишней. Одним словом, поля ИНН/КПП нельзя объявлять форсированными, но если в нашей БД они пусты, то на их место надо записать данные из внешнего источника. Именно так работает sql_select_id, никаких дополнительных действий предпринимать не требуется.
Далее при импорте документов нам понадобится вычислять ссылки на организации в нашей БД. Для этого снова следует воспользоваться функцией sql_select_id, только не указывать значение поля label. Можно воспользоваться и sql_select_scalar, но это менее предпочтительно. Если вдруг случится так, что сначала вам придётся импортировать документ, а потом организацию, то sql_select_scalar вернёт пустое значение и документ останется "подвешенным", а sql_select_id заведёт запись с пустым наименованием и нужным кодом, которая может быть найдена и обновлена при дальнейшем импорте. Кроме того, вызов sql_select_id гораздо нагляднее.
Удаление и слияние в справочниках
Теперь обратимся к тому прескорбному случаю, когда некая таблица (к примеру, справочник организаций) может как пополняться из внешнего источника, так и редактироваться пользователями вашей информационной системы. Вообразите себе такой сценарий: ваш оператор заводит карточку организации, после чего та же самая организация появляется в импортируемых данных и порождает дубликат. Допустим, дубликат замечен и ликвидирован (см. выше), причём удалённой оказалась как раз запись, порождённая при синхонизации. Что произойдёт при следующем импорте? Оказывается, ничего страшного: sql_select_id будет выдавать номер актуальной записи. Во-первых, потому что значение external_id скопируется из дубликата в пустое поле актуальной записи. Во-вторых, даже если оно каким-либо образом испортится, всё равно процедура sql_select_id найдёт удалённый дубликат и пройдёт по цепочке ссылок is_merged_to до актуальной записи.
Кэширование id
Если документов много, а организаций, на которые они ссылаются, относительно мало, то sql_select_id будет помногу раз вызываться с одинаковыми аргументами. В этой связи разумно реализовать кэширование результатов этой функции. Тогда поиск организаций с использованием кэша может выглядеть примерно так:
my %organisations = ();
while (...) {
my $id =
$organisations {"$inn/$kpp/$ogrn"}
||= sql_select_id (organisations => {
fake => 0,
-label => $label,
inn => $inn,
kpp => $kpp,
ogrn => $ogrn,
}, ['inn', 'kpp'], ['ogrn']);
}
При использовании кэша надо внимательно следить за габаритными параметрами импортируемых данных, поскольку есть реальная опасность перерасходовать память. В простых случаях стандартные хэши вполне применимы, а для больших объёмов справочников можно использовать модули типа Tie::Cacher.
Вложенные вызовы sql_select_id
При импорте плоской таблицы, где значения полей многих справочников перемешаны с атрибутами одной большой таблицы фактов, в нормализованную реляционную схему, бывает удобно совместить несколько вызовов sql_select_id в рамках одной операции:
my %organisations = ();
while (...) {
sql_select_id (documents => {
fake => 0,
external_id => $external_id,
no => $no,
dt => $dt,
id_organisation =>
$organisations {"$inn/$kpp/$ogrn"} ||=
sql_select_id (organisations => {
fake => 0,
-label => $label,
inn => $inn,
kpp => $kpp,
ogrn => $ogrn,
}, ['inn', 'kpp'], ['ogrn'])
}, ['external_id']),
}
