meta_query с мета-значениями как сериализованные массивы

37

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

<p> 
    <label for="latitude">Latitude:</label><br /> 
    <input type="text" id="latitude" name="coordinates[latitude]" class="full-width" value="" /> 
</p> 
<p>     
    <label for="longitude">Longitude:</label><br /> 
    <input type="text" id="longitude" name="coordinates[longitude]" class="full-width" value="" /> 
</p>

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

update_post_meta($post_id, '_coordinates', $_POST['coordinates']);

Я сделал это, потому что у меня есть три метабокса, и мне нравится просто иметь 3 значения postmeta для каждого поста; однако теперь я понял потенциальную проблему с этим. Я могу захотеть использовать WP_Query только для извлечения определенных сообщений на основе этих мета значений. Например, я могу захотеть получить все сообщения, значения широты которых превышают 50. Если бы у меня были эти данные в базе данных по отдельности, возможно, с использованием ключа latitude, я бы сделал что-то вроде:

$args = array(
    'post_type' => 'my-post-type',
    'meta_query' => array(
        array(
            'key' => 'latitude',
            'value' => '50',
            'compare' => '>'
        )
    )
 );
$query = new WP_Query( $args );

Поскольку у меня есть широта как часть _coordinatesпостмета, это не сработает.

Итак, мой вопрос, есть ли способ использовать meta_queryдля запроса сериализованного массива, как у меня в этом сценарии?

tollmanz
источник

Ответы:

37

Нет, это невозможно, и даже может быть опасно.

Я настоятельно рекомендую вам отменить сериализацию ваших данных и изменить процедуру сохранения. Нечто подобное должно преобразовать ваши данные в новый формат:

$args = array(
    'post_type' => 'my-post-type',
    'meta_key' => '_coordinates',
    'posts_per_page' => -1
 );
$query = new WP_Query( $args );
if($query->have_posts()){
    while($query->have_posts()){
        $query->the_post();
        $c = get_post_meta($post->id,'_coordinates',true);
        add_post_meta($post->ID,'_longitude',$c['longitude']);
        add_post_meta($post->ID,'_latitude',$c['latitude']);
        delete_post_meta($post->ID,'_coordinates',$c);
    }
}

Тогда вы сможете запросить, как вы хотите с отдельными ключами

Если вам нужно хранить несколько долгот и несколько широт, вы можете хранить несколько мета-записей с одним и тем же именем. Просто используйте третий параметр get_post_meta, и он вернет их все в виде массива

Почему вы не можете запросить внутри сериализованных данных?

MySQL видит это как просто строку и не может разбить ее на структурированные данные. Разбиение его на структурированные данные - это именно то, что делает код выше

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

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

Замечание о хранении записей / объектов / объектов как сериализованных объектов в мета-версии

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

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

Безопасность и сериализованные объекты

Хранение сериализованных объектов PHP через serializeфункцию может быть опасным , что прискорбно, поскольку передача объекта в WordPress будет означать, что он сериализуется. Это связано с тем, что при десериализации объекта создается объект и выполняются все его методы и конструкторы пробуждения. Это может показаться не таким уж большим делом, пока пользователю не удастся украсть тщательно созданный ввод, что приведет к удаленному выполнению кода, когда данные считываются из базы данных и десериализуются WordPress.

Этого можно избежать, используя вместо этого JSON, что также облегчает запросы, но гораздо проще / быстрее просто правильно хранить данные и избегать структурированных сериализованных данных для начала.

Том Дж Новелл
источник
5
Для людей, проходящих мимо, не прекращайте читать: более полезные (и последние) ответы находятся ниже
Erenor Paz
Что если у меня есть массив идентификаторов для сохранения, и каждый из них не представляет отдельный ключ, который я мог бы сохранить, например, «широта» и т. Д., Это всего лишь один ключ для всех (например, при сохранении отношений и т. Д.). Что делать то? решение Рабни?
trainoasis
1
Вы можете хранить ключ более одного раза, пары ключ-значение не являются уникальными. Что касается отношений, это то, для чего нужны таксономии, если вы используете мета для отображения нескольких вещей на что-то, вместо этого поместите их в термин таксономии
Том Дж. Новелл
24

Я тоже столкнулся с этой ситуацией. Вот что я сделал:

$args = array(
    'post_type' => 'my-post-type',
    'meta_query' => array(
        array(
            'key' => 'latitude',
            'value' => sprintf(':"%s";', $value),
            'compare' => 'LIKE'
        )
    )
);

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

