Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements: rendering the Subscription Relationship column and checking if order is a renewal, switch etc. #732

Merged

Conversation

mattallan
Copy link
Contributor

@mattallan mattallan commented Nov 20, 2024

Part of #731

Description

When a merchant visits the WooCommerce > Orders list table, WooCommerce Subscriptions adds a custom "Subscription Relationship" column that displays an icon based on what type of order it is:

  1. Parent (initial subscription) Order
  2. Renewal Order
  3. Resubscribe Order

image

As reported in #731, our render_contains_subscription_column_content() function currently performs unnecessary queries which impacts overall site performance and performance when viewing/managing orders via the WooCommerce > Orders table. After looking at this function, I noticed a couple of issues that we should address:

  1. wcs_order_contains_subscription() unnecessarily loads subscription objects into memory.
  2. We already have the Order object when rendering the row/column so we should use it instead of passing the order ID around.
  3. There's no efficient helper function to check if an order is a parent order.
  4. All of our wcs_order_contains_{order_type}() functions are doing a lot when all we really need to do is check for meta on an order.

This PR targets all of these issues:

  • dd1299a - Introduces a new wcs_is_parent_order( $order ) function which runs a much more performant query to check if an order is a parent order.
  • 44321ec - Refactors all of our wcs_order_contains_renewal(), wcs_order_contains_resubscribe() and wcs_order_contains_switch() functions to use our WCS_Related_Order_Store class to check if the order belongs to a renewal, resubscribe and switch, without having to load subscription objects into memory.
  • 976c961 - Refactors the render_contains_subscription_column_content() to call the updated/more performant wcs_order_contains_{order_type}( $order ) functions directly instead of going through wcs_order_contains_subscription( $order_id, {order_type} )
  • fc91135 - Updates wcs_order_contains_subscription() to use more performant method for checking parent order.

How to test this PR

Important

The changes in this PR impact potentially a lot of critical subscription flows. We should make sure these functions are heavily unit tested and test our critical flows to assess impact.

Product impact

  • Added changelog entry (or does not apply)
  • Will this PR affect WooCommerce Subscriptions? yes/no/tbc, add issue ref
  • Will this PR affect WooCommerce Payments? yes/no/tbc, add issue ref
  • Added deprecated functions, hooks or classes to the spreadsheet

@mattallan
Copy link
Contributor Author

Breakdown of these changes

Checking if an order is renewal/resubscribe

Old
  1. Get order object: wc_get_order( $order_id )
  2. Get subscription id from meta: $order->get_meta( '_subscription_renewal', false );
  3. Load in subscription object: wcs_get_subscription( $subscription_id );
  4. Check if $subscription is not empty.
This branch
  1. Get subscription id from meta: $order->get_meta( '_subscription_renewal', false );
  2. Check if $subscription_id is exists.

Important

As seen, these changes will no longer load a subscription object into memory which means we're no longer checking if the subscription ID stored in _subscription_renewal is actually a valid subscription object. This means with the changes in this PR, wcs_order_contains_renewal() and other similar "contains_x()" functions, will now return true even if the subscription no longer exists.

To determine if this is a bug or not, we should ask ourselves whether an order should revert to a standalone/regular order if the subscription has been deleted 🤔

I can't think of good reason why we should revert an order to a regular order if the linked subscription has been removed and I believe it would still be good to know that this order was reference to a subscription renewal or resubscribe etc.

If we consider this to be a bug, one way we could address this is to hook onto woocommerce_subscription_deleted and delete any relation meta for this subscription. This would prevent us having to load a subscription into memory just to check if the relation is still valid.

Checking if an order is a parent

Old
  1. Get order object: wc_get_order( $order_id )
  2. Query for subscriptions: wcs_get_orders( [ 'limit' => -1, 'parent' => 1234, 'type' => 'shop_subscription' ] )
  3. Load in subscription object: wcs_get_subscription( $subscription_id );
  4. Count subscriptions found.
