Программные перечисления, ч.2: приемы кэширования при разработке




Принцип обмена данными из 1С с сайтом (на MySQL) и выдачи (публикации) этих данных по запросу.
PHP-Скрипт автоматической загрузки данных из файла данных в формате CSV в базу данных сайта работающего на WordPress.

В продолжение моей темы: 1С:Альфа-Авто Автосалон Автосервис: обмен с сайтом.
С помощью данного скрипта можно загружать в автоматическом режиме, по расписанию, данные сервисных книжек (ремонтов авто) из 1С:Альфа-Авто Автосалон Автосервис.
Также можно загружать данные в ручном режиме: для этого делается скрытая страница, где размещается специальная кнопка.
Комментарии размещенные внутри скрипта разъяснят логику и порядок действия.
Комментарии с "/////    echo" использовались для отладки.
Дополнительно создана таблица для журналирования результатов загрузки данных.
Скрипт включает в себя защиту от SQL инъекций (думаю безопасность соблюдена в полной мере).
В кратце:
1. Пишется скрипт, который запускает этот.
2. Создается регламентное задание в WordPress, по которому запускается скрипт из п.1. 
3. Этот скрипт осуществляет проверку на существование файла обмена в папке.
4. Если данные не новые, загрузка не производится.
5. Если данные новые, очищается таблица сервисных книжек.
6. Загружаются новые данные.

Собственно сам скрипт:

<?php // Полная загрузка сервисных книжек, создан 2024-01-05 12:44:55

global $wpdb2;
global $failure;
global $file_hist;

/////  echo '<H2><b>Старт загрузки</b></H2><br>';

$failure=FALSE;
//подключаемся к базе
$wpdb2 = include_once 'connection.php'; ; // подключаемся к MySQL
// если не удалось подключиться, и нужно оборвать PHP с сообщением об этой ошибке
if (!empty($wpdb2->error))
{
/////   echo '<H2><b>Ошибка подключения к БД, завершение.</b></H2><br>';
$failure=TRUE;
wp_die( $wpdb2->error );
}

$m_size_file=0;
$m_mtime_file=0;
$m_comment='';
/////проверка существования файлов выгрузки из 1С
////файл выгрузки сервисных книжек
$file_hist = ABSPATH.'/_1c_alfa_exchange/AA_hist.csv';
if (!file_exists($file_hist))
{
/////   echo '<H2><b>Файл обмена с сервисными книжками не существует.</b></H2><br>';
$m_comment='Файл обмена с сервисными книжками не существует';
$failure=TRUE;
}

/////инициируем таблицу лога
/////если не существует файла то возврат и ничего не делаем
if ($failure){
///включает защиту от SQL инъекций и данные можно передавать как есть, например: $_GET['foo']
/////   echo '<H2><b>Попытка вставить запись в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>$m_comment));
wp_die();
/////    echo '<H2><b>Возврат в начало.</b></H2><br>';
return $failure;
}
/////проверка лога загрузки, что бы не загружать тоже самое
$masiv_data_file=stat($file_hist);   ////передаем в массив свойство файла
$m_size_file=$masiv_data_file[7];    ////получаем размер файла
$m_mtime_file=$masiv_data_file[9];   ////получаем дату модификации файла
////создаем запрос на получение последней удачной загрузки
////выбираем по штампу времени создания (редактирования) файла загрузки AA_hist.csv, $m_mtime_file

/////   echo '<H2><b>Размер файла: '.$m_size_file.'</b></H2><br>';
/////   echo '<H2><b>Штамп времени файла: '.$m_mtime_file.'</b></H2><br>';
/////   echo '<H2><b>Формирование запроса на выборку из лога</b></H2><br>';
////препарируем запрос
$text_zaprosa=$wpdb2->prepare("SELECT * FROM `vin_logs` WHERE `last_mtime_upload` = %s", $m_mtime_file);
$results=$wpdb2->get_results($text_zaprosa);

if ($results)
{   foreach ( $results as $r)
{
////если штамп времени и размер файла совпадают, возврат
if (($r->last_mtime_upload==$m_mtime_file) && ($r->last_size_upload==$m_size_file))
{////echo '<H2><b>Возврат в начало, т.к. найдена запись в логе.</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>'Загрузка отменена, новых данных нет, т.к. найдена запись в логе.'));
wp_die();
return $failure;
}
}
}
////если данные новые, пишем в лог запись о начале загрузки
/////echo '<H2><b>Попытка вставить запись о начале загрузки в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>0, 'last_size_upload'=>$m_size_file, 'comment'=>'Начало загрузки'));