rabni
источник
1
Мне очень понравилось это решение. К сожалению, это не применимо, когда $valueтакже является идентификатором. В этом случае я предлагаю создать функции для добавления символа в каждый элемент массива перед сохранением данных и другую функцию для удаления символа перед использованием данных. При этом сериализованный i:2индекс не следует путать с i:D2«реальными» данными. Параметр мета-запроса должен стать таким, 'value' => sprintf(':"D%s";', $value),и вы сохраните правильную функциональность этого замечательного ответа!
Эренор Пас
Это решение работает для меня
Вишал
Это также отлично сработало для меня. У меня была мини-паника, когда я увидел принятое решение
Шейн Джонс
@Erenor Paz, я только что опубликовал решение, которое хорошо работает как с ID, так и со строками: wordpress.stackexchange.com/a/299325/25264
Пабло С.Г. Пачеко,
Использование LIKE- это отличный и быстрый способ выключить ваш сервер (не говоря уже о ложных срабатываниях), вам лучше иметь очень хорошее кэширование.
Марк Каплун
10

Вы действительно потеряете возможность запрашивать ваши данные любым эффективным способом при сериализации записей в базе данных WP.

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

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

На самом деле присваивается значение true, как в;

$meta = get_post_meta( $post->ID, 'key', true );

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

Вы можете сосредоточиться на других оптимизациях баз данных / сайтов, таких как кэширование, минимизация CSS и JS и использование таких сервисов, как CDN, если вам нужно. Назовем лишь некоторые .... Кодекс WordPress является хорошей отправной точкой, чтобы узнать больше по этой теме: ЗДЕСЬ

Адам
источник
3

Я только что имел дело с сериализованными полями и мог запросить их. Не используя meta_query, но используя SQL-запрос.

global $wpdb; 

$search = serialize('latitude').serialize(50);

$query = $wpdb->prepare("SELECT `post_id`
FROM `wp_postmeta`
WHERE `post_id` IN (SELECT `ID` FROM `wp_posts` WHERE `post_type` = 'my-post-type')
AND `meta_key` = '_coordinates'
AND `meta_value` LIKE '%s'",'%'.$search.'%');

$ids = $wpdb->get_col($query);

$args = array(
    'post__in' => $ids
    'post_type' => 'team' //add the type because the default will be 'post'
);

$posts = get_posts($args);

Сначала запрос ищет запись с совпадающим типом post_type, поэтому количество записей wp_postmeta будет меньше для фильтрации. Затем я добавил оператор where для дальнейшего уменьшения строк путем фильтрацииmeta_key

Идентификаторы заканчиваются в массиве по мере необходимости для get_posts.

PS. MySQL v5.6 или выше необходим для хорошей производительности подзапроса

Tomas
источник
1

Этот пример действительно помог мне. Это специально для плагина S2Members (который сериализует метаданные пользователя). Но это позволяет вам запрашивать часть сериализованного массива в meta_key.

Это работает с использованием функции MySQL REGEXP.

Вот источник

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

  <?php
global $wpdb;
$users = $wpdb->get_results ("SELECT `user_id` as `ID` FROM `" . $wpdb->usermeta . 
          "` WHERE `meta_key` = '" . $wpdb->prefix . "s2member_custom_fields' AND 
           `meta_value` REGEXP '.*\"country_code\";s:[0-9]+:\"US\".*'");
if (is_array ($users) && count ($users) > 0)
    {
        foreach ($users as $user)
            {
                $user = /* Get full User object now. */ new WP_User ($user->ID);
                print_r($user); /* Get a full list of properties when/if debugging. */
            }
    }
?>
До н.э. Смит
источник
1

Я думаю, что есть 2 решения, которые могут попытаться решить проблему хранения результатов в виде как String, так и Integer. Однако, как отмечали другие, важно сказать, что невозможно гарантировать целостность результатов, хранящихся в виде целых чисел, поскольку, поскольку эти значения хранятся в виде сериализованных массивов, индекс и значения сохраняются точно по одному и тому же шаблону. Пример:

array(37,87);

хранится в виде сериализованного массива, как это

a:2:{i:0;i:37;i:1;i:87;}

Обратите внимание i:0как первая позиция массива и i:37как первое значение. Картина такая же. Но давайте перейдем к решениям


1) РЕГЭКСП РЕШЕНИЕ

Это решение работает для меня независимо от мета-значения, сохраняемого в виде строки или числа / идентификатора. Однако он использует REGEXP, что не так быстро, как использованиеLIKE

$args = array(
    'post_type' => 'my-post-type',
    'meta_query' => array(
        array(
            'key' => 'latitude',
            'value' => '\;i\:' . $value . '\;|\"' . $value . '\";',
            'compare' => 'REGEXP'
        )
    )
);

2) как решение

Я не уверен в разнице в производительности, но это решение, которое использует LIKEи работает как для числа, так и для строки

 $args = array(
        'post_type' => 'my-post-type',
        'meta_query' => array(
            'relation' => 'OR',
            array(
                'key' => 'latitude',
                'value' => sprintf(':"%s";', $value),
                'compare' => 'LIKE'
            ),
            array(
                'key' => 'latitude',
                'value' => sprintf(';i:%d;', $value),
                'compare' => 'LIKE'
            )
        )
    );
Пабло С.Г. Пачеко
источник
REGEXPЭто хорошо в определенных ситуациях, но если вы можете использовать LIKE, я думаю, что это предпочтительный метод. Старая ссылка, но все еще довольно полезная, по моему мнению: thingsilearn.wordpress.com/2008/02/28/… :-)
Эренор Пас
@ErenorPaz Вы правы. LIKEбыстрее. Но это решение, которое работает как для строк, так и для чисел
Пабло С.Г. Пачеко
Да .. так, ответ (как всегда): в зависимости от ситуации, если вы можете использовать «LIKE»; это предпочтительнее, в противном случае REGEXP будет делать то же
самое
@ErenorPaz, я отредактировал свой ответ, добавив новое решение, которое использует, LIKEно работает как для чисел, так и для строк. Я не уверен насчет производительности, потому что нужно сравнивать результаты с помощьюOR
Pablo SG Pacheco
В точку !!! что мне нужно, чтобы получить такой же результат, как это .... Спасибо, человек !!!
Кулдип Макадия
0

После прочтения нескольких советов по выполнению WP_Queryфильтрации по сериализованным массивам, вот как я, наконец, сделал это: создав массив значений, разделенных запятыми, используя implode в сочетании с $wpdbпользовательским запросом SQL, который используется FIND_IN_SETдля поиска запрошенного списка с разделителями-запятыми.

(это похоже на ответ Томаса, но его производительность немного ниже для SQL-запроса)

1. В functions.php:

В вашем файле functions.php (или где бы вы ни настраивали мета-поле) в yourname_save_post()функции используйте

update_post_meta($post->ID, 'checkboxArray', implode(",", $checkboxArray)); //adding the implode

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

Вы также захотите изменить свою выходную переменную в yourname_post_meta()функции построения мета-окна администратора на

$checkboxArray = explode(",", get_post_custom($post->ID)["checkboxArray"][0]); //adding the explode

2. В шаблоне PHP-файла:

Тест: если вы запустите a, get_post_meta( $id );вы должны увидеть checkboxArrayмассив, содержащий значения, разделенные запятыми, а не сериализованный массив.

Теперь мы создаем наш собственный запрос SQL с помощью $wpdb.

global $wpdb;

$search = $post->ID;

$query = "SELECT * FROM wp_posts
          WHERE FIND_IN_SET( $search, (
              SELECT wp_postmeta.meta_value FROM wp_postmeta
              WHERE wp_postmeta.meta_key = 'blogLocations'
              AND wp_postmeta.post_id = wp_posts.ID )
          )
          AND ( wp_posts.post_type = 'post' )
          AND ( wp_posts.post_status = 'publish' );";

$posts = $wpdb->get_results($query);

foreach ($posts as $post) {
    //your post content here
}

Обратите внимание FIND_IN_SET, вот где происходит волшебство.

Теперь ... так как я использую SELECT *это, возвращает все данные поста, и в нем foreachвы можете получить то, что вы хотите от этого (сделайте, print_r($posts);если вы не знаете, что включено. Это не устанавливает "цикл" для вы (я предпочитаю это так), но его можно легко изменить, чтобы настроить цикл, если вы предпочитаете (взгляните на setup_postdata($post);кодекс, вам, вероятно, придется изменить, SELECT *чтобы выбрать только идентификаторы постов и $wpdb->get_resultsправильный $wpdbтип - - см. также кодекс для $wpdbполучения информации по этому вопросу).

Понятно, что это заняло немного усилий, но так wp_queryкак не поддерживает 'compare' => 'IN'сериализацию или разделение запятыми, эта прокладка - ваш лучший вариант!

Надеюсь, это кому-нибудь поможет.

Гиффорд Н.
источник
0

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

$wp_user_search = new WP_User_Query(array(
    'meta_query' => array(
        array(
            'key'     => 'wp_capabilities',
            'value'   => 'subscriber',
            'compare' => 'not like'
            )
        )
    )
);

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

[query_where] => WHERE 1=1 AND (
  ( wp_usermeta.meta_key = 'wp_capabilities' 
  AND CAST(wp_usermeta.meta_value AS CHAR) NOT LIKE '%subscriber%' )
benklocek
источник
0

Если мои метаданные имеют тип массива, я использую этот метод для запроса по мета:

$args = array(
    'post_type' => 'fotobank',
    'posts_per_page' => -1,
    'meta_query' => array(
            array(
                   'key' => 'collections',
                   'value' => ':"'.$post->ID.'";',
                   'compare' => 'LIKE'
            )
     )
);
$fotos = new WP_Query($args);
Ден Медиа
источник
Это может привести к нежелательным результатам, когда идентификатор записи имеет то же значение, что и идентификатор сериализованной строки
Erenor Paz
0

Мне стало любопытно, что ответы выше, где meta_queryнацелены на ключ, latitudeа не _coordinates. Пришлось пойти и проверить, действительно ли было возможно в мета-запросах нацеливаться на определенный ключ внутри сериализованного массива. :)

Это явно не тот случай.

Итак, обратите внимание, что правильный ключ для цели _coordinatesвместо latitude.

$args = array(
     'post_type' => 'my-post-type',
     'meta_query' => array(
         array(
             'key' => '_coordinates',
             'value' => sprintf(':"%s";', $value),
             'compare' => 'LIKE'
         )
     )
 );

ЗАМЕТКИ:

  1. Такой подход делает возможным нацеливание только на точные совпадения. Поэтому такие вещи, как все широты больше 50 , невозможны.

  2. Чтобы включить совпадения подстроки, можно использовать 'value' => sprintf(':"%%%s%%";', $value),. (не проверял)

jgangso
источник
-1

У меня такой же вопрос. Может быть, вам нужен параметр 'type'? Проверьте этот связанный вопрос: запрос пользовательского поля - мета-значение - массив

Возможно, попробуйте:

    $ args = array (
    'post_type' => 'my-post-type',
    'meta_query' => array (
        массив (
            'key' => 'latitude',
            'value' => '50',
            'сравнить' => '>',
            'type' => 'numeric'
        )
    )
    );
user4356
источник
Спасибо за предложение, но это не совсем то, что мне нужно. Проблема в том, что значение, которое я пытаюсь найти, является частью массива, который сериализуется в базе данных.
Tollmanz
Да, ты прав. Я попробовал это сегодня утром, и у меня это тоже не сработало. У меня такая же проблема. Сохранение значения мета-ключа в виде массива. Я начинаю думать, что это невозможно, и вместо этого мне, возможно, придется хранить их как отдельные мета-поля с одинаковыми именами ... и просто правильно управлять их удалением / обновлением.
user4356
@ user4356 ... это именно то, что я собираюсь сделать. Я надеялся сократить количество строк, которые я вставлю для каждого поста, но, думаю, это невозможно.
tollmanz
-1

Я столкнулся с чем-то похожим, когда использовал плагин Magic Fields. Это может сделать трюк

$values_serialized = serialize(array('50'));
$args = array(
    'post_type' => 'my-post-type',
    'meta_query' => array(
        array(
            'key' => 'latitude',
            'value' => $values_serialized,
            'compare' => '>'
        )
    )
);
Сет Стивенсон
источник
1
Спасибо за предложение! Я думаю, что это настолько близко, насколько это возможно, но на самом деле это не сработает, потому что сравнивать сериализованный массив с другим сериализованным массивом не имеет смысла, если только я не искал точное совпадение.
Tollmanz
5
Тогда это не должно быть помечено как правильный ответ, и это безответственно с вашей стороны. Таким образом, правильным ответом будет «Нет, это невозможно»
Том Дж. Новелл
1
Согласитесь, WP также выполняет сериализацию для вас, serialize()в этом случае не требуется ...
Адам
2
На самом деле ответ @ seth-stevenson великолепен, когда он делает именно то, что сказал, используя плагин «Волшебные поля». Поскольку этот плагин сериализует определенный тип данных по умолчанию, это лучший способ выполнить ТОЧНОЕ совпадение.
Zmonteca
@ TomJNowell Готово! Просто у меня ушло 5 месяцев;)
tollmanz