среда, 30 декабря 2020 г.

Порционная обработка записей в цикле

Прямо вот совсем недавно на стеке возник вопрос - как порционно загружать записи из CSV в БД? Вопрос-то простой, но дополнительным условием было - после окончания цикла while нельзя делать дополнительный запрос на вставку.

Простое и понятное решение:

$handle = fopen(/* ... */);
$batch = [];
while (($data = fgetcsv($handle, 4096, ';')) !== false) {
  $batch[] = processData($data);

  if(100 === \count($batch)) {
    runBatchInsert($batch);

    $batch = [];
  }
}
fclose($handle);
if ($batch) {
    runBatchInsert($batch);
}

Как мы видим - здесь мы собираем записи в массив $batch, и как только в этом массиве будет 100 элементов - выполняем запрос на вставку в функции runBatchInsert(). Понятно, что после завершения цикла в $batch могут находиться данные, которые также надо вставить, что мы и делаем вторым вызовом функции runBatchInsert().

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

$handle = fopen(/* ... */);
$batch = [];
// запускаем "вечный" цикл
while (true) {
  $data = fgetcsv($handle, 4096, ';');
  // если данные есть - добавляем их в $batch
  if (false !== $data) {
    $batch[] = processData($data);
  }

  // если размер батча достиг лимита или мы больше не получили данных
  if(100 === \count($batch) || false === $data) {
    // в батче есть данные - вставляем
    if ($batch) {
      runBatchInsert($batch);
    }

    // данных больше нет - прерываем цикл
    if (false === $data) {
      break;    
    }

    // очищаем батч
    $batch = [];
  }
}
fclose($handle);

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

понедельник, 23 ноября 2020 г.

Загрузка файла при обновлении записи

Иногда на стеке встречается вопрос от новичков - как заменить файл, принадлежащий некоторой сущности. Допустим, в программе есть сущность Книга, и у нее есть файл обложки. Когда книга редактируется - файл обложки может быть изменен.

Многие начинающие разработчики будут полагать, что путь к текущему файлу обложки надо выводить на форму редактирования, затем, аналогично полям <input type="text" />, добавлять его значение в запрос\метод\функцию обновления. По их мнению получается, что если файл не был обновлен, то в БД запишется то же самое значение пути к файлу, что есть в БД сейчас. Выглядит это в их голове как-то так:

if (isset($_POST['update-btn'])) {
    $name = $_POST['name'];

    // вроде как тут должен быть путь к файлу,
    // но <input type="file" /> работает не так
    $file = $_POST['file'];

    $query = "UPDATE tbl_book SET name = '$name', filepath = '$file' WHERE id = 42";
    $dbh->execute($query);
}

Как следствие, у них возникает вопрос - как вывести текущий путь к файлу в <input type="file" />? Ответ прост - никак. Нужно понять две вещи: первое - никакое значение от сервера в <input type="file" /> предустановить нельзя. Второе - работать с заменой файла следует иначе, чем с заменой текстовых значений.

Поэтому правильный алгоритм обновления файла выглядит так:

1. На форме обновления рядом с полем загрузки файла (<input type="file" />) выводим текущий файл, например: Текущая обложка <img src="path/to/file.jpg" />. Так как в общем случае никому не интересно, как файл назван в системе, то такого вывода достаточно: пользователь видит содержимое файла (собственно картинку) и может решить - следует ли загрузить новый файл. Если же действительно надо показать текущий путь к файлу, то выводим, например, Текущий файл расположен по пути "path/to/file.jpg". В обоих случаях path/to/file.jpg - это путь к файлу, хранящийся в БД.

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

2.1. Пользователь не загрузил файл. Значит, в запросе\методе\функции обновления записи не требуется указывать поле с файлом.

2.2. Пользователь загрузил новый файл. Значит, надо этот файл сохранить в каком-то каталоге и сформировать путь к нему. Далее, в запросе\методе\функции обновления записи указать путь к новому файлу, и после успешного обновления записи удалить с диска старый файл. Старый файл можно и не удалять, если на диске достаточно места.

