FIX submission performance issues with large data

The more submissions a form receives, the more submission fields it must
process just to be able to load `getCMSFields`. Arguably submission data
does not belong here, but this is beyond the scope of this patch.

On popular forms it is not improbable to be trying to process 300,000
submitted fields just to test the unique sets of name and title...
however databases have the ability to do this without wasting PHP cycles
and memory, leaving us with a much smaller set to process and hopefully
bypassing one (of several) performance issues with this module.

The consequence of not making allowance for this is that a page in the
CMS suddenly stops saving or loading via web server or PHP (or both)
process timeouts (e.g. saving takes longer than 30 seconds so saving
never happens).
This commit is contained in:
Dylan Wagstaff 2022-06-13 19:36:35 +12:00
parent 198466a979
commit 3cd8c7ea77
3 changed files with 92 additions and 82 deletions

View File

@ -145,6 +145,10 @@ class EditableFormField extends DataObject
'ShowOnLoad' => true,
];
private static $indexes = [
'Name' => 'Name',
];
/**
* @config

View File

@ -34,6 +34,10 @@ class SubmittedFormField extends DataObject
private static $table_name = 'SubmittedFormField';
private static $indexes = [
'Name' => 'Name',
];
/**
* @param Member $member
* @param array $context

View File

@ -193,7 +193,6 @@ trait UserForm
// define tabs
$fields->findOrMakeTab('Root.FormOptions', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.CONFIGURATION', 'Configuration'));
$fields->findOrMakeTab('Root.Recipients', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.RECIPIENTS', 'Recipients'));
$fields->findOrMakeTab('Root.Submissions', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SUBMISSIONS', 'Submissions'));
// text to show on complete
@ -237,87 +236,8 @@ trait UserForm
$fields->addFieldToTab('Root.Recipients', $emailRecipients);
$fields->addFieldsToTab('Root.FormOptions', $this->getFormOptions());
// view the submissions
// make sure a numeric not a empty string is checked against this int column for SQL server
$parentID = (!empty($this->ID)) ? (int) $this->ID : 0;
// get a list of all field names and values used for print and export CSV views of the GridField below.
$columnSQL = <<<SQL
SELECT "SubmittedFormField"."Name" as "Name", COALESCE("EditableFormField"."Title", "SubmittedFormField"."Title") as "Title", COALESCE("EditableFormField"."Sort", 999) AS "Sort"
FROM "SubmittedFormField"
LEFT JOIN "SubmittedForm" ON "SubmittedForm"."ID" = "SubmittedFormField"."ParentID"
LEFT JOIN "EditableFormField" ON "EditableFormField"."Name" = "SubmittedFormField"."Name" AND "EditableFormField"."ParentID" = '$parentID'
WHERE "SubmittedForm"."ParentID" = '$parentID'
ORDER BY "Sort", "Title"
SQL;
// Sanitise periods in title
$columns = array();
foreach (DB::query($columnSQL)->map() as $name => $title) {
$columns[$name] = trim(strtr($title ?? '', '.', ' '));
}
$config = GridFieldConfig::create();
$config->addComponent(new GridFieldToolbarHeader());
$config->addComponent($sort = new GridFieldSortableHeader());
$config->addComponent($filter = new UserFormsGridFieldFilterHeader());
$config->addComponent(new GridFieldDataColumns());
$config->addComponent(new GridFieldEditButton());
$config->addComponent(new GridFieldDeleteAction());
$config->addComponent(new GridFieldPageCount('toolbar-header-right'));
$config->addComponent($pagination = new GridFieldPaginator(25));
$config->addComponent(new GridFieldDetailForm(null, true, false));
$config->addComponent(new GridFieldButtonRow('after'));
$config->addComponent($export = new GridFieldExportButton('buttons-after-left'));
$config->addComponent($print = new GridFieldPrintButton('buttons-after-left'));
// show user form items in the summary tab
$summaryarray = array(
'ID' => 'ID',
'Created' => 'Created',
'LastEdited' => 'Last Edited'
);
foreach (EditableFormField::get()->filter(array('ParentID' => $parentID)) as $eff) {
if ($eff->ShowInSummary) {
$summaryarray[$eff->Name] = $eff->Title ?: $eff->Name;
}
}
$config->getComponentByType(GridFieldDataColumns::class)->setDisplayFields($summaryarray);
/**
* Support for {@link https://github.com/colymba/GridFieldBulkEditingTools}
*/
if (class_exists(BulkManager::class)) {
$config->addComponent(new BulkManager);
}
$sort->setThrowExceptionOnBadDataType(false);
$filter->setThrowExceptionOnBadDataType(false);
$pagination->setThrowExceptionOnBadDataType(false);
// attach every column to the print view form
$columns['Created'] = 'Created';
$columns['SubmittedBy.Email'] = 'Submitter';
$filter->setColumns($columns);
// print configuration
$print->setPrintHasHeader(true);
$print->setPrintColumns($columns);
// export configuration
$export->setCsvHasHeader(true);
$export->setExportColumns($columns);
$submissions = GridField::create(
'Submissions',
'',
$this->Submissions()->sort('Created', 'DESC'),
$config
);
$submissions = $this->getSubmissionsGridField();
$fields->findOrMakeTab('Root.Submissions', _t('SilverStripe\\UserForms\\Model\\UserDefinedForm.SUBMISSIONS', 'Submissions'));
$fields->addFieldToTab('Root.Submissions', $submissions);
$fields->addFieldToTab(
'Root.FormOptions',
@ -344,6 +264,88 @@ SQL;
return $fields;
}
public function getSubmissionsGridField()
{
// view the submissions
// make sure a numeric not a empty string is checked against this int column for SQL server
$parentID = (!empty($this->ID)) ? (int) $this->ID : 0;
// get a list of all field names and values used for print and export CSV views of the GridField below.
$columnSQL = <<<SQL
SELECT DISTINCT
"SubmittedFormField"."Name" as "Name",
REPLACE(COALESCE("EditableFormField"."Title", "SubmittedFormField"."Title"), '.', ' ') as "Title",
COALESCE("EditableFormField"."Sort", 999) AS "Sort"
FROM "SubmittedFormField"
LEFT JOIN "SubmittedForm" ON "SubmittedForm"."ID" = "SubmittedFormField"."ParentID"
LEFT JOIN "EditableFormField" ON "EditableFormField"."Name" = "SubmittedFormField"."Name"
WHERE "SubmittedForm"."ParentID" = '$parentID'
AND "EditableFormField"."ParentID" = '$parentID'
ORDER BY "Sort", "Title"
SQL;
$columns = DB::query($columnSQL)->map();
$config = GridFieldConfig::create();
$config->addComponent(new GridFieldToolbarHeader());
$config->addComponent($sort = new GridFieldSortableHeader());
$config->addComponent($filter = new UserFormsGridFieldFilterHeader());
$config->addComponent(new GridFieldDataColumns());
$config->addComponent(new GridFieldEditButton());
$config->addComponent(new GridFieldDeleteAction());
$config->addComponent(new GridFieldPageCount('toolbar-header-right'));
$config->addComponent($pagination = new GridFieldPaginator(25));
$config->addComponent(new GridFieldDetailForm(null, true, false));
$config->addComponent(new GridFieldButtonRow('after'));
$config->addComponent($export = new GridFieldExportButton('buttons-after-left'));
$config->addComponent($print = new GridFieldPrintButton('buttons-after-left'));
// show user form items in the summary tab
$summaryarray = array(
'ID' => 'ID',
'Created' => 'Created',
'LastEdited' => 'Last Edited'
);
foreach (EditableFormField::get()->filter(['ParentID' => $parentID, 'ShowInSummary' => 1]) as $eff) {
$summaryarray[$eff->Name] = $eff->Title ?: $eff->Name;
}
$config->getComponentByType(GridFieldDataColumns::class)->setDisplayFields($summaryarray);
/**
* Support for {@link https://github.com/colymba/GridFieldBulkEditingTools}
*/
if (class_exists(BulkManager::class)) {
$config->addComponent(new BulkManager);
}
$sort->setThrowExceptionOnBadDataType(false);
$filter->setThrowExceptionOnBadDataType(false);
$pagination->setThrowExceptionOnBadDataType(false);
// attach every column to the print view form
$columns['Created'] = 'Created';
$columns['SubmittedBy.Email'] = 'Submitter';
$filter->setColumns($columns);
// print configuration
$print->setPrintHasHeader(true);
$print->setPrintColumns($columns);
// export configuration
$export->setCsvHasHeader(true);
$export->setExportColumns($columns);
$submissions = GridField::create(
'Submissions',
'',
$this->Submissions()->sort('Created', 'DESC'),
$config
);
return $submissions;
}
/**
* Allow overriding the EmailRecipients on a {@link DataExtension}
* so you can customise who receives an email.