////очищаем таблицу
$clear_tbl_zap=$wpdb2->prepare("TRUNCATE TABLE %s", 'vin_history');
$clear_tbl_zap_repl=str_replace("'","`",$clear_tbl_zap);
$results=$wpdb2->query($clear_tbl_zap_repl);
/////   echo '<H2><b>Очистка таблицы сервисных книжек</b></H2><br>';
if (empty($results))
{
/////   echo '<H2><b>Ошибка очистки таблицы книжек, завершение.</b></H2><br>';
//// если очистка не удалась, возврат
$failure=TRUE;
wp_die();
return $failure;
}

////загружаем данные
$table='vin_history';         // Имя таблицы для импорта
//$file_hist Имя CSV файла, откуда берется информация     // (путь от корня web-сервера)
$delim=';';          // Разделитель полей в CSV файле
$enclosed='"';      // Кавычки для содержимого полей
$escaped='\

17 Comments

  1. mifka186

    Есть еще ситуации, когда надо грузить много однотипных файлов. В таком случае я поиск данных скидываю в кэш. Получается, что все возможные значения для загрузки находятся на первых 2-3 файлах, а самих файлов может быть 70-80. Дальше заполнение данных идёт уже из кэша.

    Reply
  2. guy_septimiy

    (1) Аналогично. Вчера делал таким образом загрузку данных о операциях по мобильной связи. Файл в миллион строк с кешированием грузится где-то за 2 минуты. Если не использовать предварительное кеширование и обновление кеша, то время подскакивает раз в 20.

    Reply
  3. vano-ekt
    КлючСчета = «сч» + СтрЗаменить(Выборка.Код, «.», «_»)

    они же предопределенные, зачем их кэшить?

    А если кэшить, чем соответствие не угодило?

    Запрос = Новый Запрос(«ВЫБРАТЬ
    | Бюджетирование.Код,
    | Бюджетирование.Ссылка
    |ИЗ
    | ПланСчетов.Бюджетирование КАК Бюджетирование»);
    Выборка = Запрос.Выполнить().Выбрать();
    СчетаПоКодам = Новый Соответствие;
    Пока Выборка.Следующий() Цикл
    СчетаПоКодам.Вставить(Выборка.Код,Выборка.Ссылка);
    КонецЦикла;
    
    Если Счет = СчетаПоКодам[«20321»] Тогда
    //
    КонецЕсли;
    

    Показать

    Reply
  4. unichkin

    Счет может и не быть предопределенным, а быть пользовательским. Если использовать соответствие нужно получать значение по ключу, напр.

    СчетаУпр.Получить(<КодСчета>) или СчетаУпр[<КодСчета>]- долго, проще написать СчетаУпр.сч58. Кроме того у соответствия нет такого полезного метода как «Свойство».

    Кэшить их — затем чтобы применять в люом месте программы, не объявляя новый поиск счета.

    Reply
  5. NN2P

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

    Reply
  6. unichkin

    Я рассматриваю, см. п 2.1

    Reply
  7. jONES1979

    Cпасибо за качественное оформление блоками! Очень удобно просматривать!

    Reply
  8. Serg O.

    Идея клёвая… а как продолжение… Можно ли сохранить кэш в хранилище значений? Если такие обмены раз в сутки делаются после перезагрузки севера. Или в файл придется сохранять…

    Reply
  9. unichkin

    (9) Да, тогда промежуточное хранилище необходимо. Возможно имеет смысл задействовать подсистему присоединенных файлов, чтобы был удобный доступ к кэшу

    Reply
  10. klel

    Спасибо идея классная, сам пользуюсь при обменах между базами. Ускоряет работу в разы.

    Reply
  11. Scorpion4eg

    Хорошая статья. Вот только при действительно больших объемах кэширование скорее враг. Недавно пришлось поставить x64 платформу. Потому что 1с выедала 4,5 оперативнки. Так получилось что в кэш попало 500 тыс уникальных значений.

    Reply
  12. kasper076

    (0)

    КэшСсылок = Новый Соответствие;
    КэшСсылок.Вставить(«Объект1»,  Новый Соответствие);
    КэшСсылок.Вставить(«Объект2»,  Новый Соответствие);
    КэшСсылок.Вставить(«Объект3»,  Новый Соответствие);
    
    Пока ПолучитьСледующийЭлемент() Цикл
    
    Объект1Ссылка = КэшСсылок[«Объект1»][Ключ1];
    Если Объект1Ссылка = Неопределено Тогда
    Объект1Ссылка = ПолучитьОбъект1(Ключ1); //Функция выполняет запрос
    КэшСсылок[«Объект1»][Ключ1] = Объект1Ссылка;
    КонецЕсли;
    
    Объект2Ссылка = КэшСсылок[«Объект2»][Ключ2];
    Если Объект2Ссылка = Неопределено Тогда
    Объект2Ссылка = ПолучитьОбъект2(Ключ2); //Функция выполняет запрос
    КэшСсылок[«Объект2»][Ключ2] = Объект2Ссылка;
    КонецЕсли;
    
    Объект3Ссылка = КэшСсылок[«Объект3»][Ключ3];
    Если Объект3Ссылка = Неопределено Тогда
    Объект3Ссылка = ПолучитьОбъект3(Ключ3); //Функция выполняет запрос
    КэшСсылок[«Объект3»][Ключ3] = Объект3Ссылка;
    КонецЕсли;
    
    КонецЦикла
    

    Показать

    (4) Эффективная обработка данных в оперативной памяти за счет использования коллекции «соответствие»

    (5) Загрузка ТаблицыЗначений в TempDB производится построчно. Если таблица будет большая, то это будет не оптимально.

    Reply
  13. unichkin

    (13)

    — «доступ к элементу соответствия по ключу происходит почти со скоростью доступа к массиву или элементу структуры!» — и что? Тут больше дело привычки. У соответствия нет метода «Свойство» — нельзя проверить, существует ли параметр.

    — в приведенном коде все кэширование выполняется локально, что раздувает метод, и дублирует код. Инициализация кэша, поиск в кэше, помещение в кэш — все в одном месте. Если это модуль объекта, лучше обособить, как мне кажется — будет читабельнее.

    Пока ПолучитьСледующийЭлемент() Цикл
    
    Объект1Ссылка = ОбъектКэша(Ключ1);
    Объект2Ссылка = ОбъектКэша(Ключ2);
    Объект3Ссылка = ОбъектКэша(Ключ3);
    
    
    КонецЦикла
    
    Функция ОбъектКэша(Ключ)
    
    Если НЕ мСтруктураКэшДанных.Свойство(«СоответствиеОбъектыКэша») Тогда
    мСтруктураКэшДанных.Вставить(«СоответствиеОбъектыКэша»);
    КонецЕсли;
    
    ЗначениеКэша = мСтруктураКэшДанных.СоответствиеОбъектыКэша.Получить(Ключ);
    Если ЗначениеКэша = Неопределено Тогда
    ЗначениеКэша = ПолучитьОбъект(Ключ);
    мСтруктураКэшДанных.СоответствиеОбъектыКэша.Вставить(Ключ, ЗначениеКэша);
    КонецЕсли;
    
    Возврат ЗначениеКэша;
    
    КонецФункции

    Показать

    Reply
  14. Synoecium

    (6) можно исходную таблицу нарезать порциями (например по 10к элементов) и в цикле загружать их как описано в 2.1. На мой взгляд наиболее универсальный и быстрый способ.

    Reply
  15. pavlovsv

    (12) Согласен с Вами, тем не менее, идея очень здравая. Позволяет существенно снизить количество обращений к БД в случае обработки больших объемов одних и тех же данных, которые необходимо получать «через точку».

    Что же касается механизма для «нормализации» кэша, контроля за его размерами, управление жизненным циклом кэшированных данных… Думаю, что можно попробовать самостоятельно развить этот концепт.

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

    В конкретном примере из этой статьи, наверное, это избыточно(так как по условию задачи записей будет всего 10).

    Однако, в случае, когда количество строк несколько тысяч и ключ для поиска составной — индекс становится полезным.

    1. Синхронизация данных -> Как стало

    Функция НайтиСотрудникаОрганизации(Организация, ФизЛицо)

    // На входе два ссылочных реквизита — соответствие использовать неудобно, в качестве кэша применяется таблица значений:

    Если НЕ мСтруктураКэшДанных.Свойство(«ТаблицаСотрудников») Тогда

    ТаблицаСотрудников = Новый ТаблицаЗначений;

    ТаблицаСотрудников.Колонки.Добавить(«Организация»);

    ТаблицаСотрудников.Колонки.Добавить(«ФизЛицо»);

    ТаблицаСотрудников.Колонки.Добавить(«Сотрудник»);

    ТаблицаСотрудников.Индексы.Добавить(«Организация, ФизЛицо»); //добавим индекс по ключевым полям

    мСтруктураКэшДанных.Вставить(«ТаблицаСотрудников», ТаблицаСотрудников);

    КонецЕсли;

    Reply
  16. unichkin

    (16)

    Однако, в случае, когда количество строк несколько тысяч и ключ для поиска составной — индекс становится полезным.

    это вроде «и так понятно».. Об о всем не расскажешь)

    Индексировать стоит от 1000 строк Сортировка строк таблиц значений — ИТС в тему)

    Reply
  17. Rustig

    мне понравилось: проблема- задача- идея — реализация…

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *