Let's assume that we have 4 posts and 4 categories.
+----+--------+
| ID | Post |
+----+--------+
| 1 | Test 1 |
| 2 | Test 2 |
| 3 | Test 3 |
| 4 | Test 4 |
+----+--------+
+----+------------+
| ID | Category |
+----+------------+
| 1 | Category 1 |
| 2 | Category 2 |
| 3 | Category 3 |
| 4 | Category 4 |
+----+------------+
+--------+------------------------+
| Post | Category |
+--------+------------------------+
| Test 1 | Category 1, Category 2 |
| Test 2 | Category 2 |
| Test 3 | Category 3 |
| Test 4 | Category 4 |
+--------+------------------------+
If I understood your question correctly, you want to get Test 1
post using category__not_in
parameter. Arguments to your query will look's like:
$args = array(
'category__not_in' => array(2, 3, 4)
);
The problem with category__not_in
is that it produce NOT IN SELECT
SQL query.
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
WHERE 1=1
AND (wp_posts.ID NOT IN
( SELECT object_id
FROM wp_term_relationships
WHERE term_taxonomy_id IN (2, 3, 4) ))
AND wp_posts.post_type = 'post'
AND (wp_posts.post_status = 'publish'
OR wp_posts.post_status = 'private')
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_date DESC LIMIT 0, 10
NOT IN SELECT
will exclude all posts including Test 1
. If only this SQL would use JOIN
instead of NOT IN SELECT
this will work.
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID
FROM wp_posts
LEFT JOIN wp_term_relationships ON (wp_posts.ID = wp_term_relationships.object_id)
WHERE 1=1
AND (wp_term_relationships.term_taxonomy_id NOT IN (2, 3, 4))
AND wp_posts.post_type = 'post'
AND (wp_posts.post_status = 'publish'
OR wp_posts.post_status = 'private')
GROUP BY wp_posts.ID
ORDER BY wp_posts.post_date DESC LIMIT 0, 10
Above SQL will return only Test 1
post. We can make a little trick to produce such a query using WP_Query class. Instead of using category__not_in
parameter replace it with category__in
parameter and add post_where
filter which will modify SQL directly to our purpose.
function wp_286618_get_posts() {
$query = new WP_Query( array(
'post_type' => 'post',
'category__in' => array( 2, 3, 4 ) // Use `category__in` to force JOIN SQL query.
) );
return $query->get_posts();
}
function wp_286618_replace_in_operator($where, $object) {
$search = 'term_taxonomy_id IN'; // Search IN operator created by `category__in` parameter.
$replace = 'term_taxonomy_id NOT IN'; // Replace IN operator to NOT IN
$where = str_replace($search, $replace, $where);
return $where;
}
add_filter( 'posts_where', 'wp_286618_replace_in_operator', 10, 2 ); // Add filter to replace IN operator
$posts = wp_286618_get_posts(); // Will return only Test 1 post
remove_filter( 'posts_where', 'wp_286618_replace_in_operator', 10, 2 ); // Remove filter to not affect other queries
The advantage of this solution over others is that I don't need to know other categories ID, and it will keep your post loop clean.