rkr/data-diff

一个方便的工具,可以快速以键值对方式比较结构化数据

0.2.2 2022-08-17 13:45 UTC

README

Scrutinizer Code Quality Build Status Latest Stable Version License

一个方便的工具,可以快速以键值对方式比较结构化数据

composer

见此处

WTF

如果你有很多结构化数据需要导入本地数据库(例如)并且不想在每次运行时都覆盖所有内容,那么这个组件对你来说很有趣。相反,你希望了解实际发生了什么变化,并据此采取行动。

使用方法

一开始,你有两个要比较的二维数据列表。通常,此类数据列表的一些列会说明新行和缺失行的实际差异。一些列会指出已找到现有行的更改。你也可以有不会引起任何操作的列,但它们的数据可能在后续处理中需要。

例如,你有一些文章元数据,这些数据可以从外部数据源获取,并且你希望将这些数据放入本地数据库中。应该将外部数据导入该本地数据库,并且每当数据集被添加、删除或更改时(例如,记录日志),你希望采取行动。

外部数据

name;reference;price;stock;last-change
Some Notebook;B0001;1499.90;1254;2016-04-01T10:00:00+02:00
A Hairdryer;C0001;49.95;66;2016-04-01T10:00:00+02:00
A Pencil;D0001;2.9499;2481;2016-04-01T10:00:00+02:00

本地数据

name;reference;price;stock
A shiny Smartphone;A0001;519.99;213
A Hairdryer;C0001;49.95;12
A Pencil;D0001;2.95;2481

在列表中,我们有三行数据。但在两个列表中都有一个在另一个列表中不可用的行,而唯一共同的行(“吹风机;C0001”和“铅笔;D0001”)在“价格”和“库存”列中有所不同,而“名称”在两个列表中相同。无论在“current-datetime”列中是什么,都不应该进行比较,但在插入或更新时也应该考虑。最终目标是将所有更改从外部数据源带到本地数据库。_知道一个“current-datetime”已经更改,而所有其他列保持不变,但这在这种情况下我不重要。_

通过比较两个不同的键值列表来计算实际的比较结果。通过三种方法进行比较,可以找到添加的键、缺失的键和键相等的更改数据。因此,为了获取这些信息,你需要了解如何表达特定的行已添加、删除或更改。这并不总是清晰的任务,并且取决于所涉及的数据。在这个例子中,我将设置一些规则,这些规则_可能_与你的场景不同。

在这个例子中,我们只考虑使用reference来判断列表中的一行是新的还是已删除。因此,本地数据库有一个指向文章A0001reference,它不包括在外部数据中。因此,我们希望因为这一点而将A0001从我们的本地数据中删除。B0001不在我们的本地数据中,因此应该添加。吹风机库存不同,铅笔价格略有不同。由于我们以两位小数的精度本地存储价格,这两个铅笔价格实际上相同,比较不应报告对行D0001的更改。

首先,你需要告诉Storage确切什么是键,什么是值,以定义Storage应理解为何键值列表的模式。我们不想转换列表,因为数据已经很好。

因此,让我们给列一些意义

  • reference列告诉我们,特定的行是否存在。这是每行的标识。一行可能有多个列作为标识符(例如referenceenvironment-id),但在此情况下,我只有一个标识符。
  • 当一行已经存在于其他列表中时,才应考虑name列。
  • 当一行已经存在于其他列表中时,才应考虑price列。
  • 当一行已经存在于其他列表中时,才应考虑stock列。
  • last-change列根本不需要检查。

因此,当我们构建一个键值数组来进行实际比较时,键部分由reference组成,值部分由列namepricestock表示。

第一个列表的键值数组将如下所示

'B0001' => ['Some Notebook', 1499.90, 1254]
'C0001' => ['A Hairdryer', 49.95, 66]
'D0001' => ['A Pencil', 2.9499, 2481]

第二个列表的键值数组将如下所示

'A0001' => ['A shiny Smartphone', 519.99, 213]
'C0001' => ['A Hairdryer', 49.95, 12]
'D0001' => ['A Pencil', 2.95, 2481]

现在,让我们以三种不同的方式比较这些数组

在第一个列表中但不在第二个列表中存在的行

'B0001' => ['Some Notebook', 1499.90, 1254]

在第二个列表中但不在第一个列表中存在的行

'A0001' => ['A shiny Smartphone', 519.99, 213]

在第一个列表中,但与第二个列表相比值已更改的行?

'C0001' => ['A Hairdryer', 49.95, 66]
'D0001' => ['A Pencil', 2.9499, 2481]

您拥有了匹配两个列表之间所有差异所需的所有信息。

这里有一个特殊情况。铅笔在第一个列表中的价格是2.9499。但由于我们只想比较两位小数的价格,因此实际上价格是相同的,因为计算出的价格在两种情况下都是2.95。这是Schema组件发挥作用的领域。

当您定义一个MemoryDiffStorage时,您指定两个模式。一个用于键部分,一个用于值部分

<?php
use DataDiff\MemoryDiffStorage;

