1

a post has the following custom fields:

'my_custom_field' => 1
'my_custom_field' => 2
'my_custom_field' => 3

I now want to query all posts that do NOT contain the value 2 for 'my_custom_field'. I'm trying this:

$args = Array('posts_per_page' => -1,
    'post_type' => 'page',
    'meta_query' => array(
      array(
        'key' => 'my_custom_field',
        'value' => 2,
        'compare' => '!='
      )
    )
  );

However, this is still returning my sample post, as my sample post has a field of 'my_custom_field' with a value other than 2 (1 and 3). I somehow need to change my query to say "Exclude posts that have at least one field of 'my_custom_field' with the value of 2".

Can anyone help me? Thanks!

7
  • To be clear, your query currently asks for posts that have a meta key that isn't 2, which is true of your post, because it has a 1 and a 3
    – Tom J Nowell
    Commented Mar 7, 2014 at 12:01
  • Can you clarify what your custom field is intended to do? Its possible that a custom taxonomy may be more appropriate here
    – Tom J Nowell
    Commented Mar 7, 2014 at 12:02
  • Thanks for your comments. Yeah, the query does what it says, but I need to change the query. I want the query to be "Find posts where none of the values are 2." As for what I'm trying to do. The custom fields are a one-way relation, similar to categories. The field is called "has_category" and a value would be the post_id of the specific category. So basically I want to query all posts which are not in category '2'.
    – etiennedi
    Commented Mar 7, 2014 at 13:13
  • 3
    That doesn't explain why you want this, what is the point of the has_category field? ( I don't mean what should it do, I mean why does it need to do it? This question sounds like an XY problem, are you trying to implement related posts/upselling?
    – Tom J Nowell
    Commented Mar 7, 2014 at 14:11
  • 2
    I am pretty sure I could make this work, and pretty sure it would take a filter to do it, but I am also wondering if this is just the wrong approach altogether. Please explain the why.
    – s_ha_dum
    Commented Mar 7, 2014 at 14:45

2 Answers 2

1

This might require writing custom sql with a subquery:

