Lazy load ancestors

Description

It should be pretty easy to lazy load ancestors using a magic get function on 'ancestors', rather than querying for them individually via _get_post_ancestors().

johnjamesjacoby presented a use case where get_post_field() was triggering ancestors queries, with no way to avoid that if you know you need that information. Not sure how to solve that specific problem without thinking bigger.

The use case is get_post_field( 'post_type', $post_id ) or some other query that doesn't require populating the ancestors. It could even be a straight get_post() call -- really, the use case doesn't have anything to do with the suggestion here other than it gave me the idea.

Being able to lazy load ancestors can save a number of queries. They're fast, but with a complex hierarchy and a number of get_post() calls, they can add up. Then only the queries are triggered when needed.

Thinking about it some more, this will require a real WP_Post object -- #12267. And is probably only one of a few improvements that can come out of that.

In the mean time, using get_posts in _get_post_ancestors() rather than hitting the DB directly with an uncached query cuts down on cache misses and DB hits in the double digits in the bbPress plugin, particularly in situations where there are Forum, Topic, or Reply widgets in sidebars, and/or subforums are involved.

How about instead we deprecate the post object's ancestors property with a magic __get method, and then use a working get_post_ancestors() function to get the ancestors in core WP?

That way, we maintain backwards-compatibility for those who are referencing the ancestors post property in plugins, but we are explicit about actually querying the post ancestors, in core (and as recommended practice).

Resource-intensive queries like that should be as explicit as possible, for code readability and debugging purposes. Hiding the query in a magic __get method is slightly slower, and it makes it harder to identify the conditions in which such queries will occur.

In other words, as a band-aid to handle deprecated properties, it's fine, but let's not make it standard practice, particularly for retrieving external resources (database records) as opposed to e.g. internal object properties, which I think was the intent of __get.

Using get_post here also reduces cache misses with no adverse effects for things like single blog post views, where the next and previous posts are automatically added to wp_head, and then loaded again in the content area. Without this method, ancestor queries aren't cached. Same goes for pages in nav menus, etc...

As a general update, 16574.3.diff is still good. I've been running several .org installs with it since posted with only positive results. It doesn't add any fancy magic functions, but it does at least give ancestor queries the opportunity to hit the WP cache instead of always hitting the DB.

Any install with a page widget and a page menu would see a noticeable decrease in DB hits with this patch alone. The bbPress plugin sees a dramatic decrease (of sometimes 10+ queries) because it relies heavily on the hierarchy to manage the post relationships between forums/topics/replies.

If there are alternatives (magic functions, etc...) I'd love to collaborate on them to test the benefits.

Doesn't seem like it would need a new group all together. The key in the posts cache group could simply be ancestors-$id.

That said, breaking compatibility with $post->ancestors is definitely not something we can get away with right now. Perhaps after the success of WP_Theme, someone (me?) might take a crack at a proper WP_Post in a future release, which could alleviate back compat issues.

I did very quick grep in the plugins directory for 'post->ancestors' and stopped it after seeing at least 50 plugins (100s of instances) using it.

As for the isset() in __get(), I'm not sure if it's a good idea to silently fail if the key doesn't exist.

Plugins like BuddyPress and bbPress don't always load a real $post and rely on silent failure. The $post global is sometimes set with dummy (or semi relevant) information to satisfy the bare minimum of WordPress's API that expects it to exist. Silent failure is the way the WP_User magic methods work; given WordPress's pluggable nature, it's fitting that it prevent notices where it can.