Грязная работа: отладка до последней минуты
12 мая 2022 г.PHP/Laravel так же сексуальна, как сандалии с носками. Это основной продукт питания миллионов посредственных веб-программистов. Это был мой вердикт с нулевым опытом работы с ним. Однако я не высказывал своего мнения, потому что мне не приходилось трогать PHP-код голыми руками. Пока Workwize не нанял меня. В качестве ведущего разработчика.
Вы ожидаете, что ведущий разработчик будет самым осведомленным среди разработчиков. Показывайте пример, говорят рекрутеры. Будь альфа-самцом или самкой стаи, подсознательно имеют они в виду.
Мне нужно было наверстать упущенное. Заполнение базы данных, фабрики, весь Eloquent ORM. Но чтение документации не генерирует код, а это высшая мера нашего прогресса. Я выбрал простую задачу, чтобы разогреть внутренние мышцы: вывести общую сумму, потраченную клиентом.
Конечная точка уже позаботилась об этом, но вернула значение, основанное на устаревшем расчете. Модель ценообразования изменилась, поскольку она стала более сложной. Все, что мне нужно было сделать, подумал я, это обновить расчет. База данных выглядит достойно. Достаточно заменить запрос несколькими строками кода.
Отладка
Я провел тесты, сделал минимальное изменение и снова провел тесты. Это как поздороваться с кодовой базой, которую я никогда раньше не видел. Двести с чем-то тестов за двадцать секунд. Хорошо, мы можем быть друзьями. Давайте сосредоточимся на моей задаче и запустим только testTotalSpent()
. Девять секунд. Что за? Попробовал снова. (Есть ли у вас привычка не верить своим глазам и пробовать то же самое снова, скрестив пальцы?) Девять секунд. Раздражающий. Но я решил решить другую проблему, как я понял.
Мой первый запрос Eloquent был похож на «смотри, мама, без рук». Однако в нем были синтаксические ошибки. Я случайно пропустил несколько скобок. И обнаружил несколько особенностей базы. customers.customer_id
относится к… нет, не к самому себе (что бы это вообще значило?), а к users.id
. Некоторые ссылки даже меняются местами, что нормально, если это отношение один к одному. Однажды они вернутся и прострелят тебе ногу. Некоторые поля являются остатками оптимистичного дизайнерского решения в старые времена, например, products.price
. Они больше не используются. Наверное.
Ни у кого не хватило смелости удалить их и полагаться только на новую таблицу «цен». Код выполняет некоторую гимнастику для обработки таких случаев. Я серьезно подумывал о рефакторинге всей модели ценообразования и продукта. От этого меня отговорил давний друг. Одна из причин, почему я называю его другом.
Я приручил запрос до такой степени, что он спешил от таблицы к сводной таблице, следовал по всем соединениям, суммировал расходы и ничего не возвращал. Я проверил базу данных. Действительно пустой. Я был неправ, и запрос был правильным. Один из унизительных аспектов программирования. Но я вспомнил тестовый набор, названный парой модельных заводов. Где те записи? Унесенные ветром?
Какой я глупый! Каждому тесту нужен чистый лист, поэтому записи, вставленные ранее, не будут мешать моей текущей выполняемой функции. Это реализуется путем запуска каждого теста в транзакции и его отката, как только тест завершит свою жизнь, независимо от того, прошел он или нет. (В тестировании есть духовный элемент.) Имеет смысл запускать каждый тест отдельно. Но можем ли мы сделать исключение на этот раз? Зафиксируйте транзакцию и сохраните созданные данные, чтобы я мог их проверить.
Это понимание пришло ко мне не бесплатно. Мне пришлось запускать тест столько раз, чтобы прийти к такому выводу, и каждый раз отставание, те девять секунд, о которых я упоминал ранее. Можете себе представить степень моего разочарования: я пожаловался на это младшему коллеге. И правильному человеку, как оказалось. Он знал, чем занят бегун-испытатель. Он выполнил весеннюю уборку (усечение таблиц, перенос данных, стирание пыли с полок), прежде чем даже прислушаться к моим командам.
Теперь у меня было две причины, чтобы остановить одержимую даму, подметающую перед моей базой данных, и отключить ее манию уборки. Чтобы ускорить выполнение теста и, наконец, получить данные, которые я создал. Две причины, вполне оправданные с технической точки зрения и с точки зрения производительности. И у меня был третий, который я предпочитал держать при себе.
Хорошо выглядеть для команды
На тот момент я уже две недели исправлял ошибку. Тривиальное изменение в две строчки, моя задница. Джуниор помог мне с вопросом, который я мог бы решить сам. Никто не спорил со мной о том, на что я потратил свое время, и моя тарелка была полна другой рыбы для жарки. Я все еще чувствовал давление, чтобы иметь что-то, чтобы показать для моей должности, ведущего разработчика. Раз я не могу исправить ошибку за день (вполне вероятно, к сожалению), я должен принести что-то еще на стол. Ускорение выполнения тестов для всех моих товарищей по команде звучало как идеальный первый вклад.
У моего младшего коллеги уже было простое решение. Он закомментировал строку в тестовом примере, строку, отвечающую за всю магию очистки, в которой говорится «использовать RefreshTable». Ему просто нужно было обратить внимание и никогда не совершать это изменение. Если бы он это сделал случайно, он мог бы исправить это в следующем коммите. Не очень элегантно. Я могу сделать лучше.
Через четыре коммита это сработало на моей машине. Я по локоть копался в конфигурации phpunit. Наконец, столы стояли передо мной голыми со всеми их данными. Теперь было легко понять, в чем я ошибся. Таблица OrderItem
содержала так много полей, что они не поместились на моем экране. Я прокрутил вправо, чтобы посмотреть, что есть, затем просто проигнорировал то, что не понял. Поля отображаются не по алфавиту, а в порядке создания. Некоторые важные поля были добавлены позже, например, «цена», значение которой копируется из таблицы «Цены» во время размещения заказа.
Я набрал аккуратный запрос Eloquent, всего шесть строк, несколько «join», несколько «where» и «groupBy». Позвольте мне воспроизвести это здесь, чтобы сохранить на вечность
```php
ОрдерИтем::запрос
->join('заказы', 'orders.customer_id', '=', 'customer.id')
->join('order_items', 'order_items.order_id', '=', 'orders.id')
->where('order_items.created_at', '>=', $startDate)
->where('customer.id', $customer->id)
->сумма('order_item.price')
->groupBy('order_item.type');
Я уловил суть структуры данных. Это момент, когда в кино начинает играть симфонический оркестр. Главный герой медленно поднимает руку. И нажимает Enter на клавиатуре. Вырезать ему лицо. Вы видите ожидание. Первые признаки победы. Потом что-то пойдет не так. Его лицо тает в недоверии. Переход к экрану. Курсор мигает. Данные печатаются. totalSpent: [Купить => 3]
. Очевидно неправильно. Нет «Аренда», только «Купить»? А месячных расходов не может быть три.
В SQL мы доверяем, в Eloquent нет. В построителе запросов есть метод toSql(). Это слишком умно (или слишком глупо, в зависимости от того, как вы на это смотрите) и возвращает вопросительный знак для параметров. Это не повод отчаиваться, getBindings()
даст вам нужные параметры. Несколько строк волшебства PHP из Stackoverflow объединяют эти два в оператор SQL.
```javascript
$query = str_replace(массив('?'), массив('\'%s\''), $builder->toSql());
$query = vsprintf($query, $builder->getBindings());
Однако этот метод не безупречен. Я попытался выполнить сгенерированный запрос. Mysql пожаловался на последнюю строку, на предложение group-by. Простой. Я удалил строку. Ведь я хотел посмотреть выбранные записи. Группировка их здесь второстепенна. Жалобы у гада не иссякли. На этот раз о предложении select. Понятно, без группировки нет смысла агрегировать цены. Удалите и эту строку. Замените его простым select *
, это никогда не повредит. Он также не выполняется, так как не указана таблица. Хорошо, выберите order_items.*
. А теперь заткнись и покажи мне, что у тебя есть.
Записи, о боже, записи, которые я так сильно хотел увидеть, появились на экране. Я в принципе готов, подумал я, алгоритм работает. Осталось только убедить Eloquent сделать то, что уже умеет мой сырой SQL.
Eloquent, вероятно, зародился за стенами Хогвартса. Иногда мне даже казалось, что сам Вальдеморт закодировал ее первую версию. Я мог правильно получить записи на стороне Eloquent. Но когда я попытался получить их сумму, он вернул только первый. Общая стоимость купленных товаров была правильно рассчитана. Но это не дало мне, сколько было арендовано или арендовано.
Вот моя ситуация тогда в цифрах
```javascript
number_of_commits_ahead_of_main: 8
number_of_commits_behind_main: 32
days_passed_fixing_the_damn_thing: 19
pressure_that_i_should_show_something: 1,8 бар
Я решил начать заново. Новая ветка, новая жизнь. Вместо метода select()
моей первой попытки я использовал
```javascript
->selectRaw('order_items.type, sum(order_items.price * order_items.quantity)'
Да, я понял, что забыл умножить цену на количество. Эта ошибка осталась скрытой, потому что мои тестовые данные содержали только по одному элементу каждого элемента. Моя предыдущая версия работала только для количества один.
Эта новая версия дала правильные результаты, и Eloquent вернул все объекты. Победа. Дело закрыто, ветка объединена с основной. Я не чувствовал заслуженного удовлетворения. Недостаточно исправить ошибку, вам нужен урок, который вы усвоили. Вам нужна автомобильная погоня и, наконец, преступник пойман. Меня еще мучил один вопрос. Почему Eloquent вернул только одну запись?
Я знаю только часть ответа. Когда я написал эти строки
```php
ЭлементЗаказа::выбрать()
->groupBy('order_item.type')
->сумма('order_item.price')
(игнорируйте проблему с количеством), на самом деле это не имело смысла с точки зрения SQL. Вот что написано на простом английском. Выберите все поля позиций заказа, сгруппируйте записи по типу и просуммируйте их цены. Ничего не сказано о том, что делать с другими полями? Группировать по дате создания тоже? Нет. Объединить их? Точно нет. Возьмем первую, последнюю, медиану?
Это именно то, на что жаловался Mysql. Сколько раз я должен усвоить древний урок?! Прочитайте все сообщение об ошибке и поймите, что оно говорит.
Как получилось, что Eloquent удалось выполнить этот запрос и вернуть первую запись? Почему это не дало мне ошибку SQL? Это та часть, на которую я не могу ответить. Я подозреваю, что Eloquent занимается черной магией. В некоторых пограничных случаях он создает необработанный SQL, отличный от того, который он фактически выполняет. Но у меня нет на это доказательств.
Эпилог
Когда я писал эту историю, я хотел вставить сюда фактическое сообщение об ошибке. Который я не читал до двух минут назад. Вот.
``` ударить
Код ошибки: 1055. Выражение № 1 списка SELECT не находится в предложении GROUP BY и содержит неагрегированный столбец «order_items.id», который функционально не зависит от столбцов в предложении GROUP BY; это несовместимо с sql_mode=only_full_group_by
Есть параметр sql_mode, который по умолчанию имеет значение only_full_group_by. Но посмотрите на этот фрагмент из моего config/database.php
```php
'связи' => [
'mysql' => [
'строгий' => ложь,
Он указывает Mysql вести себя более расслабленно. Это включает пропуск проверки предложения group-by. Красноречивый был не виноват здесь. Как я уже говорил ранее: прочитайте сообщение об ошибке.
Эта статья была впервые опубликована [здесь].
Оригинал