MDL-63120 core_badges: Avoid multiple joins in sql statement

This commit is contained in:
= 2023-03-09 23:05:27 +01:00
parent 2e1c6fd43e
commit a076a80dec
2 changed files with 119 additions and 12 deletions

View File

@ -236,6 +236,7 @@ class award_criteria_activity extends award_criteria {
* @return array list($join, $where, $params)
*/
public function get_completed_criteria_sql() {
global $DB;
$join = '';
$where = '';
$params = array();
@ -257,18 +258,41 @@ class award_criteria_activity extends award_criteria {
}
return array($join, $where, $params);
} else {
foreach ($this->params as $param) {
$join .= " LEFT JOIN {course_modules_completion} cmc{$param['module']} ON
cmc{$param['module']}.userid = u.id AND
cmc{$param['module']}.coursemoduleid = :completedmodule{$param['module']} AND
( cmc{$param['module']}.completionstate = :completionpass{$param['module']} OR
cmc{$param['module']}.completionstate = :completionfail{$param['module']} OR
cmc{$param['module']}.completionstate = :completioncomplete{$param['module']} )";
$where .= " AND cmc{$param['module']}.coursemoduleid IS NOT NULL ";
$params["completedmodule{$param['module']}"] = $param['module'];
$params["completionpass{$param['module']}"] = COMPLETION_COMPLETE_PASS;
$params["completionfail{$param['module']}"] = COMPLETION_COMPLETE_FAIL;
$params["completioncomplete{$param['module']}"] = COMPLETION_COMPLETE;
// Get all cmids of modules related to the criteria.
$cmids = array_map(fn ($x) => $x['module'], $this->params);
list($cmcmodulessql, $paramscmc) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
// Create a sql query to get all users who have worked on these course modules.
$sql = "SELECT DISTINCT userid FROM {course_modules_completion} cmc "
. "WHERE coursemoduleid " . $cmcmodulessql . " AND "
. "( cmc.completionstate IN ( :completionpass, :completionfail, :completioncomplete ) )";
$paramscmcs = [
'completionpass' => COMPLETION_COMPLETE_PASS,
'completionfail' => COMPLETION_COMPLETE_FAIL,
'completioncomplete' => COMPLETION_COMPLETE
];
$paramscmc = array_merge($paramscmc, $paramscmcs);
$userids = $DB->get_records_sql($sql, $paramscmc);
// Now check each user if the user has a completion of each module.
$useridsbadgeable = array_keys(array_filter(
$userids,
function ($user) use ($cmcmodulessql, $paramscmc, $cmids) {
global $DB;
$params = array_merge($paramscmc, ['userid' => $user->userid]);
$select = "coursemoduleid " . $cmcmodulessql . " AND userid = :userid";
$cmidsuser = $DB->get_fieldset_select('course_modules_completion', 'coursemoduleid', $select, $params);
return empty(array_diff($cmidsuser, $cmids));
}
));
// Finally create a where statement (if neccessary) with all userids who are allowed to get the badge.
// This list also includes all users who have previously received the badge. These are filtered out in the badge.php.
$join = "";
$where = "";
if (!empty($useridsbadgeable)) {
list($wherepart, $params) = $DB->get_in_or_equal($useridsbadgeable, SQL_PARAMS_NAMED);
$where = " AND u.id " . $wherepart;
}
return array($join, $where, $params);
}

View File

@ -31,6 +31,7 @@ require_once($CFG->libdir . '/badgeslib.php');
require_once($CFG->dirroot . '/badges/lib.php');
use core_badges\helper;
use core\task\manager;
class badgeslib_test extends advanced_testcase {
protected $badgeid;
@ -511,6 +512,88 @@ class badgeslib_test extends advanced_testcase {
$this->assertEquals(badge_message_from_template($message, $params), $result);
}
/**
* Test for working around the 61 tables join limit of mysql in award_criteria_activity in combination with the scheduled task.
* @return void
* @throws dml_transaction_exception
* @throws coding_exception
* @throws dml_exception
* @throws moodle_exception
* @throws ddl_exception
* @throws AssertionFailedError
* @throws InvalidArgumentException
* @throws ExpectationFailedException
* @covers \core_badges\badge->review_all_criteria()
*/
public function test_badge_activity_criteria_with_a_huge_number_of_coursemodules() {
global $CFG;
require_once($CFG->dirroot.'/completion/criteria/completion_criteria_activity.php');
// Messaging is not compatible with transactions.
$this->preventResetByRollback();
// Create more than 61 modules to potentially trigger an mysql db error.
$assigncount = 75;
$assigns = [];
for ($i = 1; $i <= $assigncount; $i++) {
$assigns[] = $this->getDataGenerator()->create_module('assign', ['course' => $this->course->id], ['completion' => 1]);
}
$assigncmids = array_flip(array_map(fn ($assign) => $assign->cmid, $assigns));
$criteriaactivityarray = array_fill_keys(array_keys($assigncmids), 1);
// Set completion criteria.
$criteriadata = (object) [
'id' => $this->course->id,
'criteria_activity' => $criteriaactivityarray,
];
$criterion = new completion_criteria_activity();
$criterion->update_config($criteriadata);
$badge = new badge($this->coursebadge);
$criteriaoverall = award_criteria::build(array('criteriatype' => BADGE_CRITERIA_TYPE_OVERALL, 'badgeid' => $badge->id));
$criteriaoverall->save(array('agg' => BADGE_CRITERIA_AGGREGATION_ANY));
$criteriaactivity = award_criteria::build(['criteriatype' => BADGE_CRITERIA_TYPE_ACTIVITY, 'badgeid' => $badge->id]);
$modulescrit = ['agg' => BADGE_CRITERIA_AGGREGATION_ALL];
foreach ($assigns as $assign) {
$modulescrit['module_' . $assign->cmid] = $assign->cmid;
}
$criteriaactivity->save($modulescrit);
// Take one assign to complete it later.
$assingtemp = array_shift($assigns);
// Mark the user to complete the modules.
foreach ($assigns as $assign) {
$cmassign = get_coursemodule_from_id('assign', $assign->cmid);
$completion = new \completion_info($this->course);
$completion->update_state($cmassign, COMPLETION_COMPLETE, $this->user->id);
}
// Run the scheduled task to issue the badge. But the badge should not be issued.
ob_start();
$task = manager::get_scheduled_task('core\task\badges_cron_task');
$task->execute();
ob_end_clean();
$this->assertFalse($badge->is_issued($this->user->id));
// Now complete the last uncompleted module.
$cmassign = get_coursemodule_from_id('assign', $assingtemp->cmid);
$completion = new \completion_info($this->course);
$completion->update_state($cmassign, COMPLETION_COMPLETE, $this->user->id);
// Run the scheduled task to issue the badge. Now the badge schould be issued.
ob_start();
$task = manager::get_scheduled_task('core\task\badges_cron_task');
$task->execute();
ob_end_clean();
$this->assertDebuggingCalled('Error baking badge image!');
$this->assertTrue($badge->is_issued($this->user->id));
}
/**
* Test badges observer when course module completion event id fired.
*/