This branch
  1. Query for subscriptions: wcs_get_orders( [ 'limit' => 1, 'parent' => 1234, 'type' => 'shop_subscription' ] )

Copy link
Contributor

@james-allan james-allan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes look good to me. On a change by change comparison the changes in this PR only improve the performance of the wcs_order_contains_x functions.

Diving into the root difference, it's essentially bypassing mid level functions to avoid a feature of those mid level functions which is to instantiate the subscription objects returned.

Your concerns around checking if those subscriptions still exist is fair, however, I don't think we need to be concerned with that too much.

Deleting subscriptions isn't something we routinely do or even expect. Even if it was a thing, the relationship stored in order meta would be incorrect and the UI's displaying of that relationship on the front end would be a reflection of incorrect data. That is, I agree with your earlier comment that the "fix" would be to make sure that meta was deleted on subscription deletion.

I left a couple of comments and questions. Let me know what you think of them.

*
* @return array The subscription IDs.
*/
function wcs_get_subscription_ids_for_order( $order, $order_types = [] ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the intention behind the empty [] $order_types param?

Passing an empty array returns no results. Is it supposed to be interpreted as all order types?

It's probably worth detailing this in the function block param comment too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @james-allan. I think it makes sense for us to return 'any' subscription IDs linked to the given order so I've just implemented the any param here cd7ac49.

I also added these default details to the docblock (see 4051a4c)

$subscription_ids += WCS_Related_Order_Store::instance()->get_related_subscription_ids( $order, $order_type );
}

// An order cannot be both a renewal, switch or resubscribe as well as a parent order, so only fetch subscription IDs if we didn't find any in the related order store.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can make this assumption.

If I resubscribe to a cancelled subscription, that order is a parent to the new subscription and a resubscribe to the old subscription.

wcs_get_subscription_ids_for_order( $resubscribe_order, [ 'resubscribe', 'parent' ] )

This should return 2 results, the cancelled subscription and the new one. In it's current form, it would just return the resubscribe.

I understand the intention is to avoid unnecessary expensive queries. I think callers if they were being super performance conscious, they would be be better off doing something like this:

$related_subscriptions = wcs_get_subscription_ids_for_order( $order, [ 'resubscribe', 'renewal', 'switch' ] );

// If the subscription contains a subscription
if ( ! empty( $related_subscriptions ) || wcs_is_parent_order( $order ) {
   ... 

ie separate the related order relations from the parent check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging this! I've fixed this in 5f237ab


return apply_filters( 'woocommerce_subscriptions_is_resubscribe_order', $is_resubscribe_order, $order );
$related_subscription_ids = wcs_get_subscription_ids_for_order( $order, 'resubscribe' );
return apply_filters( 'woocommerce_subscriptions_is_resubscribe_order', ! empty( $related_subscription_ids ), $order );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This applies to all the contains_x() functions but these filters use to always receive an order object. Given these functions can accept an ID, they will now receive whatever was passed in -- ID or object.

Unfortunately I think this means we will need to return to the

if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
    $order = wc_get_order( $order );
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @james-allan!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just pushed up d492796 to address this! Going to run through some more tests on these latest changes

Copy link
Contributor

@james-allan james-allan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this @mattallan. Thanks for working through all my feedback 😅

I think this PR is in a good place now. I particularly like the fact that we've got a good foundation for these subscription-order relationship functions.

We have a full suite of is/contains functions:

  • wcs_is_parent_order()
  • wcs_order_contains_switch()
  • wcs_order_contains_resubscribe()
  • wcs_order_contains_renewal()

When you want to check multiple types, you can use:

  • wcs_order_contains_subscription()

That approach is more user friendly but also performant as it runs fewer queries by the nature of its if - elseif structure and parent checking is last.

They all fall through to a base function wcs_get_subscription_ids_for_order() which interfaces with the caches and data layer in 1 place. ❤️

@mattallan mattallan merged commit 98dfba7 into trunk Feb 26, 2025
9 checks passed
@mattallan mattallan deleted the for/731-improve-performance-rendering-order-type-column branch February 26, 2025 05:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants