Skip to:
Content

BuddyPress.org

Changeset 13280


Ignore:
Timestamp:
05/18/2022 05:32:26 AM (3 years ago)
Author:
imath
Message:

Improve group members count query performance

[13103] introduced a change in 10.0.0 that can be very time consuming when a group has a lot of members.

To keep the main improvements of the referenced commit (only refreshing group members count when a user joins or leaves a group) but optimize queries performance, we are introducing a new way to count group members in the BP_Group_Member_Query.

We are also introducing a way to defer group members count when adding a batch of members to a group. Using bp_groups_defer_group_members_count() avoids to refresh the count each time a member of this batch is added. For more information about how to use this function, you can have a look at how BuddyPress is using it into src/bp-groups/bp-groups-admin.php.

Props dd32, espellcaste

See #8688 (trunk)

Location:
trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/bp-groups/bp-groups-admin.php

    r13193 r13280  
    355355
    356356        if ( ! empty( $user_names ) ) {
     357            $new_members = count( $user_names );
     358
     359            if ( 1 < $new_members ) {
     360                bp_groups_defer_group_members_count( true );
     361            }
    357362
    358363            foreach( array_values( $user_names ) as $user_name ) {
     
    372377                    }
    373378                }
     379            }
     380
     381            if ( 1 < $new_members ) {
     382                bp_groups_defer_group_members_count( false, $group_id );
    374383            }
    375384        }
  • trunk/src/bp-groups/bp-groups-functions.php

    r13195 r13280  
    36793679}
    36803680add_action( 'bp_init', 'bp_init_group_extensions', 11 );
     3681
     3682/**
     3683 * Updates a group members count when a user joined or left the group.
     3684 *
     3685 * @since 10.3.0
     3686 *
     3687 * @param BP_Groups_Member|int $groups_member The BP_Groups_Member object or the group member ID.
     3688 * @param int                  $group_id      The group's ID.
     3689 */
     3690function bp_groups_update_group_members_count( $groups_member, $group_id = 0 ) {
     3691    if ( $groups_member instanceof BP_Groups_Member ) {
     3692        $group_id = $groups_member->group_id;
     3693    }
     3694
     3695    BP_Groups_Member::refresh_total_member_count_for_group( (int) $group_id );
     3696}
     3697add_action( 'groups_member_after_save', 'bp_groups_update_group_members_count' );
     3698add_action( 'groups_member_after_remove', 'bp_groups_update_group_members_count' );
     3699add_action( 'bp_groups_member_after_delete', 'bp_groups_update_group_members_count', 10, 2 );
     3700
     3701/**
     3702 * Defers a group's counting to avoid updating it when batch adding/removing users to this group.
     3703 *
     3704 * @since 10.3.0
     3705 *
     3706 * @param bool $defer True to defer, false otherwise.
     3707 * @param int $group_id The group's ID.
     3708 */
     3709function bp_groups_defer_group_members_count( $defer = true, $group_id = 0 ) {
     3710    if ( $defer ) {
     3711        remove_action( 'groups_member_after_save', 'bp_groups_update_group_members_count' );
     3712        remove_action( 'groups_member_after_remove', 'bp_groups_update_group_members_count' );
     3713        remove_action( 'bp_groups_member_after_delete', 'bp_groups_update_group_members_count', 10, 2 );
     3714    } else {
     3715        add_action( 'groups_member_after_save', 'bp_groups_update_group_members_count' );
     3716        add_action( 'groups_member_after_remove', 'bp_groups_update_group_members_count' );
     3717        add_action( 'bp_groups_member_after_delete', 'bp_groups_update_group_members_count', 10, 2 );
     3718    }
     3719
     3720    if  ( $group_id ) {
     3721        bp_groups_update_group_members_count( 0, (int) $group_id );
     3722    }
     3723}
  • trunk/src/bp-groups/classes/class-bp-group-member-query.php

    r13108 r13280  
    5151
    5252    /**
     53     * Constructor.
     54     *
     55     * @since 10.3.0
     56     *
     57     * @param string|array|null $query See {@link BP_User_Query}.
     58     */
     59    public function __construct( $query = null ) {
     60        $qv = bp_parse_args(
     61            $query,
     62            array(
     63                'count' => false, // True to perform a count query. False otherwise.
     64            )
     65        );
     66
     67        parent::__construct( $qv );
     68    }
     69
     70    /**
    5371     * Set up action hooks.
    5472     *
     
    6381        }
    6482
    65         // Set the sort order.
    66         add_action( 'bp_pre_user_query', array( $this, 'set_orderby' ) );
    67 
    68         // Set up our populate_extras method.
    69         add_action( 'bp_user_query_populate_extras', array( $this, 'populate_group_member_extras' ), 10, 2 );
     83        if ( ! $this->query_vars_raw['count'] ) {
     84            // Set the sort order.
     85            add_action( 'bp_pre_user_query', array( $this, 'set_orderby' ) );
     86
     87            // Set up our populate_extras method.
     88            add_action( 'bp_user_query_populate_extras', array( $this, 'populate_group_member_extras' ), 10, 2 );
     89        } else {
     90            $this->query_vars_raw['orderby'] = 'ID';
     91        }
     92    }
     93
     94    /**
     95     * Use WP_User_Query() to pull data for the user IDs retrieved in the main query.
     96     *
     97     * If a `count` query is performed, the function is used to validate existing users.
     98     *
     99     * @since 10.3.0
     100     */
     101    public function do_wp_user_query() {
     102        if ( ! $this->query_vars_raw['count'] ) {
     103            return parent::do_wp_user_query();
     104        }
     105
     106        /**
     107         * Filters the WP User Query arguments before passing into the class.
     108         *
     109         * @since 10.3.0
     110         *
     111         * @param array         $value Array of arguments for the user query.
     112         * @param BP_User_Query $this  Current BP_User_Query instance.
     113         */
     114        $wp_user_query = new WP_User_Query(
     115            apply_filters(
     116                'bp_group_members_count_query_args',
     117                array(
     118                    // Relevant.
     119                    'fields'      => 'ID',
     120                    'include'     => $this->user_ids,
     121
     122                    // Overrides
     123                    'blog_id'     => 0,    // BP does not require blog roles.
     124                    'count_total' => false // We already have a count.
     125
     126                ),
     127                $this
     128            )
     129        );
     130
     131        // Validate existing user IDs.
     132        $this->user_ids = array_map( 'intval', $wp_user_query->results );
     133        $this->results  = $this->user_ids;
     134
     135        // Set the total existing users.
     136        $this->total_users = count( $this->user_ids );
    70137    }
    71138
     
    474541        return wp_list_pluck( $group_user_ids, 'user_id' );
    475542    }
     543
     544    /**
     545     * Perform a database query to populate any extra metadata we might need.
     546     *
     547     * If a `count` query is performed, the function is used to validate active users.
     548     *
     549     * @since 10.3.0
     550     */
     551    public function populate_extras() {
     552        if ( ! $this->query_vars_raw['count'] ) {
     553            return parent::populate_extras();
     554        }
     555
     556        // Validate active users.
     557        $active_users    = array_filter( BP_Core_User::get_last_activity( $this->user_ids ) );
     558        $active_user_ids = array_keys( $active_users );
     559        $this->results   = array_intersect( $this->user_ids, $active_user_ids );
     560
     561        // Set the total active users.
     562        $this->total_users = count( $this->results );
     563    }
    476564}
  • trunk/src/bp-groups/classes/class-bp-groups-group.php

    r13185 r13280  
    17901790     */
    17911791    public static function get_total_member_count( $group_id, $skip_cache = false ) {
    1792         $cache_key = 'total_member_count';
    1793         $count     = groups_get_groupmeta( $group_id, $cache_key );
     1792        $meta_key = 'total_member_count';
     1793        $count    = groups_get_groupmeta( $group_id, $meta_key );
    17941794
    17951795        if ( false === $count || true === $skip_cache ) {
    1796             $members = groups_get_group_members(
     1796            $group_members = new BP_Group_Member_Query(
    17971797                array(
    17981798                    'group_id'   => $group_id,
    17991799                    'group_role' => array( 'member', 'admin', 'mod' ),
    1800                     'type'       => 'active',
     1800                    'count'      => true,
    18011801                )
    18021802            );
    18031803
    1804             $count = $members['count'] ? $members['count'] : 0;
    1805 
    1806             groups_update_groupmeta( $group_id, $cache_key, (int) $count );
     1804            $count = $group_members->total_users;
     1805            groups_update_groupmeta( $group_id, $meta_key, $count );
    18071806        }
    18081807
     
    18151814         * @param int $group_id The ID of the group.
    18161815         */
    1817         return (int) apply_filters( 'bp_groups_total_member_count', (int) $count, (int) $group_id );
     1816        return (int) apply_filters( 'bp_groups_total_member_count', $count, (int) $group_id );
    18181817    }
    18191818
  • trunk/src/bp-groups/classes/class-bp-groups-member.php

    r13103 r13280  
    310310        self::refresh_total_group_count_for_user( $this->user_id );
    311311
    312         // Update the group's member count.
    313         self::refresh_total_member_count_for_group( $this->group_id );
    314 
    315312        /**
    316313         * Fires after the current group membership item has been saved.
     
    448445        self::refresh_total_group_count_for_user( $this->user_id );
    449446
    450         // Update the group's member count.
    451         self::refresh_total_member_count_for_group( $this->group_id );
    452 
    453447        /**
    454448         * Fires after a member is removed from a group.
     
    517511        // Update the user's group count.
    518512        self::refresh_total_group_count_for_user( $user_id );
    519 
    520         // Update the group's member count.
    521         self::refresh_total_member_count_for_group( $group_id );
    522513
    523514        /**
  • trunk/tests/phpunit/testcases/groups/functions.php

    r13103 r13280  
    88    static public $group_ids;
    99    static public $user_ids;
     10    protected $did_group_member_count = 0;
    1011
    1112    static public function wpSetUpBeforeClass( $factory ) {
     
    343344        $this->assertEquals( 2, groups_get_total_member_count( $g1 ) );
    344345        $this->assertEquals( 2, BP_Groups_Group::get_total_member_count( $g1 ) );
     346    }
     347
     348    /**
     349     * @group total_member_count
     350     * @ticket BP8688
     351     */
     352    public function test_total_member_count_groups_inactive_user() {
     353        $u1 = self::factory()->user->create();
     354        $u2 = wp_insert_user( array(
     355            'user_pass'  => 'foobar',
     356            'user_login' => 'foobar',
     357            'user_email' => 'foobar@buddypress.org',
     358        ) );
     359
     360        $g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
     361
     362        groups_join_group( $g1, $u2 );
     363
     364        $this->assertEquals( 1, groups_get_total_member_count( $g1 ) );
     365    }
     366
     367    /**
     368     * @group total_member_count
     369     * @ticket BP8688
     370     */
     371    public function test_total_member_count_groups_spammed_user() {
     372        $u1 = self::factory()->user->create();
     373        $u2 = self::factory()->user->create();
     374
     375        $g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
     376
     377        groups_join_group( $g1, $u2 );
     378        bp_core_process_spammer_status( $u2, 'spam' );
     379
     380        $this->assertEquals( 1, groups_get_total_member_count( $g1 ) );
     381    }
     382
     383    /**
     384     * @group total_member_count
     385     * @ticket BP8688
     386     */
     387    public function test_total_member_count_groups_deferred() {
     388        $u1 = self::factory()->user->create();
     389        $g1 = self::factory()->group->create( array( 'creator_id' => $u1 ) );
     390        $members = array();
     391        $this->did_group_member_count = 0;
     392
     393        add_filter( 'bp_groups_total_member_count', array( $this, 'filter_bp_groups_total_member_count' ) );
     394
     395        bp_groups_defer_group_members_count( true );
     396        for ( $i = 1; $i < 6; $i++ ) {
     397            $members[ $i ] = self::factory()->user->create();
     398            groups_join_group( $g1, $members[ $i ] );
     399        }
     400        bp_groups_defer_group_members_count( false, $g1 );
     401
     402        remove_filter( 'bp_groups_total_member_count', array( $this, 'filter_bp_groups_total_member_count' ) );
     403
     404        $this->assertTrue( 1 === $this->did_group_member_count );
     405        $this->assertEquals( count( $members ) + 1, groups_get_total_member_count( $g1 ) );
     406    }
     407
     408    public function filter_bp_groups_total_member_count( $count ) {
     409        $this->did_group_member_count += 1;
     410        return $count;
    345411    }
    346412
Note: See TracChangeset for help on using the changeset viewer.