MDL-34055 add bulk DB->insert_records() method

This patch was inspired by patch by Simon Coggins from Totara.
This commit is contained in:
Petr Škoda 2014-01-18 16:03:31 +08:00 committed by Petr Skoda
parent 9788e26805
commit cc5dba8e54
4 changed files with 369 additions and 2 deletions

View file

@ -506,7 +506,7 @@ class mysqli_native_moodle_database extends moodle_database {
* Returns detailed information about columns in table. This information is cached internally.
* @param string $table name
* @param bool $usecache
* @return array array of database_column_info objects indexed with column names
* @return database_column_info[] array of database_column_info objects indexed with column names
*/
public function get_columns($table, $usecache=true) {
@ -1122,6 +1122,124 @@ class mysqli_native_moodle_database extends moodle_database {
return $this->insert_record_raw($table, $cleaned, $returnid, $bulk);
}
/**
* Insert multiple records into database as fast as possible.
*
* Order of inserts is maintained, but the operation is not atomic,
* use transactions if necessary.
*
* This method is intended for inserting of large number of small objects,
* do not use for huge objects with text or binary fields.
*
* @since 2.7
*
* @param string $table The database table to be inserted into
* @param array|Traversable $dataobjects list of objects to be inserted, must be compatible with foreach
* @return void does not return new record ids
*
* @throws coding_exception if data objects have different structure
* @throws dml_exception A DML specific exception is thrown for any errors.
*/
public function insert_records($table, $dataobjects) {
if (!is_array($dataobjects) and !$dataobjects instanceof Traversable) {
throw new coding_exception('insert_records() passed non-traversable object');
}
// MySQL has a relatively small query length limit by default,
// make sure 'max_allowed_packet' in my.cnf is high enough
// if you change the following default...
static $chunksize = null;
if ($chunksize === null) {
if (!empty($this->dboptions['bulkinsertsize'])) {
$chunksize = (int)$this->dboptions['bulkinsertsize'];
} else {
if (PHP_INT_SIZE === 4) {
// Bad luck for Windows, we cannot do any maths with large numbers.
$chunksize = 5;
} else {
$sql = "SHOW VARIABLES LIKE 'max_allowed_packet'";
$this->query_start($sql, null, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
$size = 0;
if ($rec = $result->fetch_assoc()) {
$size = $rec['Value'];
}
$result->close();
// Hopefully 200kb per object are enough.
$chunksize = (int)($size / 200000);
if ($chunksize > 50) {
$chunksize = 50;
}
}
}
}
$columns = $this->get_columns($table, true);
$fields = null;
$count = 0;
$chunk = array();
foreach ($dataobjects as $dataobject) {
if (!is_array($dataobject) and !is_object($dataobject)) {
throw new coding_exception('insert_records() passed invalid record object');
}
$dataobject = (array)$dataobject;
if ($fields === null) {
$fields = array_keys($dataobject);
$columns = array_intersect_key($columns, $dataobject);
unset($columns['id']);
} else if ($fields !== array_keys($dataobject)) {
throw new coding_exception('All dataobjects in insert_records() must have the same structure!');
}
$count++;
$chunk[] = $dataobject;
if ($count === $chunksize) {
$this->insert_chunk($table, $chunk, $columns);
$chunk = array();
$count = 0;
}
}
if ($count) {
$this->insert_chunk($table, $chunk, $columns);
}
}
/**
* Insert records in chunks.
*
* Note: can be used only from insert_records().
*
* @param string $table
* @param array $chunk
* @param database_column_info[] $columns
*/
protected function insert_chunk($table, array $chunk, array $columns) {
$fieldssql = '('.implode(',', array_keys($columns)).')';
$valuessql = '('.implode(',', array_fill(0, count($columns), '?')).')';
$valuessql = implode(',', array_fill(0, count($chunk), $valuessql));
$params = array();
foreach ($chunk as $dataobject) {
foreach ($columns as $field => $column) {
$params[] = $this->normalise_value($column, $dataobject[$field]);
}
}
$sql = "INSERT INTO {$this->prefix}$table $fieldssql VALUES $valuessql";
list($sql, $params, $type) = $this->fix_sql_params($sql, $params);
$rawsql = $this->emulate_bound_params($sql, $params);
$this->query_start($sql, $params, SQL_QUERY_INSERT);
$result = $this->mysqli->query($rawsql);
$this->query_end($result);
}
/**
* Import a record into a table, id field is required.
* Safety checks are NOT carried out. Lobs are supported.