Схематично код выглядит так, его аналоги можно реализовать в любом фреймворке:

if (isset($_POST['update-btn'])) {
    $name = $_POST['name'];

    if ($_FILES['file']) {
        move_uploaded_file();
        $file = 'path/to/file.jpg';

        // опционально, за этим значением нужно сходить в БД
        $oldFile = 'path/to/old_file.jpg';
    }

    if (isset($file)) {
        $query = "UPDATE tbl_book SET name = '$name', filepath = '$file' WHERE id = 42";
    } else {
        $query = "UPDATE tbl_book SET name = '$name' WHERE id = 42";
    }
    
    $dbh->execute($query);
    
    // опционально
    unlink($oldFile);
}

Это всё, что требуется сделать в случае замены файла в сущности. Никаких костылей изобретать не требуется.

понедельник, 26 октября 2020 г.

Short circuit evaluation в php

Сегодня поближе познакомимся с short circuit evaluation: выясним что это за зверь такой, посмотрим примеры и выясним, как он нам может помочь.

Для начала немного теории. Short circuit evaluation (не могу предложить простого русского перевода) - это стратегия в языках программирования, которая используется, чтобы избежать ненужных вычислений.
Лучше всего это понять на примере булевых выражений. Допустим, мы проверяем условие вида if (checkSomething() && checkSomethingElse()). Если checkSomething() вернет false, то true в итоге уже никак не получить, следовательно, вычислять второе значение в checkSomethingElse() не имеет смысла - любое вычисленное значение никак не повлияет на итоговый результат.

воскресенье, 20 сентября 2020 г.

Размеры таблиц и индексов в БД PostgreSQL

Сегодня рассмотрим несколько команд для определения размеров различных сущностей в PostgreSQL.

Перед тем как начать - рассмотрим вспомогательную функцию pg_size_pretty(), которая:

Преобразует размер в байтах, представленный в 64-битном целом, в понятный человеку формат с единицами измерения

То есть вместо какого-то огромного числа байтов показывает понятную строчку, например:

select pg_size_pretty(100250408::bigint);
-- Вывод:
pg_size_pretty
text
--------------
96 MB

Теперь, вооружившись этой функцией, переходим к основным функциям.

Первое и самое нужное - размер таблицы PostgreSQL без учета индексов и прочих деталей. Для этого есть функция pg_relation_size(), принимающая в качестве аргумента название таблицы. Комбинируем ее с pg_size_pretty() и получаем:

select pg_size_pretty(pg_relation_size('my_table'));
-- Вывод (небольшая тестовая таблица в моей БД):
pg_size_pretty
text
--------------
48 kB

Далее - определим сколько места занимают все индексы для таблицы. Для этого есть функция pg_indexes_size(), также принимающая в качестве аргумента название таблицы:

select pg_size_pretty(pg_indexes_size('my_table'));
-- Вывод (опять же для некоей тестовой таблицы):
pg_size_pretty
text
--------------
88 kB

Функция pg_total_relation_size() определяет сколько места суммарно занимает таблица, ее индексы и данные TOAST, так что ее результат будет отличаться от суммы результатов двух предыдущих запросов.

Ну и чтобы не складывать на калькуляторе размеры всех таблиц и индексов - размер базы данных PostgreSQL определим с помощью pg_database_size():

select pg_size_pretty(pg_indexes_size('my_table'));
-- Вывод (опять же для некоей тестовой базы данных):
pg_size_pretty
text
--------------
8137 kB

Больше информации про описанные функции - здесь: https://postgrespro.ru/docs/postgrespro/12/functions-admin#FUNCTIONS-ADMIN-DBOBJECT

понедельник, 1 июня 2020 г.

Неочевидное поведение функций конвертации даты

Продолжаем изучать неочевидное поведение функций php.

Рассмотрим такой код:

