Конструктор запросов базы данных иногда возвращает массив вместо объекта, запущенного как задание в очереди

TL; DR

Я столкнулся с заданиями, которые иногда кажутся неудачными, потому что регулярный запрос базы данных (с использованием стандартного конструктора запросов Laravel) не надежно возвращает объекты PHP, определенные в режиме выборки в моей config/database.php . Казалось, что иногда возвращают объект или массив, поэтому каким-то образом режим выборки изменяется (и даже изменяется обратно).

Детальный вопрос

Я запрашиваю внешнюю базу данных с помощью конструктора запросов Laravel. Режим выборки установлен для возврата объектов в config/database.php :

 'fetch' => PDO::FETCH_OBJ, 

… который в основном работает во многих местах моего приложения.

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

Кто-нибудь имеет представление о том, что может привести к такому результату?

Это происходит как с использованием обычного метода get() , так и с помощью chunk . Перезапуск очереди бегунов помогает избавиться от ошибки, но в конце концов она вернется!

Вот как выглядит мой код:

 $assetData = DB::connection('connection_name')->table('system') ->whereIn('system_id', $this->system_ids)->orderBy('system_id'); $emlAssetData->chunk(5000, function ($assetDataChunk) { foreach ($assetDataChunk AS $assetData) { if (!is_object($assetData)) { \Log::warning(__CLASS__.': '.json_encode($assetData)); } } $assetData->field; // Sometimes fails because result is an array, instead of an object } 

Я использую:

  • PHP 7.0
  • MySQL v5.7.16 v5.1.49
  • Laravel 5.5

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

 if (is_array($assetData)) { \Log::warning(__CLASS__." Converting array to object: ".json_encode($assetData)); $assetData = (object)$assetData; } 

Отладки довольно трудно сделать в этих условиях, потому что это происходит только в очереди 🙁

Обновление: 2017-12-11: более подробная информация о используемом SQL / кодексе

Чтобы обобщить более конкретные вещи, которые я здесь делаю, это может иметь отношение к моей проблеме:

  • Я не запрашиваю соединение по умолчанию, запущенное на localhost, но внешнюю базу данных (во внутренней сети)
  • Я не использую Eloquent, но обычный построитель запросов Laravel
  • Чтобы выполнить результат, я использую самозаписываемую пользовательскую функцию, которая вызывает функцию обратного вызова для каждой строки

Предпосылки: Это импортирует различные части устаревшей базы данных MySQL v5.1.49 в базу данных нашего проекта. Чтобы сделать это проще, вы указываете какое-то сопоставление столбцов (от старых до новых имен полей / таблиц), как это

 $columnMapping = collect([ 'system.system_id' => 'system_id', 'staud_colours.colour_name' => 'system_name', ]); 

Затем вы выполняете свой собственный запрос и сопоставляете старые поля с новыми полями с помощью вспомогательной функции:

 $items = \DB::connection('slave')->table('system') ->join('staud_colours', 'staud_colours.colour_id', '=', 'system.system_fremd_id') ->where('system.system_klasse', 'colours')->where('system.system_status', 1); $this->prepareQueryToBeInsertedToDB($items, $columnMapping, function ($insertData) { static::create($insertData); }); 

И вспомогательная функция, где вы видите все, if я добавил, потому что иногда я получаю массив вместо объектов:

 protected function prepareEmlQueryToBeInsertedToDB( Builder $items, Collection $columnMapping, Closure $callback, $orderBy = 'system.system_id' ) { // Step through each element of the mapping $items->orderBy($orderBy)->select($columnMapping->keys()->toArray()) ->chunk(5000, function ($items) use ($columnMapping, $callback, $items) { foreach ($items AS $item) { $values = $columnMapping->mapWithKeys(function ($item, $key) use ($item) { $key = Str::lower($key); if (Str::contains($key, ' as ')) { $column = array_reverse(explode(' as ', $key))[0]; } else { $column = substr(strrchr($key, "."), 1); } if (!$item) { \Log::error("Received damaged item from slave db: ".json_encode($item)); } if (is_array($item)) { $item = (object)$item; } if (!property_exists((object)$item, $column)) { \Log::error("{$column} does not exist on item from slave db: ".json_encode($item)); } $value = $item->$column; return [$item => $value]; }); if (!$values || $values->isEmpty()) { info('No values: '.json_encode($values)); } // Now call the callback method for each item, passing an well prepared array in format: // column_name => value // so that it can be easily be used with something like static::create() $callback($values->toArray()); } }); } с protected function prepareEmlQueryToBeInsertedToDB( Builder $items, Collection $columnMapping, Closure $callback, $orderBy = 'system.system_id' ) { // Step through each element of the mapping $items->orderBy($orderBy)->select($columnMapping->keys()->toArray()) ->chunk(5000, function ($items) use ($columnMapping, $callback, $items) { foreach ($items AS $item) { $values = $columnMapping->mapWithKeys(function ($item, $key) use ($item) { $key = Str::lower($key); if (Str::contains($key, ' as ')) { $column = array_reverse(explode(' as ', $key))[0]; } else { $column = substr(strrchr($key, "."), 1); } if (!$item) { \Log::error("Received damaged item from slave db: ".json_encode($item)); } if (is_array($item)) { $item = (object)$item; } if (!property_exists((object)$item, $column)) { \Log::error("{$column} does not exist on item from slave db: ".json_encode($item)); } $value = $item->$column; return [$item => $value]; }); if (!$values || $values->isEmpty()) { info('No values: '.json_encode($values)); } // Now call the callback method for each item, passing an well prepared array in format: // column_name => value // so that it can be easily be used with something like static::create() $callback($values->toArray()); } }); } 

Начиная с версии 5.4, Laravel больше не поддерживает настройку режима выборки PDO через config / database.php . По умолчанию структура устанавливает режим выборки в PDO::FETCH_OBJ , хотя мы можем переопределить этот параметр, прослушивая событие StatementPrepared :

 Event::listen(StatementPrepared::class, function ($event) { $event->statement->setFetchMode(PDO::FETCH_ASSOC); }); 

Кажется возможным, что определенное задание в очереди подписывается на это событие и изменяет режим выборки. Если мы начнем работу с менеджером очереди с помощью queue:work командой консоли Artisan, слушатель задерживается для любых последующих заданий, потому что эта команда загружает приложение один раз для всех заданий, которые обрабатывает рабочий. Это объясняет, почему перезагрузка рабочего исправляет проблему временно.

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

Сначала вы должны заставить его работать. Добавьте if / else_if, который проверяет, является ли результат объектом или массивом, и извлекает данные соответствующим образом. Если вы можете абстрагировать это с помощью класса BaseDB, который все запросы будут использовать, еще лучше.

Для второй фазы поместите некоторые записи в код, чтобы найти, какое задание очереди возвращает массив и вызывает проблему. Как упоминал @cyrossignol, может быть какой-то прослушиватель событий, который запускается скриптом. Посмотрите на это. Также имейте в виду, что проблема может быть связана с MySQL. Возможно, какое-то условие вызвано в db вашим запросом, а какой-то код исключения запускается и возвращает массив вместо объекта.

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