Как потоковое создание файла JSON?

Я пытаюсь создать JSON файл из большого дампа запроса базы данных и работает, когда я устанавливаю LIMIT в 100000 строк, возвращаемых, но когда я хочу, чтобы все строки были возвращены, он просто переходит к ошибке 502 (запрос страницы был отменен, потому что потребовалось слишком много времени для завершения). Хотите узнать, есть ли способ упростить создание файла JSON в битах с использованием php или если есть библиотека, которая позволит мне создать json файл по частям?

В основном я запускаю файл.php здесь, чтобы попытаться получить все заказы в json-формате из woocommerce, поскольку плагин, который я приобрел "CSV Import Suite", не работает при импорте заказов, он просто остается в очереди.

Итак, я решил попробовать экспортировать все заказы самостоятельно, но продолжаю удалять 502 страницу ошибки, и он никогда не создает файл.json, так что я думаю, что мне нужно каким-то образом передать это сообщение. Любая помощь по этому поводу будет оценена по достоинству...

ini_set('memory_limit', '-1');
ini_set('max_execution_time', '-1');
set_time_limit(0);
error_reporting(E_ALL);
ob_implicit_flush(TRUE);
ob_end_flush();

global $wpdb, $root_dir;

if (!defined('ABSPATH'))
    $root_dir = dirname(__FILE__) . '/';
else
    $root_dir = ABSPATH;


$download = isset($_GET['download']);

// Allows us to use WP functions in a .php file without 404 headers!
require_once($root_dir . 'wp-config.php');
$wp->init();
$wp->parse_request();
$wp->query_posts();
$wp->register_globals();

if (empty($download))
    $wp->send_headers();

// exclude
$exclude_post_statuses = array('trash', 'wc-refunded', 'wc_cancelled');


$start_date = !empty($_GET['start_date']) ? DateTime::createFromFormat('Y-m-d', $_GET['start_date']) : '';
$end_date = !empty($_GET['end_date']) ? DateTime::createFromFormat('Y-m-d', $_GET['end_date']) : '';


$order_db = array(
    'columns' => array(
        'p' => array('ID', 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_title', 'post_excerpt', 'post_status', 'comment_status', 'ping_status', 'post_password', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt', 'post_content_filtered', 'post_parent', 'guid', 'menu_order', 'post_type', 'post_mime_type', 'comment_count'),
        'pm' => array('meta_id', 'post_id', 'meta_key', 'meta_value'),
        'oi' => array('order_item_id', 'order_item_name', 'order_item_type', 'order_id'),
        'oim' => array('meta_id', 'order_item_id', 'meta_key', 'meta_value')
    )
);

$select_data = '';
$total_columns = count($order_db['columns']);
$i = 1;

foreach($order_db['columns'] as $column_key => $columns)
{
    $select_data .= implode(', ', array_map(
        function ($v, $k) { return $k . '.' . $v . ' AS ' . $k . '_' . $v; },
        $columns,
        array_fill(0, count($columns), $column_key)
    ));

    if ($i < $total_columns)
        $select_data .= ', ';

    $i++;
}