$var = strtotime(date('d.m.Y', time()));

Кажется, что здесь выполняется лишняя работа. Сначала мы берем таймштамп, возвращаемый функцией time(), и на его основе получаем строку времени определенного формата. Далее мы преобразуем полученную строку времени обратно в таймштамп. И логично, что таймштампы должны совпадать. Но нет.

Неочевидность в том, что строка времени в формате "d.m.Y" преобразуется в таймштамп начала дня, то есть "d.m.Y" аналогичен "d.m.Y 00:00:00".

Добавляем немного вывода и видим:

$ts = time();

$var = strtotime(date('d.m.Y', $ts));

var_dump(date('d.m.Y H:i:s', $ts));    // string(19) "01.06.2020 22:36:09"
var_dump(date('d.m.Y H:i:s', $var));   // string(19) "01.06.2020 00:00:00"

$varDayStart = strtotime(date('d.m.Y 00:00:00', $ts));
var_dump($var === $varDayStart);    // bool(true)


Таким образом, данный код можно считать одним из способов получения таймптампа начала текущего дня.

пятница, 10 апреля 2020 г.

Уникальные или неуникальные значения в массиве

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

Сразу к примеру:

$array = [1, 2, 3, 4, 3, 4, 5, 6, 6];

// Для любого из вариантов задачи нам потребуется знать
// сколько раз в массиве встречается каждое значение
$freqs = array_count_values($array);

// Массив уникальных значений - [1, 2, 5]
$unique = array_keys(
    array_filter(
        $freqs,
        function ($freq) { return 1 === $freq; }
    )
);

// Массив неуникальных значений - [3, 4, 6]
$nonunique = array_keys(
    array_filter(
        $freqs,
        function ($freq) { return 1 < $freq; }
    )
);

// Фильтрация исходного массива с оставлением только повторяющихся значений -[3, 4, 3, 4, 6, 6]
$allNonunique = array_filter(
    $array,
    function ($v) use ($freqs) { return 1 < $freqs[$v]; }
);


В общем-то всё, на досуге можете расширить этот код и задать количество появлений в виде переменной, а не фиксированного значения 1.

понедельник, 6 апреля 2020 г.

Неявное поведение DateTime::createFromFormat

Данный пост есть результат недавнего обсуждения в одном из php-каналов некоторого странного (как кажется изначально) поведения функции DateTime::createFromFormat.

Рассмотрим простейший код, надо отметить, что исследуемое поведение отмечается только 31-го числа каждого месяца (ну и плюс 29-30 для февраля):

$months = [1, 2, 3, 4];

foreach ($months as $month) {
    $dt = '2019-' . $month;
    echo $dt . ': ' . (\DateTime::createFromFormat('Y-m', $dt))->format('Y-m-d') . PHP_EOL;
}


Обратите внимание, что при создании объекта не указывается день. Так как день не указан, то разумно предположить (это же подтверждается в комментариях), что php берет в качестве дня текущий день запуска скрипта из системных настроек.

Таким образом, запуская скрипт, например, 31-го мая, получим такой вывод:

// ожидаемо, в январе есть 31 число
2019-1: 31.01.2019
// в феврале-2019 нет 29 (и 30 и 31) числа, потому 31-му февраля
// соответствует третье марта (а 29-му февраля - первое марта)
2019-2: 03.03.2019
// ожидаемо, в марте есть 31 число
2019-3: 31.03.2019
// в апреле нет 31 числа, и следующим после 30 апреля идет 1 мая
2019-4: 01.05.2019


Как видим, происходит не то, что ожидается, хотя в другие дни - всё работает нормально, и даже имеющиеся тесты будут проходить.

Можно, конечно, рассуждать о том, что раз день не указан, то может надо кидать эксепшен при создании объекта, но так как такого поведения нет, то придерживаемся мудрого принципа "Явное лучше, чем неявное" и повнимательней пишем свой код.

Всем здоровья)