mehr-it/lara-db-batch-import

为laravel的eloquent模型提供批量导入功能

1.14.0 2022-03-02 10:55 UTC

This package is auto-updated.

Last update: 2024-09-29 05:36:15 UTC


README

Latest Version on Packagist Build Status

批量导入是任何大型系统的常见任务。此包实现了laravel的eloquent模型的批量导入。

如何使用

首先,必须将 ProvidesBatchImportDbExtensions(来自包 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);
    }
]);