【问题标题】:How to efficiently compare 2 objects with varying keys but potentially matching values?如何有效地比较具有不同键但可能匹配值的 2 个对象?
【发布时间】:2018-09-28 19:08:51
【问题描述】:

我有 2 个对象,1 个来自 SQL 数据库,另一个来自 JSON REST API。在使用来自 api 的新数据更新数据库中的一行之前,我不仅要检查 ID 是否已经出现在数据库中(这意味着更新而不是插入),还要检查大部分属性。这样做的原因是,当它确实在数据库端更新时,会添加一个额外的“lastUpdate”日期时间以供以后在 PowerBI 中进行处理,如果我只检查 ID,那么“lastUpdate”将被触发每一个来自 API 的条目已经在数据库中的时间,即使它的属性实际上没有更新。每边的单个对象(它们以数组的形式出现)如下:


案例和说明:

重要提示:此部分是根据 Nina Scholz(现在)接受的解决方案的要求添加的。请在阅读时牢记这一点。

  • 如果路径在 API 端不能完全遍历(即它的父级为 null),则它应该返回 Null。例如,DB 端 callerLocationID 的路径在 API 上将是 callerLocation.id,但是当没有设置 callerLocation 时,父 callerLocation 已经是 null,从而使 id 无法访问

  • 如果一条路径在两边都可以完全通过,则需要比较这些值。

  • 如果无法在 API 端遍历路径,则该路径应为 null。例如,我可以将callerLocationID: nullcallerLocation.id 进行比较,因为后者是null,而null 将与null 相同

  • 一旦遍历所有路径并比较所有值,我需要知道“是的,它们都相同”(true)或“不,它们不相同”(false)。无需知道在哪里如果它们不相同,则发送整个对象以进行更新。

  • DB端的路径基本是一成不变的,如果有null那是因为数据库允许这样,可以接受

  • DB 端以 ID 结尾的所有属性(incidentID 除外)都引用 API 中的单个嵌套 id 值。例如callerID 引用caller.idcallerBranchID 引用callerBranch.idoperatorID 引用operator.id

  • 一些例外情况是:

    • impact 引用 impact.name
    • urgency 引用 urgency.name
    • priority 参考priority.name
    • duration 引用 duration.name
    • escalationOperator 引用 escalationOperator.id
  • API 中的所有 optionalField 内容都可以忽略


// Database Object
{
    incidentID: '0dc1a10f-2899-485a-b814-f72f29c9a15a',
    status: 'secondLine',
    briefDescription: 'Support niet bereikbaar',
    callDate: '2018-04-10T19:01:00.000Z',
    lastUpdate: '2018-04-18T14:02:17.000Z',
    number: 'M1804 021',
    request: '10-04-2018 21:02 Middelkoop, Paul: \nIk kan de servicedesk niet bereiken, telkens in gesprek',
    callerID: '4e723042-0037-4e05-a362-e65c620ba734',
    callerBranchID: 'f66e7804-b57a-4418-a991-997e574ead29',
    callerLocationID: null,
    externalNumber: null,
    categoryID: 'cafb0af8-e43a-4391-ac9e-a0345abbcc4f',
    subcategoryID: 'f56099a9-7c60-45e3-94b4-6555a79d4bd7',
    callTypeID: '04b678a6-791e-4662-9bc8-97573555f15e',
    entryTypeID: 'a9c486fd-a93e-565e-bfeb-17619fafe1a8',
    branchID: null,
    locationID: null,
    impact: null,
    urgency: null,
    priority: null,
    duration: null,
    operatorID: 'a17aba85-13a7-4ac6-8c57-693a512b633e',
    operatorGroupID: 'a17aba85-13a7-4ac6-8c57-693a512b633e',
    supplierID: null,
    targetDate: '2019-04-10T15:30:00.000Z',
    onHold: false,
    onHoldDate: null,
    onHoldDuration: 0,
    feedbackMessage: null,
    feedbackRating: null,
    processingStatus: 'Afgemeld',
    completed: true,
    completedDate: '2018-04-10T19:09:00.000Z',
    closed: true,
    closedDate: null,
    closureCode: null,
    creatorID: '226082ea-8d74-4dee-ae1e-74c33c883792',
    creationDate: '2018-04-10T19:02:34.000Z',
    timeSpent: 0,
    timeSpentFirstLine: 0,
    timeSpentSecondLineAndPartials: 0,
    costs: 0,
    escalationStatus: null,
    escalationReason: null,
    escalationOperator: null,
    modifier: '226082ea-8d74-4dee-ae1e-74c33c883792',
    modificationDate: '2018-04-10T19:12:08.000Z',
    expectedTimeSpent: 0,
    majorCall: false,
    majorCallID: null,
    publishToSSD: false,
    monitored: false,
    archivingReason: null
}