$ds = new MemoryDiffStorage([
	'reference' => 'STRING',
], [
	'name' => 'STRING',
	'price' => 'MONEY',
	'stock' => 'INT',
]);

MemoryDiffStorage由两个存储器组成:StoreAStoreB。只要行至少包含在模式中定义的列,您就可以在每个存储器中插入任意数量的行和列。 列还需要有适当的名称,因为这些名称不会自动翻译。尽管如此,您可以在使用addRowaddRows的第二个参数添加行时指定翻译。这意味着,如果您的列在数据库和其他源中有不同的名称,您必须在将数据放入每个Store之前对这些键进行归一化。

以下是一个示例

<?php
use DataDiff\MemoryDiffStorage;

require 'vendor/autoload.php';

$ds = new MemoryDiffStorage([
	'reference' => 'STRING',
], [
	'name' => 'STRING',
	'price' => 'MONEY',
	'stock' => 'INT',
]);

$ds->storeA()->addRow(['name' => 'Some Notebook', 'reference' => 'B0001', 'price' => '1499.90', 'stock' => '1254', 'last-change' => '2016-04-01T10:00:00+02:00']);
$ds->storeA()->addRow(['name' => 'A Hairdryer', 'reference' => 'C0001', 'price' => '49.95', 'stock' => '66', 'last-change' => '2016-04-01T10:00:00+02:00']);
$ds->storeA()->addRow(['name' => 'A Pencil', 'reference' => 'D0001', 'price' => '2.9499', 'stock' => '2481', 'last-change' => '2016-04-01T10:00:00+02:00']);

$ds->storeB()->addRow(['name' => 'A shiny Smartphone', 'reference' => 'A0001', 'price' => '519.99', 'stock' => '213']);
$ds->storeB()->addRow(['name' => 'A Hairdryer', 'reference' => 'C0001', 'price' => '49.95', 'stock' => '12']);
$ds->storeB()->addRow(['name' => 'A Pencil', 'reference' => 'D0001', 'price' => '2.95', 'stock' => '2481']);

一个很好的经验法则是使用store a来存储您已经拥有的数据,并使用store b来存储要比较的数据(例如从外部数据源导入的数据)。

接下来,我们可以查询其中一个存储器以查找列表中的差异。由于store a包含我们的本地数据,我们使用store b来查询差异

获取所有存在于store b但不存在于store a的数据集

foreach($ds->storeB()->getNew() as $row) {
	$data = $row->getData();
	printf("This row is not present in store a: %s\n", $data['reference']);
}

结果是此行不在存储器b中:B0001

获取所有存在于store a但不存在于store b的数据集

foreach($ds->storeB()->getMissing() as $row) {
	$data = $row->getForeignData();
	printf("This row is not present in store a: %s\n", $data['reference']);
}

结果是此行不在存储器a中:A0001

获取所有更改的数据集

foreach($ds->storeB()->getChanged() as $row) {
	printf("This row is not present in store a: %s\n", $row->getDiffFormatted());
}

结果是此行不在存储器a中:stock:12 -> 66,last-change:-> 2016-04-01T10:00:00+02:00

如您所注意到的,D0001不在结果集中。这是因为模式已经规范化了列price的十进制精度,因此没有发生任何差异。

您还可以按每个模式中定义的键和值访问数据。如果您想从模式构建SQL语句,这很有帮助。您可以将键视为UPDATE语句中的WHERE条件,并将值视为实际要更改的数据(《SET》)

print_r($row->getLocal()->getKeyData());
print_r($row->getLocal()->getValueData());

示例

<?php
use DataDiff\MemoryDiffStorage;

require 'vendor/autoload.php';

$ds = new MemoryDiffStorage([
	'client_id' => 'INT',
], [
	'description' => 'STRING',
	'total' => 'MONEY',
]);

for($i=2; $i <= 501; $i++) {
	$row = ['client_id' => $i, 'description' => 'This is a test', 'total' => $i === 50 ? 60 : 59.98999, 'test' => $i % 2];
	$ds->storeA()->addRow($row);
}
for($i=1; $i <= 500; $i++) {
	$row = ['client_id' => $i, 'description' => 'This is a test', 'total' => 59.98999, 'test' => $i % 3];
	$ds->storeB()->addRow($row);
}

$res = $ds->storeA()->getNew();
foreach($res as $key => $value) {
	printf("Added  : %s\n", $value['client_id']);
}

$res = $ds->storeA()->getChanged();
foreach($res as $key => $value) {
	printf("Changed: %s\n", $value['client_id']);
}

$res = $ds->storeA()->getMissing();
foreach($res as $key => $value) {
	printf("Removed: %s\n", $value['client_id']);
}

echo "\n";

$res = $ds->storeA()->getChanged();
foreach($res as $key => $value) {
	print_r($value->getDiff());
}

输出

Added  : 501
Changed: 50
Removed: 1

Array
(
    [total] => Array
        (
            [local] => 60
            [foreign] => 59.98999
        )

    [test] => Array
        (
            [local] => 0
            [foreign] => 2
        )

)