mehr-it / lara-db-batch-import
为laravel的eloquent模型提供批量导入功能
Requires
- php: >=7.1
- laravel/framework: ^6.0|^7.0|^8.0
- mehr-it/buffer: ^1.1
- mehr-it/lara-db-ext: ^2.0
- mehr-it/lara-transactions: ^1.0
Requires (Dev)
- orchestra/testbench: ^4.0|^5.0|^6.0
- phpunit/phpunit: ^7.4|^8.4
README
批量导入是任何大型系统的常见任务。此包实现了laravel的eloquent模型的批量导入。
如何使用
首先,必须将 ProvidesBatchImport
和 DbExtensions
(来自包 mehr-it/lara-db-ext)特性添加到模型中
class User extends Model {
uses DbExtensions;
uses ProvidesBatchImport;
}
这将为模型添加一个新的静态方法 batchImport()
。它创建一个新的批量导入实例,可以配置并用于导入数据。请参见以下示例
User::batchImport()
->matchBy('name')
->updateIfExists(['phone', 'email'])
->import([
new User([
'name' => 'Hans Maier',
'phone' = '+ 49 6081 1234',
'email' = 'hans.maier@example.com',
]),
new User([
'name' => 'Max Mustermann',
'phone' = '+ 49 61200 1234',
'email' = 'max.m@example.com',
]),
/* ... */
]);
示例代码旨在导入用户数据。对于现有记录(通过匹配“name”字段确定)插入新记录,更新“phone”和“email”字段。
幕后,导入数据以500条记录的块进行处理:对于每个块,数据库都会搜索现有记录。在提供的示例中,查询用户表以获取与当前块中提供的任何名称匹配的记录。结果与块数据进行比较:检查现有记录是否需要更新,并标记新记录以插入。使用批量插入/更新策略将结果修改发送到数据库。
记录匹配条件
可以使用 matchBy()
方法指定用于将两个记录视为“匹配”(因此更新而不是插入新数据)的字段。可以传递多个字段作为数组。
如果所有给定匹配字段字符串表示相等,则记录被认为是匹配的。默认情况下,匹配是区分大小写的。将第二个参数设置为false,以进行不区分大小写的匹配。
注意:根据SQL的“三值逻辑”,比较null值永远不会返回true。这意味着,如果“matchBy”字段中的任何一个为null,则记录不会与任何其他记录匹配!
比较仅限于相等比较,但是可以传递可调用函数来在比较之前处理值。
$import->matchBy(
[
'name' => function($v) { return substr($v, 0, 1); }
],
false /* false for case-insensitive */
);
如果没有显式设置“matchBy”字段,则使用模型的主键。
记录匹配内部
尽可能将“matchBy”字段转换为查询条件以匹配现有记录。但是,可调用函数不能用于SQL。
使用“matchBy”条件检索现有记录后,为每个记录构建一个使用所有“matchBy”标准进行比较的键。在此点,也正常化所有键为小写。具有相同比较键的模型被视为“相等”。
如果您需要为模型调整比较键,可以使用 withComparisonKey()
设置生成比较键的自定义回调。
修改模型查询
有时您可能想修改模型查询。假设您正在使用软删除并希望包括已删除的模型。在这种情况下,请使用 tapModelQuery()
方法。
$import->tapModelQuery(function($query){
return $query->withTrashed();
});
// when modifying the model query, usage of bypassModel() is mandatory
$import->bypassModel();
导入 仅当使用 bypassModel()
时,才能修改模型查询!
更新现有记录
如果应更新现有记录,则可以使用 updateIfExists()
方法指定在存在时更新的字段列表。
如果没有调用 updateIfExists
,则仅插入新记录!
《updateIfExists()`方法还接受静态值、SQL表达式或可调用的更新,以生成更定制化的值。
$import->updateIfExists([
'phone' => function($v) {
return str_replace(' ', '', $v);
},
'email' => new Expression('lower(email)'),
]);
添加回调函数
通常需要知道哪些记录受到了批量导入的影响。可以使用onInserted()`
、onUpdated()`
和onInsertedOrUpdated()`
方法来注册回调函数,接收相应的记录(默认为每批500条)。
$import->onUpdated(function($records) {
foreach($record as $currRecord) {
/* do s.th. */
}
});
注意:传递给回调函数的模型实例不一定与传递给导入函数的实例相同。此外,它们不包含更新的时间戳或插入的ID值,因为数据库操作使用了批量插入/更新策略。
确定缺失的记录
由于所有这些记录都“被看到”,因此确定哪些记录已被更新或插入相当容易。但是,批量导入并不了解其他任何记录。
尽管如此,之后可以帮助检测它们。它可以对任何“被看到”的记录标记一个顺序的批次ID。这使得在第二步中查询“缺失”的记录成为可能。这需要将新的批次ID传递给withBatchId()
方法
$import->withBatchId(100001);
这将把“last_batch_id”(必须在数据库中存在)设置给指定的值。
对于每个新的批量导入,必须递增批次ID!
导入后,可以使用whereMissingAfterBatch()
条件查询任何缺失的记录
User::query()
->whereMissingAfterBatch(100001)
->chunk(500, function($records) {
/* process missing records */
});
它返回任何批次ID为空或小于给定值的记录。
为了进行高效的SQL操作,应针对表设置相应的索引。
具有批次ID的模型
遵循最佳实践,批次ID应按模型生成。因此,将生成逻辑添加到模型类中是个好主意。实现GeneratesBatchIds
接口可以让批量导入操作自动从模型中获取下一个批次ID,而无需手动调用withBatchId()
class User extends Model implements GeneratesBatchIds {
/**
* Gets the next batch id
* @return string The next batch id as string
*/
public function nextBatchId(): string {
/* custom logic here */
}
}
import()
方法接受第二个参数,该参数将返回最后一个使用的批次ID。或者,可以使用getLastBatchId()
方法
$import->import($data, $lastBatchId);
// or
$import->getLastBatchId();
准备导入
有时准备导入、收集数据然后刷新导入非常方便。可以使用prepare()
方法进行此操作
// prepare
$prepared = $import->prepare();
// add records
$prepared->add($record1);
$prepared->addMultiple([$record2, $record3]);
// flush
$prepared->flush($lastBatchId);
绕过模型
使用模型来管理数据库数据提供了一个干净、舒适的接口。然而,这也带来了一些开销。当对大型数据集执行批量导入时,模型属性的设置/获取操作会执行数千次。性能影响可能是显著的。
如果您不需要模型属性功能,例如修改器、访问器、转换等,则bypassModel()
方法可以使您的应用程序速度更快。
$import->bypassModel();
在这种情况下,您必须以原始数组的形式提供数据,而不是以模型实例的形式。
bypassModel()
方法还接受一个名为“rawComparators”的第二个参数。它接受用于字段的自定义比较函数。这些用于将新数据与现有数据比较以检测更改。例如,数据库中存储的小数可能返回为'12.90',但您的输入可能是'12.9'。没有自定义比较器,该行将被检测为更改。但是,自定义比较器可以避免这种情况。
$import->bypassModel(true, [
'price' => function($new, $old) {
return bccomp($new, $old, 2);
}
]);