// HUGE DATABASE DUMP HERE, needs to be converted to JSON, after getting all columns of all tables...
$orders_query = $wpdb->get_results('
    SELECT ' . $select_data . '
    FROM ' . $wpdb->posts . ' AS p
    INNER JOIN ' . $wpdb->postmeta . ' AS pm ON (pm.post_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_items AS oi ON (oi.order_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta AS oim ON (oim.order_item_id = oi.order_item_id)
    WHERE p.post_type = "shop_order"' . (!empty($exclude_post_statuses) ? ' AND p.post_status NOT IN ("' . implode('","', $exclude_post_statuses) . '")' : '') . (!empty($start_date) ? ' AND post_date >= "' . $start_date->format('Y-m-d H:i:s') . '"' : '') . (!empty($end_date) ? ' AND post_date <= "' . $end_date->format('Y-m-d H:i:s') . '"' : '') . '
    ORDER BY p.ID ASC', ARRAY_A);

$json = array();

if (!empty($orders_query))
{
    foreach($orders_query as $order_query)
    {
        if (!isset($json[$order_query['p_post_type']], $json[$order_query['p_post_type']][$order_query['p_post_name']]))
            $json[$order_query['p_post_type']][$order_query['p_post_name']] = array(
                'posts' => array(),
                'postmeta' => array(),
                'woocommerce_order_items' => array(),
                'woocommerce_order_itemmeta' => array()
            );

        if (!empty($order_query['p_ID']))
            $json[$order_query['p_post_type']][$order_query['p_post_name']]['posts'][$order_query['p_ID']] = array_filter($order_query, function($k) {
                $is_p = strpos($k, 'p_');
                return $is_p !== FALSE && empty($is_p);
            }, ARRAY_FILTER_USE_KEY);

        if (!empty($order_query['pm_meta_id']))
            $json[$order_query['p_post_type']][$order_query['p_post_name']]['postmeta'][$order_query['pm_meta_id']] = array_filter($order_query, function($k) {
                $is_pm = strpos($k, 'pm_');
                return $is_pm !== FALSE && empty($is_pm);
            }, ARRAY_FILTER_USE_KEY);

        if (!empty($order_query['oi_order_item_id']))
            $json[$order_query['p_post_type']][$order_query['p_post_name']]['woocommerce_order_items'][$order_query['oi_order_item_id']] = array_filter($order_query, function($k) {
                $is_io = strpos($k, 'oi_');
                return $is_io !== FALSE && empty($is_io);
            }, ARRAY_FILTER_USE_KEY);


        if (!empty($order_query['oim_meta_id']))
            $json[$order_query['p_post_type']][$order_query['p_post_name']]['woocommerce_order_itemmeta'][$order_query['oim_meta_id']] = array_filter($order_query, function($k) {
                $is_oim = strpos($k, 'oim_');
                return $is_oim !== FALSE && empty($is_oim);
            }, ARRAY_FILTER_USE_KEY);
    }
}

// Downloading or viewing?
if (!empty($download))
{
    // Outputs json in a textarea for you to copy and paste into a .json file for import...

    if (!empty($json))
    {
        $filename = uniqid('orders_') . '.json';

        $fp = fopen($filename, 'w');
        fwrite($fp, json_encode($json));
        fclose($fp);


        $size   = filesize($root_dir . '/' . $filename);
        header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');
        header("Content-Disposition: attachment; filename=\"" . $filename . "\""); 
        header('Content-Transfer-Encoding: binary');
        header('Connection: Keep-Alive');
        header('Expires: 0');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header('Pragma: public');
        header('Content-Length: ' . $size);

        readfile($root_dir . '/' . $filename);
    }

}
else
{
    // Outputs json in a textarea for you to copy and paste into a .json file for import...
    if (!empty($json))
        echo '<textarea cols="200" rows="50">', json_encode($json), '</textarea>';
}

Созданный файл JSON может содержать более 500 МБ и, возможно, даже до 1 Гб данных. Итак, я считаю, что в PHP заканчивается память здесь, и ее нужно обрабатывать по частям как-то, либо в фоновом режиме, либо полностью, не попадая в пределы памяти php. Я считаю, что ограничение памяти установлено на 1024 МБ, что довольно высоко, но недостаточно высоко, и tbh, для чего я делаю, я не думаю, что у нас может быть достаточно памяти для выполнения операции как есть. Что-то нужно изменить в том, как я обрабатываю json и/или загружаю его. И я не хочу создавать несколько json файлов, пожалуйста, всего 1 файл JSON.

Ответы

Ответ 1

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

    // HUGE DATABASE DUMP HERE, needs to be converted to JSON, after getting all columns of all tables...
    echo 'Start Time: '. date("Y-m-d H:i:s");
    echo ' Memory Usage: ' . (memory_get_usage()/1048576) . ' MB \n';

    $orders_query = $wpdb->get_results('
        SELECT ' . $select_data . '
        FROM ' . $wpdb->posts . ' AS p
        INNER JOIN ' . $wpdb->postmeta . ' AS pm ON (pm.post_id = p.ID)
        LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_items AS oi ON (oi.order_id = p.ID)
        LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta AS oim ON (oim.order_item_id = oi.order_item_id)
        WHERE p.post_type = "shop_order"' . (!empty($exclude_post_statuses) ? ' AND p.post_status NOT IN ("' . implode('","', $exclude_post_statuses) . '")' : '') . (!empty($start_date) ? ' AND post_date >= "' . $start_date->format('Y-m-d H:i:s') . '"' : '') . (!empty($end_date) ? ' AND post_date <= "' . $end_date->format('Y-m-d H:i:s') . '"' : '') . '
        ORDER BY p.ID ASC', ARRAY_A);

    echo 'End Time: '. date("Y-m-d H:i:s");
    echo ' Memory Usage: ' . (memory_get_usage()/1048576) . ' MB \n';
    die('Finished');

    $json = array();

Вышеупомянутое поможет вам узнать, сколько памяти используется, до этого момента. Если он не работает до того, как он напечатает "Готово", мы знаем, что это не проблема json. Если скрипт работает нормально, мы можем сначала создать файл csv, а не json. Поскольку вы выполняете запрос select (на данный момент), вам не нужно вставлять json файл, который вам нужен. Плоскую структуру можно достичь, просто создав файл CSV.

$csvFile = uniqid('orders') . '.csv';
$fp = fopen($csvFile, 'w');
if (!empty($orders_query))
{
    $firstRow = true;
    foreach($orders_query as $order_query)
    {
        if(true === $firstRow) {
            $keys = array_keys($order_query);
            fputcsv($fp, $order_query);
            $firstRow = false;
        }

        fputcsv($fp, $order_query);
    }
}
fclose($fp);

Если вышеописанное работает отлично, у вас есть как минимум файл csv для работы.

На данный момент я не уверен, насколько сложна ваша структура данных вложенной. Например, сколько различных значений существует для "p_post_type" и "p_post_name", которые у вас есть. Вам может потребоваться проанализировать файл csv и создать несколько json файлов для каждого ['p_post_type'] ['p_post_name'] ['posts'], ['p_post_type'] ['p_post_name'] ['posts'], ['p_post_type '] [' p_post_name '] [' woocommerce_order_items '] и [' p_post_type '] [' p_post_name '] [' woocommerce_order_itemmeta '].

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

Если количество json файлов много, я хотел бы знать, какова цель создания такого огромного одного json файла. Если экспорт является проблемой, проблемой может быть и проблема, особенно глотая такой огромный json файл в памяти. Я полагаю, что если целью создания json файла является его импорт в какой-то форме, на каком-то этапе в будущем, я думаю, вам, возможно, придется рассмотреть возможность просто иметь csv файл, который вы используете для фильтрации всех требуется в этот момент времени.

Надеюсь, это поможет.

ДАЛЬНЕЙШЕЕ ОБНОВЛЕНИЕ

Мне кажется, что $wpdb-> get_results использует mysqli_query/mysql_query (в зависимости от вашей конфигурации) для получения результатов. См. Документы Wordpress query. Это не эффективный способ хранения данных таким образом. Я полагаю, что вы можете потерпеть неудачу на этом этапе ($wpdb-> get_results). Я предлагаю вам выполнить запрос без использования $ wpdb. Существует концепция небуферизованного запроса, когда требуется большой поиск данных, что очень мало влияет на память. Дополнительную информацию можно найти здесь mysql unbuffering.

Даже если вы закончите этот момент, вы все равно столкнетесь с проблемами памяти из-за того, как вы храните все в переменной $ json, которая поглощает много вашей памяти. $ json - это массив, и было бы интересно узнать, как работает массив PHP. Массивы PHP являются динамическими, и они не выделяют дополнительную память каждый раз, когда добавляется новый элемент, поскольку это будет очень медленным. Вместо этого он увеличивает размер массива до двух, что означает, что всякий раз, когда предел исчерпан, он увеличивает предел массива в два раза по сравнению с его текущим пределом и в процессе пытается увеличить объем памяти до двух раз. Это было меньше проблем с PHP 7, поскольку они внесли некоторые серьезные изменения в ядро php. Поэтому, если у вас есть данные 2 ГБ, которые могут потребоваться для хранения в $ json, сценарий может легко распределить где угодно между памятью 3-4 ГБ, в зависимости от того, когда он достигнет предела. Более подробную информацию можно найти здесь, в php-массиве и как работает PHP-память

Если вы считаете накладные расходы $ orders_query, который представляет собой массив в сочетании с накладными расходами в $ json, он довольно существенен из-за того, как работает массив PHP.

Вы также можете попытаться создать другую базу данных B. Поэтому, пока вы читаете из базы данных A, вы одновременно начинаете записывать данные в базу данных B. В итоге у вас есть база данных B со всеми данными в ней с мощью MySQL. Вы также можете вставлять одни и те же данные в MongoDB, который будет молниеносно, и может помочь вам с json-гнездом, которым вы пользуетесь. MongoDB предназначен для эффективной работы с большими наборами данных.

JSON STREAMING SOLUTION

Во-первых, я хотел бы сказать, что потоковая передача является последовательным/линейным процессом. Таким образом, у него нет памяти о том, что было добавлено до этого момента или что будет добавлено после этого момента времени. Он работает в небольших кусках, и именно по этой причине он настолько эффективен для памяти. Поэтому, когда вы на самом деле пишете или читаете, ответственность лежит на скрипте, что он поддерживает определенный порядок, который говорит о том, что вы пишете/читаете свой собственный json, поскольку потоковая передача только понимает текст и не имеет понятия о том, что такое json и не будет беспокоить себя в письменной форме/чтении правильной.

Я нашел библиотеку на github https://github.com/skolodyazhnyy/json -stream, которая поможет вам в достижении того, чего вы хотите. Я экспериментировал с кодом, и я вижу, что он будет работать для вас с некоторыми изменениями в вашем коде.

Я собираюсь написать для вас псевдо-код.

//order is important in this query as streaming would require to maintain a proper order.
$query1 = select distinct p_post_type from ...YOUR QUERY... order by p_post_type;
$result1 = based on $query1; 

$filename = 'data.json';
$fh = fopen($filename, "w");
$writer = new Writer($fh);
$writer->enter(Writer::TYPE_OBJECT);  

foreach($result1 as $fields1) {
    $posttype = $fields1['p_post_type'];
    $writer->enter($posttype, Writer::TYPE_ARRAY); 

    $query2 = select distinct p_post_name from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype order by p_post_type,p_post_name;
    $result2 = based on $query2;

    foreach($result2 as $fields2) {
        $postname = $fields1['p_post_name'];
        $writer->enter($postname, Writer::TYPE_ARRAY); 

        $query3 = select ..YOUR COLUMNS.. from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype and p_post_name=$postname where p_ID is not null order by p_ID;
        $result3 = based on $query3;
        foreach($result2 as $field3) {
            $writer->enter('posts', Writer::TYPE_ARRAY); 
            // write an array item
            $writer->write(null, $field3);
        }
        $writer->leave(); 

        $query4 = select ..YOUR COLUMNS.. from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype and p_post_name=$postname where pm_meta_id is not null order by pm_meta_id;
        $result4 = based on $query4;
        foreach($result4 as $field4) {
            $writer->enter('postmeta', Writer::TYPE_ARRAY); 
            // write an array item
            $writer->write(null, $field4);
        }
       $writer->leave(); 

        $query5 = select ..YOUR COLUMNS.. from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype and p_post_name=$postname where oi_order_item_id is not null order by oi_order_item_id;
        $result5 = based on $query5;
        foreach($result5 as $field5) {
            $writer->enter('woocommerce_order_items', Writer::TYPE_ARRAY); 
            // write an array item
            $writer->write(null, $field5);
        }
        $writer->leave(); 

        $query6 = select ..YOUR COLUMNS.. from ...YOUR QUERY... YOUR WHERE ... and p_post_type= $posttype and p_post_name=$postname where oim_meta_id is not null order by oim_meta_id;
        $result6 = based on $query6;
        foreach($result6 as $field6) {
            $writer->enter('woocommerce_order_itemmeta', Writer::TYPE_ARRAY); 
            // write an array item
            $writer->write(null, $field5);
        }
        $writer->leave(); 

    }
$writer->leave(); 
fclose($fh);

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

Ответ 2

Создание файла

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

Мы собираемся генерировать запрос несколько раз, поэтому я выделил ваш запрос в функцию. Я пропустил прохождение требуемых параметров, хотя (или сделав их глобальными, если хотите) для краткости, поэтому вам нужно заставить это работать самостоятельно.

function generate_query($select, $limit = null, $offset = null) {
    $query = 'SELECT ' . $select . '
    FROM ' . $wpdb->posts . ' AS p
    INNER JOIN ' . $wpdb->postmeta . ' AS pm ON (pm.post_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_items AS oi ON (oi.order_id = p.ID)
    LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta AS oim ON (oim.order_item_id = oi.order_item_id)
    WHERE p.post_type = "shop_order"' . (!empty($exclude_post_statuses) ? ' AND p.post_status NOT IN ("' . implode('","', $exclude_post_statuses) . '")' : '') . (!empty($start_date) ? ' AND post_date >= "' . $start_date->format('Y-m-d H:i:s') . '"' : '') . (!empty($end_date) ? ' AND post_date <= "' . $end_date->format('Y-m-d H:i:s') . '"' : '') . '
    ORDER BY p.ID ASC';

    if ($limit && $offset) {
        $query .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
    }

    return $query;
}

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

define('BATCH_COUNT', 500);

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

$orders_count = $wpdb->get_col(generate_query('COUNT(*)'));
$iteration_count = ceil($orders_count / BATCH_COUNT);

В результате мы хотели бы иметь огромную строку JSON внутри файла результата. Поскольку с каждой итерацией у нас будет отдельный JSON, содержащий массив объектов, мы просто разделим [ и ] с каждой стороны строки JSON и поместим эту строку в файл.

Итоговый код:

define('FILE', 'dump.json');
file_put_contents(FILE, '[');

for ($i = 0; $i < $iteration_count; $i++) {
    $offset = $i * BATCH_COUNT;

    $result = $wpdb->get_results(
        generate_query($select_data, BATCH_COUNT, $offset),
        ARRAY_A
    );

    // do additional work here, add missing arrays etc.
    // ...

    // I assume here the $result is a valid array ready for
    // creating JSON from it
    // we append the result file with partial JSON

    file_put_contents(FILE, trim(json_encode($result), '[]'), FILE_APPEND);
}

file_put_contents(FILE, ']', FILE_APPEND);

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

Отправка файла

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

Я предполагаю, что вы используете Apache. Вы должны рассмотреть возможность использования SendFile и позволить Apache выполнять тяжелую работу за вас. Этот метод намного эффективнее при работе с огромными файлами. Этот метод очень прост, все, что вам нужно сделать, это передать путь к файлу в заголовке:

header('X-Sendfile: ' . $path_to_the_file);

Если вы используете Nginx, там также поддерживается XSendFile.

Этот метод не использует много памяти, не блокирует процесс PHP. Файл также не должен быть доступен в webroot. Я все время использую XSendFile, чтобы обслуживать 4K видео для аутентифицированных пользователей.

Ответ 3

Во-первых, вы должны задать себе вопрос: нужно ли мне самому писать базу данных?

Если нет, то вы можете просто использовать некоторую услугу, которая будет работать для вас. Mysqldump-php должен уметь выполнять эту работу.

Тогда вы можете просто:

include_once(dirname(__FILE__) . '/mysqldump-php-2.0.0/src/Ifsnop/Mysqldump/Mysqldump.php');
$dump = new Ifsnop\Mysqldump\Mysqldump('mysql:host=localhost;dbname=testdb', 'username', 'password');
$dump->start('storage/work/dump.sql');

Это должно создать файл .sql. Однако вам нужен файл json. Однако это не проблема. Этот инструмент выполнит всю оставшуюся работу: http://www.csvjson.com/sql2json

Вы также можете найти исходный код sql2json на github: https://github.com/martindrapeau/csvjson-app

Ответ 5

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

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

Для последнего кеша может использоваться база данных nosql. Мое решение будет использовать кеш и, в частности, memcache:

class Cache {

    private $cache;

    public function __construct($cache)
    {
        $this->cache = $cache;
    }

    public function addPostName($postName)
    {
        $this->addKeyToJsonObject('postNames', $postName);
    }

    public function addKeyToJsonObject($rootName, $key)
    {
        $childNames = $this->cache->get($rootName);
        if($childNames === false) {
            $this->cache->set($rootName, [$key]);
        }
        else {
            $childNamesList = $childNames;
            // not found
            if(array_search($key, $childNamesList) === false) {
                $childNamesList[] = $key;
                $this->cache->set($rootName, $childNamesList);
            }
        }
    }

    public function getPostNames()
    {
        return $this->cache->get('postNames');
    }
    public function set($key, $value) {
        $this->cache->add($key, $value);
    }

    public function addPostIdsByNameAndType($postName, $type, $pid)
    {
        $this->addKeyToJsonObject($postName . '-' . $type, $pid);
    }

    public function getPostIdsByNameAndType($postName, $type)
    {
        return $this->cache->get($postName . '-' . $type);
    }

    public function addPostValueByNameTypeAndId($postName, $type, $pid, $value)
    {
        $this->cache->set($postName . '-' . $type . '-' . $pid, $value);
    }

    public function getPostValueByNameTypeAndId($postName, $type, $pid)
    {
        return $this->cache->get($postName . '-' . $type . '-' . $pid);
    }
}

а потом:

$memcache = new Memcache();
$memcache->connect('127.0.0.1', 11211) or die ("Could not connect");

$memcache->flush();

$cache = new Cache($memcache);

header('Content-disposition: attachment; filename=file.json');
header('Content-type: application/json');
echo '{"shop_order":{';

function getResultSet($wpdb, $offset = 1) {
    return $wpdb->get_results('
    SELECT ' . $select_data . '
FROM ' . $wpdb->posts . ' AS p
INNER JOIN ' . $wpdb->postmeta . ' AS pm ON (pm.post_id = p.ID)
LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_items AS oi ON (oi.order_id = p.ID)
LEFT JOIN ' . $wpdb->prefix . 'woocommerce_order_itemmeta AS oim ON (oim.order_item_id = oi.order_item_id)
WHERE p.post_type = "shop_order"' . (!empty($exclude_post_statuses) ? ' AND p.post_status NOT IN ("' . implode('","', $exclude_post_statuses) . '")' : '') . (!empty($start_date) ? ' AND post_date >= "' . $start_date->format('Y-m-d H:i:s') . '"' : '') . (!empty($end_date) ? ' AND post_date <= "' . $end_date->format('Y-m-d H:i:s') . '"' : '') . '
ORDER BY p.ID ASC LIMIT 1000 OFFSET ' . $offset, ARRAY_A);

}
$offset = 1;

$orders_query = getResultSet($wpdb, 1);
while(!empty($orders_query)) {
    cacheRowData($cache, $orders_query);
    $offset = $offset + 1000;
    $orders_query = getResultSet($wpdb, $offset);
}

outputRowData($cache);

function cacheRowData($cache, $orders_query)
{
    foreach($orders_query as $order_query) {

        if(empty($order_query)) { continue; }
        $cache->addPostName($order_query['p_post_name']);

        // posts
        if (!empty($order_query['p_ID'])) {
            $cache->addPostIdsByNameAndType($order_query['p_post_name'],'posts', $order_query['p_ID']);

            $value = array_filter($order_query, function($k) {
                $is_p = strpos($k, 'p_');
                return $is_p !== FALSE && empty($is_p);
            }, ARRAY_FILTER_USE_KEY);
            $cache->addPostValueByNameTypeAndId($order_query['p_post_name'],'posts', $order_query['p_ID'], $value);
        }
        if (!empty($order_query['pm_meta_id'])) {
        $cache->addPostIdsByNameAndType($order_query['p_post_name'],'postmeta', $order_query['pm_meta_id']);

        $value = array_filter($order_query, function($k) {
            $is_pm = strpos($k, 'pm_');
            return $is_pm !== FALSE && empty($is_pm);
        }, ARRAY_FILTER_USE_KEY);
        $cache->addPostValueByNameTypeAndId($order_query['p_post_name'],'postmeta', $order_query['pm_meta_id'], $value);
    }
        // here do the same for "woocommerce_order_items" and "woocommerce_order_itemmeta"
    }
}

function outputRowData($cache)
{

    $cachedPostNames = $cache->getPostNames();
    $firstRow = true;

    foreach($cachedPostNames as $postName) {

        if(empty($postName)) { continue; }

        if($firstRow === false) {
            echo ',';
        }
        $firstRow = false;

        echo '"' . $postName . '":{';
        $postIds = $cache->getPostIdsByNameAndType($postName, 'posts');
        if(!$postIds) {
            $postIds = [];
        }

        // generate posts
        $postValues = [];
        foreach ($postIds as $postId) {
            $postValues[$postId] = $cache->getPostValueByNameTypeAndId($postName, 'posts', $postId);
        }

        $postMetaIds = $cache->getPostIdsByNameAndType($postName, 'postmeta');
        if(!$postMetaIds) {
            $postMetaIds = [];
        }
        $postMetaValues = [];
        foreach ($postMetaIds as $postMetaId) {
            $postMetaValues[$postMetaId] = $cache->getPostValueByNameTypeAndId($postName, 'postmeta', $postMetaId);
        }
        // here do the same for "woocommerce_order_items" and "woocommerce_order_itemmeta"

        echo '"posts":' . json_encode($postValues) . ',';
        echo '"postmeta":' . json_encode($postMetaValues);
        echo '}';
        ob_flush();
        flush();   // flush the output to start the download
    }
}


echo '}}';

Ответ 6

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

Прекращение работы с помощью WebServer

Если вы используете Apache или Nginx/PHP-FPM, у обоих по умолчанию есть тайм-аут для URL-адреса, который попал. Поэтому, хотя вы использовали

ini_set('memory_limit', '-1');
ini_set('max_execution_time', '-1');
set_time_limit(0);

Чтобы сценарий работал долго, но Apache, Nginx, PHP-FPM имеют тайм-аут, который не позволит вашему скрипту работать. Поэтому вам нужно исправить это, чтобы заставить его работать. Вы никогда не упоминали, какой сервер вы использовали. Но NGINX + PHP-FPM приведет к уверенности 502 в конфигурации по умолчанию.

Использование памяти

Даже если вы использовали

ini_set('memory_limit', '-1');

Если ваша память нуждается в повышении, PHP может начать использовать пейджинг, и ваш код может стать медленным.

PHP CLI или PHP Web?

Не уверен, какова частота выполнения здесь, но если она низкая, вы можете считать, что ваш сценарий демпинга данных запускается через PHP-CLI вместо HTTP. Это означало бы, что вы запускаете скрипт PHP напрямую через терминал, чтобы выгрузить JSON в файл, а затем использовать URL-адрес для загрузки файла напрямую

Использование X-Sendfile или X-Accel-Redirect

Если вы используете apache, вы можете отправить заголовок

header('X-Sendfile: /data/generated.json');

В случае Nginx вы можете отправить

header('X-Accel-Redirect: /data/generated.json');

Это вы сделали бы только в том случае, если вы решили запустить скрипт как сеть, а не как CLI. Когда поколение JSON закончилось, вы не хотите, чтобы ваш скрипт читал файл и сервер. Вы просто хотите, чтобы веб-сервер заботился об этом.

Небуферизованный запрос вместо запроса WPDB

https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/wp-db.php#L2480

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

Example #1 Unbuffered query example: mysqli

<?php
$mysqli  = new mysqli("localhost", "my_user", "my_password", "world");
$uresult = $mysqli->query("SELECT Name FROM City", MYSQLI_USE_RESULT);

if ($uresult) {
   while ($row = $uresult->fetch_assoc()) {
       echo $row['Name'] . PHP_EOL;
   }
}
$uresult->close();
?>
Example #2 Unbuffered query example: pdo_mysql

<?php
$pdo = new PDO("mysql:host=localhost;dbname=world", 'my_user', 'my_pass');
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

$uresult = $pdo->query("SELECT Name FROM City");
if ($uresult) {
   while ($row = $uresult->fetch(PDO::FETCH_ASSOC)) {
       echo $row['Name'] . PHP_EOL;
   }
}
?>
Example #3 Unbuffered query example: mysql

<?php
$conn = mysql_connect("localhost", "my_user", "my_pass");
$db   = mysql_select_db("world");

$uresult = mysql_unbuffered_query("SELECT Name FROM City");
if ($uresult) {
   while ($row = mysql_fetch_assoc($uresult)) {
       echo $row['Name'] . PHP_EOL;
   }
}
?>

https://secure.php.net/manual/en/mysqlinfo.concepts.buffering.php

PS: Могло бы быть несколько очков, которые мне не хватает прямо сейчас в моей голове, скоро это обновит