Разбиение / "суммирование" строк в запросе




Принцип обмена данными из 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='\

10 Comments

  1. CratosX

    А что по производительности по сравнению с постобработкой? Стоит ли оно того в свете поддержки/доработки?

    Reply
  2. ditp

    (1) CratosX, а что имеется ввиду под «постобработкой»?

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

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

    Допускаю, что в каких-либо других задачах, на каких-то более специфичных данных, предложенный алгоритм будет уступать процедурной обработке. Все ж зависит…

    Reply
  3. ildarovich

    Поставил плюс за развитие темы.

    По задаче 2 есть три замечания.

    1) Сначала мелкое:

    Условие проверки нечетности

    ГДЕ (ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0))) <> (ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 1)))

    лучше записать как

    ГДЕ (ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0))) <> т0.Номер / 2

    2) Замена группировки на соединение не было сделано вполне сознательно, учитывая, что вот это условие

    (т0.Номер + 1 = т1.Номер)

    в соединении почти наверняка приведет к сканированию второй таблицы, то есть для 1000 соединяемых строчек потребуется 500х1000 проверок. Тогда как при группировке операций будет не больше 1000. Чтобы работал ХэшМатч в этом соединении сравнение должно быть на равенство полей. Этого можно добиться, вычисляя номер пары для соединения заранее (в предшествующем запросе), поэтому при большом желании соединение все же можно использовать, но запись получается длиннее.

    3) Упрощение, достигнутое отказом от предварительного разбиения строк на буквы, имеет границы применимости (об этом в исходной статье написано). Это как ответ на вопрос: сколько сигарет поместится в объеме блока сигарет: всего двадцать, если в каждой пачке будет лежать по одной. Здесь также: если строка будет объявлена длиной 50, а равна «а», то в итоге можно будет получить только строку «аааааааааааааааа» (длиной 16), выполнив вполовину меньшее максимального число соединений.

    Reply
  4. ditp

    (3) ildarovich, спасибо за комментарий.

    По пунктам:

    1) согласен, в какой-то момент при написании включилось «безобразно, зато единообразно».

    2) запись получается длиннее совсем ненамного:

    ВЫБРАТЬ
    т1.Владелец КАК Номенклатура,
    т1.Наименование КАК Характеристика,
    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ т2.Ссылка) КАК Номер,
    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ т2.Ссылка)+1 КАК НомерСлед
    ПОМЕСТИТЬ т0
    ИЗ
    Справочник.ХарактеристикиНоменклатуры КАК т1 ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.ХарактеристикиНоменклатуры КАК т2 ПО т1.Владелец = т2.Владелец И т1.Ссылка >= т2.Ссылка
    СГРУППИРОВАТЬ ПО
    т1.Владелец, т1.Наименование;
    
    ВЫБРАТЬ
    т0.Номенклатура,
    ВЫБОР КОГДА т1.Номенклатура ЕСТЬ NULL ТОГДА т0.Характеристика ИНАЧЕ т0.Характеристика + «; » + т1.Характеристика КОНЕЦ КАК Характеристика,
    ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0)) КАК Номер,
    ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0)) + 1 КАК НомерСлед
    ПОМЕСТИТЬ т1
    ИЗ
    т0 КАК т0 ЛЕВОЕ СОЕДИНЕНИЕ т0 КАК т1 ПО т0.Номенклатура = т1.Номенклатура И (т0.НомерСлед = т1.Номер)
    ГДЕ ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0)) <> т0.Номер / 2 ;
    

    Показать

    3) возможно, мне не хватает понимания глубинных механизмов работы запросов в 1С, но каким образом длина строки повлияет на количество соединений, мне не ясно.

    Reply
  5. ildarovich

    (4)

    2) можно и так

    3) Я провел эксперимент и выяснил, что при выполнении запроса

    ВЫБРАТЬ Строка1 + Строка2 КАК Поле1

    возникает ошибка, если длина строк в сумме превышает 1024 (или 2048 в некоторых случаях).

    Аналогично

    ВЫРАЗИТЬ Строка1 + Строка2 + Строка3 + … + Строка32 КАК Поле1

    тоже будет давать ошибку, если длина исходных строк будет, например, равна 40. Потому, что тогда итоговая строка окажется длиной 1280 и не поместится в длину 1024, которая является ограничением при манипуляции со строками в запросе. Например, нельзя написать ВЫРАЗИТЬ(СтрокаХ КАК Строка(1280)). Я сделал вывод, что при выполнении операции Строка1 + Строка2 длина итогового поля не переменная, а фиксированная и равна сумме фиксированных длин строк-аргументов. Это логично для операций с таблицами.

    Используя пять парных соединений, мы фактически реализуем то же самое выражение

    ВЫРАЗИТЬ Строка1 + Строка2 + Строка3 + … + Строка32 КАК Поле1

    . Значит, длина исходных строк не может быть больше 32. Или должно быть меньше соединений.

    Максимальное количество соединений можно сделать тогда, когда исходные строки имеют длину 1. Тогда можно сделать десять соединений и получить в итоге строку длиной 1024, заполненную «до отказа». Поэтому и нужно предварительно разбивать строки на отдельные буквы.

    Если написал непонятно, просто попробуйте задать длину наименования справочника 500, записать четыре элемента с наименованием «Первый», «Второй», «Третий», «Четвертый» и двумя парными соединениями получить конкатенацию вида «ПервыйВторойТретийЧетвертый».

    Reply
  6. ditp

    (5) ildarovich, готово!



    и

    ВЫБРАТЬ
    «» КАК Номенклатура,
    т1.Реквизит1 КАК Характеристика,
    КОЛИЧЕСТВО(РАЗЛИЧНЫЕ т2.Ссылка) КАК Номер
    ПОМЕСТИТЬ т0
    ИЗ
    Справочник.Справочник1 КАК т1
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Справочник1 КАК т2
    ПО т1.Ссылка >= т2.Ссылка
    
    СГРУППИРОВАТЬ ПО
    т1.Реквизит1
    ;
    
    ////////////////////////////////////////////////////////////­////////////////////
    ВЫБРАТЬ
    т0.Номенклатура,
    ВЫБОР
    КОГДА т1.Номенклатура ЕСТЬ NULL
    ТОГДА т0.Характеристика
    ИНАЧЕ т0.Характеристика + «; » + т1.Характеристика
    КОНЕЦ КАК Характеристика,
    ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0)) КАК Номер
    ПОМЕСТИТЬ т1
    ИЗ
    т0 КАК т0
    ЛЕВОЕ СОЕДИНЕНИЕ т0 КАК т1
    ПО т0.Номенклатура = т1.Номенклатура
    И (т0.Номер + 1 = т1.Номер)
    ГДЕ
    (ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0))) <> (ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 1)));
    
    ВЫБРАТЬ
    т0.Номенклатура, ВЫБОР КОГДА т1.Номенклатура ЕСТЬ NULL ТОГДА т0.Характеристика ИНАЧЕ т0.Характеристика + «; » + т1.Характеристика КОНЕЦ КАК Характеристика,
    ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0)) КАК Номер
    //ПОМЕСТИТЬ т2
    ИЗ т1 КАК т0 ЛЕВОЕ СОЕДИНЕНИЕ т1 КАК т1 ПО т0.Номенклатура = т1.Номенклатура И (т0.Номер + 1 = т1.Номер)
    ГДЕ (ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 0))) <> (ВЫРАЗИТЬ(т0.Номер / 2 КАК ЧИСЛО(10, 1)));
    

    Показать

    в результате:

    Reply
  7. ditp

    Также без проблем отработало

    выбрать
    выразить(«1» как строка(1000))+
    выразить(«2» как строка(1000))+
    выразить(«3» как строка(1000))+
    выразить(«4» как строка(1000))+
    выразить(«5» как строка(1000))+
    выразить(«6» как строка(1000))+
    выразить(«7» как строка(1000))+
    выразить(«8» как строка(1000))+
    выразить(«9» как строка(1000))+
    выразить(«0» как строка(1000)) рез

    Показать

    P.S. Я допускаю возможность получить ошибку или некорректный результат, если в результате сложения получим «непустую» строку длиной свыше ХХХХ символов.

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

    Скриншоты из предыдущего сообщения в нормальном размере: http://imgur.com/a/kv28Z

    Reply
  8. ildarovich

    (6) да, сделали все в точности, но … не могли бы (все уже настроено) проверить вариант с длиной 600. Дело в том, что 500х4 = 2000 < 2048, а 600х4 = 2400 > 2048. Максимальная 2048 — это не документированная фитча для файловой (?) версии.

    Reply
  9. ditp

    (8) ildarovich, при длине в 600 интересная вещь получается: просто скопировать текст запроса как в посте выше не удается.

    Вызов конструктора выдает ошибку, в обработке написать что типа

    построитель  = Новый ПостроительОтчета;
    построитель.Текст= «ВЫБРАТЬ…»;

    то ошибка возникает при попытке открыть обработку.

    Видимо, 1С на этапе анализа запроса проверяет, какой длины строки получатся. Сужу по тому, что при замене

    т0.Характеристика + «»; «» + т1.Характеристика

    на

    выразить(т0.Характеристика как строка(1000))+ «»; «» + выразить(т1.Характеристика как строка(1000))

    все нормально.

    Так что, видимо, можно остановиться таки на том, что

    1) если итоговая строка может быть больше 2000 символов — требуется разбиение на символы и ограничение повторений в запросе, дальше только процедурно.

    2) если длина строки 2000 не превысит — можно мой механизм использовать.

    Reply
  10. ildarovich

    (9) я бы написал: … если суммарная максимальная(объявленная) длина соединяемых строк может быть больше 1024 символов, то …

    (1024 — чтобы учесть MS SQL и Postgre). Но занудствовать не люблю и спорить больше не буду.

    Reply

Leave a Comment

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