// API Object
{
    id: '0dc1a10f-2899-485a-b814-f72f29c9a15a',
    status: 'secondLine',
    number: 'M1804 021',
    request: '10-04-2018 21:02 Middelkoop, Paul: \nIk kan de servicedesk niet bereiken, telkens in gesprek',
    requests: '/tas/api/incidents/id/0dc1a10f-2899-485a-b814-f72f29c9a15a/requests',
    action: '/tas/api/incidents/id/0dc1a10f-2899-485a-b814-f72f29c9a15a/actions',
    attachments: '/tas/api/incidents/id/0dc1a10f-2899-485a-b814-f72f29c9a15a/attachments',
    caller: {
        id: '4e723042-0037-4e05-a362-e65c620ba734',
        dynamicName: 'Mafficioli del Castelletto, Richard',
        branch: {
            id: 'f66e7804-b57a-4418-a991-997e574ead29',
            name: 'Ask Roger! Delft',
            clientReferenceNumber: '',
            timeZone: 'Europe/Amsterdam',
            extraA: null,
            extraB: null
        }
    },
    callerBranch: {
        id: 'f66e7804-b57a-4418-a991-997e574ead29',
        name: 'Ask Roger! Delft',
        clientReferenceNumber: '',
        timeZone: 'Europe/Amsterdam',
        extraA: null,
        extraB: null
    },
    callerLocation: null,
    branchExtraFieldA: null,
    branchExtraFieldB: null,
    briefDescription: 'Support niet bereikbaar',
    externalNumber: '',
    category: {
        id: 'cafb0af8-e43a-4391-ac9e-a0345abbcc4f',
        name: 'Communicatie'
    },
    subcategory: {
        id: 'f56099a9-7c60-45e3-94b4-6555a79d4bd7',
        name: 'Vaste telefonie'
    },
    callType: {
        id: '04b678a6-791e-4662-9bc8-97573555f15e',
        name: 'Klacht'
    },
    entryType: {
        id: 'a9c486fd-a93e-565e-bfeb-17619fafe1a8',
        name: 'Mondeling'
    },
    object: null,
    branch: null,
    location: null,
    impact: null,
    urgency: null,
    priority: null,
    duration: null,
    targetDate: '2019-04-10T15:30:00.000+0000',
    onHold: false,
    onHoldDate: null,
    onHoldDuration: 0,
    feedbackMessage: null,
    feedbackRating: null,
    operator: {
        id: 'a17aba85-13a7-4ac6-8c57-693a512b633e',
        status: 'operatorGroup',
        name: 'Systeembeheer'
    },
    operatorGroup: {
        id: 'a17aba85-13a7-4ac6-8c57-693a512b633e',
        name: 'Systeembeheer'
    },
    supplier: null,
    processingStatus: {
        id: '70b2967d-e248-4ff9-a632-ec044410d5a6',
        name: 'Afgemeld'
    },
    completed: true,
    completedDate: '2018-04-10T19:09:00.000+0000',
    closed: true,
    closedDate: '2018-04-10T19:12:00.000+0000',
    closureCode: null,
    timeSpent: 0,
    timeSpentFirstLine: 0,
    timeSpentSecondLineAndPartials: 0,
    costs: 0,
    escalationStatus: null,
    escalationReason: null,
    escalationOperator: null,
    callDate: '2018-04-10T19:01:00.000+0000',
    creator: {
        id: '226082ea-8d74-4dee-ae1e-74c33c883792',
        name: 'Middelkoop, Paul'
    },
    creationDate: '2018-04-10T19:02:34.000+0000',
    modifier: {
        id: '226082ea-8d74-4dee-ae1e-74c33c883792',
        name: 'Middelkoop, Paul'
    },
    modificationDate: '2018-04-10T19:12:08.000+0000',
    majorCall: false,
    majorCallObject: null,
    publishToSsd: false,
    monitored: false,
    expectedTimeSpent: 0,
    archivingReason: null,
    optionalFields1: {
        boolean1: false,
        boolean2: false,
        boolean3: false,
        boolean4: false,
        boolean5: false,
        number1: 0,
        number2: 0,
        number3: 0,
        number4: 0,
        number5: 0,
        date1: null,
        date2: null,
        date3: null,
        date4: null,
        date5: null,
        text1: '',
        text2: '',
        text3: '',
        text4: '',
        text5: '',
        memo1: null,
        memo2: null,
        memo3: null,
        memo4: null,
        memo5: null,
        searchlist1: null,
        searchlist2: null,
        searchlist3: null,
        searchlist4: null,
        searchlist5: null
    },
    optionalFields2: {
        boolean1: false,
        boolean2: false,
        boolean3: false,
        boolean4: false,
        boolean5: false,
        number1: 0,
        number2: 0,
        number3: 0,
        number4: 0,
        number5: 0,
        date1: null,
        date2: null,
        date3: null,
        date4: null,
        date5: null,
        text1: '',
        text2: '',
        text3: '',
        text4: '',
        text5: '',
        memo1: null,
        memo2: null,
        memo3: null,
        memo4: null,
        memo5: null,
        searchlist1: null,
        searchlist2: null,
        searchlist3: null,
        searchlist4: null,
        searchlist5: null
    }
}

