How to join two object arrays by different keys?

Asked

Viewed 686 times

4

I have two object arrays:

let mergedScreensAllCompanies = [
    {
        id: 1,
        description: Cadastro de usuários,
    },
    {
        id: 2,
        description: Cadastro de filiais,
    }
]

let userScreens = [
    {
        id: 1,
        user_id: 1,
        screen_id: 1,
        allow_read: true,
        allow_create: true,
        allow_update: false,
        allow_delete: true,
    }
]

I need to join these two arrays in just one, combining the properties where mergedScreensAllCompanies.id is equal to userScreens.screen_id and then the following result:

merged = [{
        id: 1,
        description: Cadastro de usuários,
        user_id: 1,
        screen_id: 1,
        allow_read: true,
        allow_create: true,
        allow_update: false,
        allow_delete: true
    },
    {
        id: 2,
        description: Cadastro de filiais
    }
]

I tried with the lodash:

let merged = _.merge(_.keyBy(userScreens , 'screen_id'), _.keyBy(mergedScreensAllCompanies , 'id'))

But that way you didn’t put my property together the way I’d hoped:

{
    "1": {
        "id": 1,
        "description": "Cadastro de usuários",
    },
    "2": {
        "id": 2,
        "description": "Cadastro de filiais",
    },
    "undefined": false
}
  • 1

    The solution needs to be given with lodash or can only use native methods?

  • @Augustovasques can be with lodash or without, is that with lodash I believe the code gets smaller, so I tried with it

2 answers

8


You don’t even necessarily use Lodash:

const mergedScreensAllCompanies = [
  { id: 1, description: 'Cadastro de usuários' },
  { id: 2, description: 'Cadastro de filiais' }
];

const userScreens = [
  { id: 1, user_id: 1, screen_id: 1, foo: 'a' },
  { id: 2, user_id: 2, screen_id: 2, foo: 'b' }
];

const merged = userScreens.map((screen) => ({
  ...mergedScreensAllCompanies.find((o) => o.id === screen.screen_id),
  ...screen
}));

console.log(merged);

But if you want to use Lodash, you can do it like this:

const mergedScreensAllCompanies = [
  { id: 1, description: 'Cadastro de usuários' },
  { id: 2, description: 'Cadastro de filiais' }
];

const userScreens = [
  { id: 1, user_id: 1, screen_id: 1, foo: 'a' },
  { id: 2, user_id: 2, screen_id: 2, foo: 'b' }
];

const merged = _.map(userScreens, (screen) =>
  _.merge(
    _.find(mergedScreensAllCompanies, (o) => o.id === screen.screen_id),
    screen
  )
);

console.log(merged);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

I personally think that Lodash ends up making it difficult, as it is one more library that the developer has to learn. Of course there may be solutions that use this library that are more "lean" than the one I did, but it’s an example.

I think it’s better to use native JS Apis than to search for libraries that are no longer as necessary as in the past.

  • 2

    "I personally think Lodash makes it difficult". I also have the same opinion, so much so that I asked AP about whether the use of native objects would be within the scope of the question.

  • 1

    @Luiz Felipe I will use the solution without Lodash. I had to invert the order and use it as follows: const merged = mergedScreensAllCompanies.map(screen => ({ ...screen, ...userScreens.find(o => o.screen_id == screen.id) })) When I put it the way you posted it merged it just possesses the common element between the two and the others does not remain

  • Exactly, @Augustovasques! :)

2

Complementing the another answer, use map/find is a good solution, but depending on the size of the arrays, it may not climb as well.

That’s because, according to the language specification, the method find always starts the search at the beginning of the array. So if the arrays start to get too big, it starts to become inefficient, since for each element of an array, you need to search the other, always starting from the beginning.

Of course, for small arrays and/or being processed a few times, the difference is irrelevant and imperceptible (after all, for few data, everything is fast). But anyway, one way to avoid these searches being done at all times is to create a temporary object that holds references to the original objects, using as key the common information (in this case, the id).

With this, the "search" for the object becomes only one lookup simple on this object. Then, discard the keys and take only the values of it (which in this case, will be in an array, as desired). So:

const mergedScreensAllCompanies = [
  { id: 1, description: 'Cadastro de usuários' },
  { id: 2, description: 'Cadastro de filiais' }
];