$results = $wpdb->get_results("
    SELECT * 
    FROM $wpdb->posts 
    WHERE ID NOT IN (
      SELECT post_id FROM $wpdb->postmeta
      WHERE meta_key = 'my_custom_field' and meta_value = 2
    )
");

then just iterate through the results as described here.

3
  • You wrapped meta_value in string quotes, but other than that it works perfectly, thanks! I'll upvote you as soon as I have the required reputation ;)
    – etiennedi
    Commented Mar 7, 2014 at 18:09
  • @Etienne I removed the quotes on meta_value.
    – gmazzap
    Commented Mar 7, 2014 at 18:23
  • @Etienne, I wrote this only because you are new here, if this answer works for you, you should mark it as accepted (there's no required reputation for that), otherwise question is seen as 'unanswered' where it seems isn't so.
    – gmazzap
    Commented Mar 7, 2014 at 18:40
0

As @TomJNowell and @s_ha_dum says you in comments, in your case probably a custom taxonomy is better than custom fields, because is very easy to get a post that has not a taxonomy term.

That said, I understand that if you have a lot of code alreadt wrote, or you are working with third party code, moving to custom taxonomy can be hard / not possible.

Aswer provided by @elleeott works, however is not flexible, because you can't change posts per page, meta value, etc..

I'll provide you a similar solution, but a bit more flexible and you can fine tune it to fit your needs.

Essentially is just a class that extend WP_Query and apply filters to request to perform a what you want

class MyMetaQuery extends WP_Query {

   protected $do_posts_request_filter = TRUE;

   function get_posts() {
     $this->query_vars['suppress_filters'] = FALSE; // force do filters
     add_action( 'posts_request', array( $this, 'filter_request' ), 999 );
     parent::get_posts();
     $this->do_posts_request_filter = FALSE; // run only once
   }

   /* build the sql request */
   function filter_request( $request ) {
      if ( ! $this->do_posts_request_filter ) return $request;
      $q = $this->get('meta_query');
      if ( empty($q) || ! is_array($q) || ! isset($q[0]['key']) ) return $request;
      global $wpdb;
      $k = $q[0]['key'];
      $v = isset( $q[0]['value'] ) ? $q[0]['value'] : '';
      if ( is_array( $v ) ) $v = implode( ',', $v );
      $mcompare = isset( $q[0]['compare'] ) ? $q[0]['compare'] : 'IN';
      if ( ! in_array( $mcompare, array('IN', 'NOT IN', '=', '!=') ) ) {
        $mcompare = 'IN';
      }
      if ( $mcompare === '!=' ) $mcompare = 'NOT IN';
      if ( $mcompare === '=' ) $mcompare = 'IN';
      $ppage = $this->get('posts_per_page') ? : get_option('posts_per_page');
      $limit = $wpdb->prepare(' LIMIT %d', $ppage);
      if ( substr_count( $limit, '-1' )  ) $limit = '';
      if ( $limit && $this->get('nopaging') ) $limit = '';
      $type_compare = 'IN';
      $type = $this->get('post_type') ? : 'post';
      if ( is_array( $type ) ) $type = implode(',', $type );
      if ( $type === 'any' ) {
        $type_compare = 'NOT IN';
        $status = '-1';
      }
      $status_compare = 'IN';
      $status = $this->get('post_status') ? : 'publish';
      if ( is_array($status) ) $type = implode(',', $status);
      if ( $status === 'any' ) {
        $status_compare = 'NOT IN';
        $status = '-1';
      }
      $query = "
        SELECT SQL_CALC_FOUND_ROWS $wpdb->posts.* FROM $wpdb->posts
        INNER JOIN $wpdb->postmeta ON $wpdb->posts.ID = $wpdb->postmeta.post_id
        WHERE post_type $type_compare (%s)
        AND post_status $status_compare (%s)
        AND $wpdb->postmeta.meta_key = %s
        AND ID NOT IN (
          SELECT DISTINCT post_id FROM $wpdb->postmeta
          WHERE meta_key = %s and CAST(meta_value AS CHAR) $mcompare (%s)
        )
      ";
      return $wpdb->prepare( $query, $type, $status, $k, $k, $v ) . $limit;
   }

}

After that, instead of using WP_Query you have to use MyMetaQuery class:

$args = array(
  'posts_per_page' => -1,
  'post_type' => 'page',
  'meta_query' => array(
    array( 'key' => 'my_custom_field', 'value' => 2, 'compare' => '!=' )
  )
);

$query = new MyMetaQuery( $args );

if ( $query->have_posts() ) {
  while( $query->have_posts() ) {
    $query->the_post(); 
    // do what you want here
  }
  wp_reset_postdata();
}

Note thats the only params the custom class accepts are:

  • 'post_type', can be a single types, a comma-separed string or an array of types or 'any'
  • 'post_status' can be a single status, a comma-separed string or an array of statuses or 'any'
  • 'posts_per_page' can be a posistive number or '-1'
  • 'nopaging' can be TRUE or FALSE. Set to TRUE will return all posts overriding 'posts_per_page'
  • 'meta_query' accepts a multidimensional array, just like WP_Query. Note that you can only pass one query array, any additional query will be ignored. The query array accepts as array keys: 'key', 'value' and 'compare'. 'value' can be a single value, a comma-separed string or an array of values. 'compare' can be 'IN', 'NOT IN', '=' and '!='

If you need other arguments (category, tags, taxonomies, date, etc) you must extend the class features or, more probably, use another approach.

2
  • Thanks a lot for your comprehensive answer. This is indeed the most flexible approach. In this case I've already implemented @elleeott 's code and extended it by a couple other WHERE clauses to exclude unpublished posts, etc. Works perfectly for this situation. I will keep your solution in mind, I'm pretty sure I might use it in the future. Thanks a lot. I will also definitely look into custom taxonomy. I'll upvote as soon as I have the required reputation.
    – etiennedi
    Commented Mar 7, 2014 at 18:19
  • @Etienne essentially my answer is just an extension of @elleeott's code too. The problems I see there are: (1) little flexibility (2) the fact that to use template tags (the_title and such) for retrieved posts you have to setup_postdata($post) or apply_filters everytime (3) possible issues with some plugins, in fact what you get are raw objects, where WP_Query returns WP_Posts objects (3) last, but not least, using that code you retrieve all the posts that have no 'my_custom_field' at all, where WP_Query with 'meta_query' doesn't work like so.
    – gmazzap
    Commented Mar 7, 2014 at 18:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.