到目前为止我已经做了什么

  1. 遍历 API 对象数组,并为每一个对象检查 ID 是否是使用 Fuse.JS 的数据库对象数组(阈值为 0,仅用于完美匹配)

  2. 结合使用 1 的结果和 UnderscoreJS 中的 .first、.keys 和 .pick 方法来确定当前迭代中的 2 个对象之间哪些键是相同的,以便快速检查这些对象

    // tdIncidents is the array of objects from the API
    // dbIncidents is the array of objects from the database
    // tdinci is my iterator, consider it the "i" in the for loop
    // at this point it has already been confirmed that both dbIncidents and tdIncidents have at least 1 entry thus using [0] won't give any problems
    const db = _.first(fuse.search(tdIncidents[tdinci].id)),
      td = tdIncidents[tdinci],
      dbKeys = _.keys(dbIncidents[0]),
      tdKeys = _.keys(tdIncidents[0]),
      identicalKeysTd = _.pick(td, (value, key) => dbKeys.includes(key)),
      identicalKeysDb = _.pick(db, (value, key) => tdKeys.includes(key));

identicalKeysTd 将导致:

{ status: 'secondLine',
  number: 'M1804 021',
  request: '10-04-2018 21:02 Middelkoop, Paul: \nIk kan de servicedesk niet bereiken, telkens in gesprek',
  briefDescription: 'Support niet bereikbaar',
  externalNumber: '',
  impact: null,
  urgency: null,
  priority: null,
  duration: null,
  targetDate: '2019-04-10T15:30:00.000+0000',
  onHold: false,
  onHoldDate: null,
  onHoldDuration: 0,
  feedbackMessage: null,
  feedbackRating: null,
  processingStatus: { id: '70b2967d-e248-4ff9-a632-ec044410d5a6', name: 'Afgemeld' },
  completed: true,
  completedDate: '2018-04-10T19:09:00.000+0000',
  closed: true,
  closedDate: '2018-04-10T19:12:00.000+0000',
  closureCode: null,
  timeSpent: 0,
  timeSpentFirstLine: 0,
  timeSpentSecondLineAndPartials: 0,
  costs: 0,
  escalationStatus: null,
  escalationReason: null,
  escalationOperator: null,
  callDate: '2018-04-10T19:01:00.000+0000',
  creationDate: '2018-04-10T19:02:34.000+0000',
  modifier:
   { id: '226082ea-8d74-4dee-ae1e-74c33c883792',
     name: 'Middelkoop, Paul' },
  modificationDate: '2018-04-10T19:12:08.000+0000',
  majorCall: false,
  monitored: false,
  expectedTimeSpent: 0,
  archivingReason: null }

identicalKeysDb 将导致:

{ status: 'secondLine',
  briefDescription: 'Support niet bereikbaar',
  callDate: '2018-04-10T19:01:00.000Z',
  number: 'M1804 021',
  request: '10-04-2018 21:02 Middelkoop, Paul: \nIk kan de servicedesk niet bereiken, telkens in gesprek',
  externalNumber: null,
  impact: null,
  urgency: null,
  priority: null,
  duration: null,
  targetDate: '2019-04-10T15:30:00.000Z',
  onHold: false,
  onHoldDate: null,
  onHoldDuration: 0,
  feedbackMessage: null,
  feedbackRating: null,
  processingStatus: 'Afgemeld',
  completed: true,
  completedDate: '2018-04-10T19:09:00.000Z',
  closed: true,
  closedDate: null,
  closureCode: null,
  creationDate: '2018-04-10T19:02:34.000Z',
  timeSpent: 0,
  timeSpentFirstLine: 0,
  timeSpentSecondLineAndPartials: 0,
  costs: 0,
  escalationStatus: null,
  escalationReason: null,
  escalationOperator: null,
  modifier: '226082ea-8d74-4dee-ae1e-74c33c883792',
  modificationDate: '2018-04-10T19:12:08.000Z',
  expectedTimeSpent: 0,
  majorCall: false,
  monitored: false,
  archivingReason: null }
  1. 此时我想我可以使用 UnderscoreJS 的 .isEqual (_.isEqual(identicalKeysDb, identicalKeysTd)) 检查这两个 identicalKeys 对象的相等性,但无济于事。除了我在数据库中直接存储了一些没有附加“ID”的键(这可以在数据库端修复)之外,更紧迫的问题是数据库将为externalNumber等值提供null,但是API 将提供''

在最近的一次尝试中,我尝试了 ES6、plain JS 和 UnderscoreJS 中的许多其他功能(太多了,而且代码早已被删除并且在我的“撤消”链上不再可用),但我找不到任何有效的方法,我真的不想硬编码一个巨大的if () 来检查每个属性与其对应的属性。我不介意需要一些节点包来简化这种比较,所以如果这是解决方案,请也分享一下。

那些实际上发生变化的对象我推送到一个名为existingIncidents 的数组中,该数组稍后会与任何新事件一起返回。这发生如下:

async filterIncidents() {
    const dbIncidents = await this.getDbIncidents(this.lastFetchTimestamp),
        fuseOpts = {
            'shouldSort': true,
            'findAllMatches': true,
            'threshold': 0,
            'location': 0,
            'distance': 100,
            'maxPatternLength': 36,
            'minMatchCharLength': 36,
            'keys': ['incidentID']
        },
        fuse = new Fuse(dbIncidents, fuseOpts),
        tdIncidents = await this.getTdIncidents(this.lastFetchTimestamp);

    const existIncidents = [],
        newIncidents = [];

    if (!dbIncidents.length) {
        for (const tdinci in tdIncidents) {
            newIncidents.push(tdIncidents[tdinci]);
        }
    } else {
        for (const tdinci in tdIncidents) {
            if (fuse.search(tdIncidents[tdinci].id).length) {
                // The value checking magic I need has to happen here. Some pseudo code:
                // if (values are different) {
                existIncidents.push(tdIncidents[tdinci]);
                // } else {
                // do nothing
                // }
            } else {
                newIncidents.push(tdIncidents[tdinci]);
            }
        }
    }

    return {
        'new': newIncidents,
        'existing': existIncidents
    };
}

编辑:在底部添加了整个功能


最终编辑:我在此处将一个 runkit 链接转储到 Nina Schulz 的最终解决方案实施,因为我必须根据我的确切用例对其进行调整,并且分享是关怀,也许它会在未来对其他人有所帮助。 固定链接:https://runkit.com/favna/so-compare-objects

【问题讨论】:

  • 可能有点矫枉过正,但我​​认为一个大的 if 语句将是处理边缘情况的最快和最灵活的方法。以后可能会对其进行重构以使其更易于维护。

标签: javascript arrays json node.js object


【解决方案1】:

这是一种方法,使用数组来存储不同样式的对象,并使用属性的相对路径来比较每个对象。

关键功能是一个单一的函数getValue,它接受一个对象和一个指向所需属性的键数组,并返回一个找到的值或链中最后一个找到的值。

另一个函数迭代给定的关系对象并(实际上)显示两个值以进行比较和后续操作,例如更新或其他想要的操作。

function getValue(object, keys) {
    return keys.reduce((o, k) => o && typeof o === 'object' ? o[k] : o, object);
}

function compaire(objects, relations) {
    relations.forEach(relation => {
        var values = relation.map((keys, i) => getValue(objects[i], keys));
        console.log(...values);
    });
}

var objectA = { foo: { bar: 42 }, a: { b: { c: 'baz' } }, callerLocation: null },
    objectB = { fooBar: 42, nested: { abc: 'bau' }, callerLocationID: null },
    objects = [objectA, objectB],
    relations = [
        [['foo', 'bar'], ['fooBar']],
        [['a', 'b', 'c'], ['nested', 'abc']],
        [['callerLocation', 'id'], ['callerLocationID']],
        [['x', 'u'], ['x', 'y']]
    ];
    
compaire(objects, relations);

【讨论】:

  • compaire 函数进行了一些重新格式化,以实现完整的比较功能,即将values 添加到1 个数组comparable,然后检查[0]th 和[1]st可比较的每个条目的值是相同的。所有这些我都给它一个objects,我知道它是相同的,一个我知道不是,它的工作原理是,据我所知,这是解决方案。然而,在我完成完整实现之前,我不会将其标记为这样(现在只检查几个属性),今天的工作日即将结束,所以明天就可以了。
  • 好吧,我继续实施它,但遇到了一个小障碍。必须将数据库中的 callerLocationID 字段与 API 中的 callerLocation.id 进行比较,但如果 API 有 null 对应于 callerLocation,它认为它不匹配,即使它在数据库中也是 null边。我已将 应该 的对象与我之前评论中修改的函数以及 runkit 上的关系放在一起,以便您可以看到它:runkit.com/favna/objectcompareso
  • 你想得到null 丢失的钥匙吗?
  • 是的。基本上,runkit 中的第二个日志也应该返回 true。
  • 请在问题中添加一些案例,如您所期望的,如果缺少某些键或该属性的路径不完整。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-06
相关资源
最近更新 更多