const userScreens = [
  { id: 1, user_id: 1, screen_id: 1, foo: 'a' },
  { id: 2, user_id: 2, screen_id: 2, foo: 'b' }
];

// objeto temporário, mapeia cada id para o respectivo objeto
let merged = {};
for (let i = 0; i < mergedScreensAllCompanies.length; i++) {
    merged[mergedScreensAllCompanies[i].id] = mergedScreensAllCompanies[i];
}
// atualiza os objetos com os dados do outro array
for (let i = 0; i < userScreens.length; i++) {
    if (merged[userScreens[i].screen_id]) {
        merged[userScreens[i].screen_id] = { ...merged[userScreens[i].screen_id], ...userScreens[i] };
    }
}
// descarta as chaves, obtém o array com os objetos atualizados
merged = Object.values(merged);

console.log(merged);

Although it seems to "do more things" (after all, it has two for), actually does less things. The map traverse the array once, and for each element it does a search with find, running through the other array. It’s okay that most of the time it doesn’t go through everything, since it stops as soon as it finds the element, but still, we will have many elements being traveled several times, which in the end ends up generating a lot more iterations (and as we will see below, this can make all the difference).

Already the above code only goes through each of the arrays once (and there is a loop at the end, within Object.values). Although it seems that the code does more things, it’s actually doing less. The difference is that with map and find, these extra things are "hidden" within these functions.

But again reinforcement that, for a few small arrays, the difference will be tiny and probably you won’t even notice. In this case, it becomes more a matter of taste and "style": many prefer to program in a more "functional" way and think using map (and his "brothers" filter, reduce, etc) the code becomes more expressive, etc. I just wanted to leave an alternative, in case the arrays start to increase a lot and this starts to be a problem (and it is always good to know other ways of doing, to "open the head" and not get stuck in a single way to solve the problems).


For the record, other alternatives are:

  1. eliminate the use of spread to update the object:

    let merged = {};
    for (let i = 0; i < mergedScreensAllCompanies.length; i++) {
        merged[mergedScreensAllCompanies[i].id] = mergedScreensAllCompanies[i];
    }
    for (let i = 0; i < userScreens.length; i++) {
        if (merged[userScreens[i].screen_id]) {
            merged[userScreens[i].screen_id].user_id = userScreens[i].user_id;
            merged[userScreens[i].screen_id].screen_id = userScreens[i].screen_id;
            merged[userScreens[i].screen_id].foo = userScreens[i].foo;
        }
    }
    merged = Object.values(merged);
    
  2. Use for...of:

    let merged = {};
    for (const screen of mergedScreensAllCompanies) {
        merged[screen.id] = screen;
    }
    
    for (const userScreen of userScreens) {
        if (merged[userScreen.screen_id]) {
            merged[userScreen.screen_id] = { ...merged[userScreen.screen_id], ...userScreen };
        }
    }
    merged = Object.values(merged);
    

The first one is even faster (because it doesn’t cost you to create another object, which is what happens when you use the spread), but on the other hand, the code becomes more verbose (which may or may not be a problem, it goes to each one - it can be a problem if there are many properties to copy, for example). The second, although a little slower (but still faster than map/find), is a little more succinct - and in my opinion, more expressive - than the for "traditional" (but again, this is very subjective).

Finally, here’s the test I did, and the fastest option was for without spread (item 1 above), followed by for normal (the first option I put), then for..of, and finally map/find and Lodash (this was by far the slowest).

I also tested on Node with Benchmark.js and the result was the same:

sem spread x 2,412 ops/sec ±1.51% (87 runs sampled)
for normal x 671 ops/sec ±4.05% (75 runs sampled)
for  of    x 651 ops/sec ±2.78% (82 runs sampled)
map/find   x 15.15 ops/sec ±3.30% (41 runs sampled)
lodash     x 11.88 ops/sec ±5.90% (33 runs sampled)
Fastest is sem spread

What counts are ops/sec (transactions per second): the more, the better

Reinforcing once again that if you don’t have a performance problem (perhaps because they are small arrays and/or few runs), it won’t make much of a difference (depending on the case, for small arrays, the performance of map can be equal or even better). Even because this is just a "random" test, what really counts is the test with real data, to know if this is really a problem...

Browser other questions tagged

You are not signed in. Login or sign up in order to post.