Getting WP Post and Postmeta in Single Rows

Share on facebook
Facebook
Share on twitter
Twitter
Share on reddit
Reddit
Share on linkedin
LinkedIn

The Custom Post Type is a great feature for WordPress developers; it allows you to take advantage of many of WordPress’ built-in functions for your own data types. On the WordPress dashboard, you can see your own Custom Post Type (CPT) information displayed through familiar screens, but with your own custom taxonomies and fields.

For a recent custom plugin that we made to help check people in at events, I wanted CPTs for “events” and “patrons.” The patrons are the members of the group for which the plugin was made, and the events are their meetings: event staff will use the plugin to check people in as they come through the door.

WordPress uses wp_posts to store post, page, and CPT data. You get all the normal fields (title, content, excerpt, etc.) and you can turn them off or re-label them for different purposes. Chances are, however, you’ll have some of your own data points to store, and for those, you can use WordPress Custom Metaboxes. As an example, this is my “patron” CPT screen in the WordPress dashboard:

WordPress Custom Post Type with Metabox

WordPress stores Metabox data in the wp_postmeta table in a key/value format. You could choose to lump these together with something like PHP’s serialize or you can have a separate wp_postmeta row for each key and value. I chose the latter. I’ve done it both ways, and keeping them separate seems more flexible.

To retrieve this data, however, you have to read for the post plus multiple linked post meta rows. For instance, the SQL for this might be:

SELECT p.ID, p.post_title, pm.meta_key, pm.meta_value
FROM wp_posts p, wp_postmeta pm
WHERE p.post_type = 'patron' AND p.post_status = 'publish'
AND pm.post_id = p.ID;

This would return rows that look like (for example):

ID     POST_TITLE     META_KEY        META_VALUE
1      'Jane Dough'   'first_name'    'Jane'
1      'Jane Dough'   'last_name'     'Dough'
1      'Jane Dough'   'email'         'jane@email.com'
2      'John Dough'   'first_name'    'John'
2      'John Dough'   'last_name'     'Dough'
2      'John Dough'   'email'         'john@email.com'

When you iterate this data – say with “foreach ($patrons as $patron)” – you’re going to need some code logic to sort it all out. Nothing wrong with that, but why not make it simpler?

We can do that with a great feature of SQL: the subquery.

Subqueries

In MySQL/MariaDB (and most every version of SQL I’ve encountered) you have the option of making subqueries. These are queries within the query that get evaluated before the main query. Using our “patrons” example, here’s what I did to get the wp_postmeta data to come back with all data points in a single row:

select p.ID, p.post_title,
  (select pm.meta_value from wp_postmeta pm
   where pm.meta_key = '_csbn_patron_first_name_key' and pm.post_id = p.ID) csbn_patron_first_name_key,
  (select pm.meta_value from wp_postmeta pm
   where pm.meta_key = '_csbn_patron_last_name_key' and pm.post_id = p.ID) csbn_patron_last_name_key,
  (select pm.meta_value from wp_postmeta pm
   where pm.meta_key = '_csbn_patron_email_address_key' and pm.post_id = p.ID) csbn_patron_email_address_key
from wp_posts p
where p.post_type = 'cpt_patron' and p.post_status = 'publish'
order by p.post_title

(The “csbn” is my plugin prefix which I used to tag my meta keys to make them distinct.)

So what’s going on here? Lines 2-7 represent three subqueries; I’ve highlighted the first and third to make them stand out. Before we talk about them, however, let’s look at the outer query by itself:

select p.ID, p.post_title,
...
from wp_posts p
where p.post_type = 'cpt_patron' and p.post_status = 'publish'
order by p.post_title

The outer query, with no subqueries, is getting data from my parent row (wp_posts) for its display line, and down in there WHERE it’s specifying my CPT’s name (“cpt_patron” types that have been published). This gets one row each from wp_posts and forms the basis of my result set:

ID     POST_TITLE
1      'Jane Dough'
2      'John Dough'

Where the “…” appears in the above stripped-down query, I inserted my subqueries. A subquery should always be in parentheses, and after the closing paren, we give it the name that will be used in the result set:

...
  (select pm.meta_value from wp_postmeta pm
   where pm.meta_key = '_csbn_patron_first_name_key' and pm.post_id = p.ID) csbn_patron_first_name_key,
...

In each case (all three subqueries are very similar), I have a complete query that retrieves the value (pm.meta_value) of the data point. It draws from wp_postmeta where the key matches one of my predefined custom keys and the wp_postmeta post_id matches the ID of the overall row.

Notice the use of aliases: “p” (wp_post) is part of the parent query, and “pm” (wp_postmeta) only appears in the subqueries. When I refer to “pm” in my subquery, it’s local to the subquery; when I reference “p.ID,” it’s from the parent. Ergo, as we process any given row, we get a post ID, a post title (both from wp_posts) and the three subqueries get their respective meta information to go with it.

In my case, I ended up with results like this:

(A couple of real names and email addresses blacked out!)

With this result set, my PHP code only has to read and dump to the screen… none of the first-row (per group) processing that I would have needed to handle wp_posts and wp_postmeta data as seperate rows.

The Plugin Code

For the sake of (semi!) completeness, here’s the query used in my WordPress plugin’s PHP code:

$patrons = $wpdb->get_results(
	$wpdb->prepare(
		"select distinct p.ID, p.post_title, " .
			"(select pm.meta_value from " . $wpdb->prefix . "postmeta pm " .
			"where pm.meta_key = '_csbn_patron_first_name_key' " .
			"and pm.post_id = p.ID) csbn_patron_first_name_key, " .
			"(select pm.meta_value from " . $wpdb->prefix . "postmeta pm " .
			"where pm.meta_key = '_csbn_patron_last_name_key' " .
			"and pm.post_id = p.ID) csbn_patron_last_name_key, " .
			"(select pm.meta_value from " . $wpdb->prefix . "postmeta pm " .
			"where pm.meta_key = '_csbn_patron_email_address_key' " .
			"and pm.post_id = p.ID) csbn_patron_email_address_key " .
		"from " . $wpdb->prefix . "posts p, " . $wpdb->prefix . "postmeta pm " .
		"where p.post_type = 'cpt_patron' and p.post_status = 'publish'" .
		"and pm.post_id = p.ID and pm.meta_key like '_csbn%'" .
		"order by p.post_title", null
	)
);

foreach ($patrons as $patron) {
    ... do exciting stuff like ...
    $screen .= '<p><button class="csbn_button csbn_button4"><a href="#header">Back to Top</a></button> <button class="csbn_button csbn_button4"><a href="#actions-sidebar">Add New</a></button></p>';
    ...
}

Nothing rocket-science here, but hopefully it’s a different way of looking at things. There are a ton of amazing techniques that can be employed on the SQL side, and once done they have a DRYing effect as well making it easier to implement standards.

More to explore

Using Redis with WordPress on Ubuntu

Redis is “an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries and streams.” Translation? It makes websites run faster.

Nasa on Unsplash

Building an API Endpoint with Amp (PHP)

API endpoints in web apps are pretty typical these days, but there may be reasons to provide data outside of the context of an application. In this post, we’ll explore how to make a stand-alone API endpoint using PHP and Amp.

Photo by Hal Gatewood on Unsplash

Making a Simple Cell

This tutorial covers the basics of creating a view cell based upon an “articles” table, similar to the one from the blog tutorial on CakePHP.org.  It is written using CakePHP 3.

Leave a Comment

Your email address will not be published. Required fields are marked *