3

I'm looking to load a list of Users by an Entity Reference Property within Rules. There may be times when I need to load hundreds or thousands of Users that have a matching Property to update a secondary Property. I'm having a difficult time determining the best performance option.

Here's my use case:

Users within our website may be linked to a Company (stored as a Node with Content-Type: company). When any user of this Company purchases membership, the company Node is updated with a Taxonomy Term Reference "Member." Then, every employee (read: User with entity reference to the same company node) of that organization needs to have a Taxonomy Reference field within their account updated to reflect they are a "Member (Corp)."

My Rules Based Solutions (that is executed when the order is first Paid In Full that sets the Data Value for that Property of the Company node, then...):

Solution A (Rules without VBO View):

  1. Fetch all User Entities By Property: company, where company = Company node.
  2. Loop through User list.
  3. Check that Entity has Field: "Membership Status."
  4. Set Data Value: "Membership Status" = "Member (Corp)."

Solution B (Rules with: Load a List of Entities from a VBO View):

  1. Pass nid as parameter to view.
  2. Loop through returned List.
  3. Check that Entity has Field: "Membership Status."
  4. Set Data Value: "Membership Status" = "Member (Corp)."

Solution C (Rules with: Load a List of Entity IDs from a VBO View):

  1. Pass nid as parameter to view.
  2. Loop through returned List.
  3. Fetch User Entity by ID.
  4. Check that Entity has Field: "Membership Status."
  5. Set Data Value: "Membership Status" = "Member (Corp)."

I'm hesitant to load up every Entity that matches due to Performance implications; however, being fairly new to Drupal, I'm not sure the most efficient method to bulk update all of the affected Users.

Can you help me better understand the performance implications of these solutions? Perhaps, I'm missing a better option that I haven't considered? Thanks in advance for your help!

*This question is in reference to Drupal 7.

1

Using Solution A will pretty sure have performance implications, so I wouldn't recommend that approach "to load hundreds or thousands of Users". Below is a blueprint of yet another approach (another option that you could considered, up to you to decide if it is "better"), which will allow you to reduce the performance risk significantly ...

Step 1 - Refine your view

Adapt the VBO view you already have, by limiting the results of that view to the first X users only. Pick a value for X that fits your needs, to start make it small enough and increase later on as much as you feel comfortable with.

Also adapt the views filter, to no longer include the users that have been processed already (by the Rules Component described in the next steps).

Step 2 - Create a Rules Component

Create a custom Rules Component which performs the Rules Actions as in your Solution "B" or "C". If the value of X is small enough, you can do some experiments with either of these solutions (B or C), i.e. to find out what happens if you increase the value of X (and based on that decide for yourself which one works best). Note that the list to be processed is limited to "X users" only (i.e. those that are contained in your views results from the previous step).

Add an additional Rules Action in your Rules Component, to actually reschedule the execution of this Rules Component itself after a few seconds or minutes (whatever fits). And trigger the very first execution of this Rules Component via a custom rule that is triggered "When any user of a Company purchases membership" (as in your question, I bet you have some appropriate Rules Event for that).

Step 3 - Stop rescheduling the Rules Component when the time is right

Also add a Rules Condition in the Rules Component of the previous step, to "Check the number of results returned by a VBO view" (using the same VBO view). The actual number of results should be at least 1. If there are zero results, then the Rules Actions of this Rules Component will not be performed, which is what we want. Since it will also prevent the the rules component reschedules itself when all users have been processed.

Step 4 - Review the frequency of your cron jobs

Make sure your cron job runs frequent enough, because each execution of your scheduled Rules Component will actually only happen when cron runs.

Conclusion: because this approach is based on the Rules Scheduler, the elapsed time will be longer, for sure. But the advantage of it is that, as compared to any of your 3 solutions as in your question, the poor user "of the Company who purchased the membership", will not have to wait for the rule to complete execution, before being able to do anything else on the site.

PS: I'm assuming this is about D7 ...

  • Thanks @pierre.vriens. I ended up marking your solution as correct since it actually addressed the question. Although, I ended up (explained in my answer) solving the issue by invoking a hook in a custom module on node_presave. – Shaun O Oct 18 '17 at 12:28
  • @ShaunO merci for the feedback! – Pierre.Vriens Oct 18 '17 at 12:40
1

I opted to use a custom module invoking node_hook_presave(), rather than Rules as I assumed it would be more efficient. This process checks if the "Membership Status" on the Company node has been changed prior to saving the Company.

/**
 * Implements hook_node_presave().
 */
function lia_company_node_presave($node){
    if($node->type=='company'){
        /**
         * If field_membership_status is set and the value has changed, then execute
         * the function  lia_company_update_membership_for_users($node->nid, $membership_status);
         */
        if(isset($node->original->field_membership_status[LANGUAGE_NONE][0]['tid'])
            && isset($node->field_membership_status[LANGUAGE_NONE][0]['tid'])
            && $node->original->field_membership_status[LANGUAGE_NONE][0]['tid'] <> $node->field_membership_status[LANGUAGE_NONE][0]['tid']) {

            $membership_status = $node->field_membership_status[LANGUAGE_NONE][0]['tid'];
            lia_company_update_membership_for_users($node->nid, $membership_status);
        }
    }
}

If the original value is set and the new value was not equal to the original value, I call a local method lia_company_update_membership_for_users($node->nid, $membership_status).

That method looks as follows. I'm thinking I probably should refactor this to run as a batch process. But any optimizations/feedback others find would be greatly appreciated.

/**
 * Implemented by hook_field_presave on Company node when field_membership_status is updated
 * on a Company node so that all of the Users referencing this Company are
 * updated to Member (Corp) or Non-Member, as appropriate.
 *
 * @param $company_nid
 * @param $membership_status_tid
 */
function lia_company_update_membership_for_users($company_nid, $membership_status_tid){
    $query = new EntityFieldQuery();
    $query
        ->entityCondition('entity_type', 'user')
        ->fieldCondition('field_company','target_id',$company_nid,'=');
    $results = $query->execute();

    $member_tid =  1744; // Taxonomy tid for Member
    $non_member_tid = 1749; // Taxonomy tid for Non-Member
    $member_corp_tid = 1745; // Taxonomy tid for Member (Corp)
    $member_status_excluded = array(
        1744, // Active
        1745, // Active (Corp)
        1748, // Deceased
        1746, // Deleted
    );

    foreach($results['user'] as $user){
        $user_wrapper = entity_metadata_wrapper('user',user_load($user->uid));

        /**
         * If the user is not already in an excluded membership_status, then we should update their status.
         * If the Company is being updated to a Member [TID: 1744] then, let's set all eligible Users to a
         * membership_status of Member (Corp) [TID: 1745]. Otherwise, we need to set them to Non-Member [TID: 1749].
         */
        if(isset($user_wrapper->field_membership_status) && !in_array($user_wrapper->field_membership_status->value()->tid, $member_status_excluded)){
            $user_wrapper->field_membership_status = ($membership_status_tid== $member_tid) ? $member_corp_tid : $non_member_tid;
            $user_wrapper->save();
        }
    }

}

Thank you @pierre.vriens for your answer. I'll probably end up refactoring when I have more time.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.