0

I want to iterate through an array of hash with one key containing an array.

I explain :

  • I have a stores table
  • I have a products table
  • I have a store_exluded_product table with a store_id column and a product_id column

I have this array of hashes:

array = [
  { :store_id => 5, :products_ids => [1, 4, 19, 40] },
  { :store_id => 13, :products_ids => [2, 20, 35] },
  # ...
]

I want to create new ActiveRecord for my StoreExludedProduct table like this with StoreExcludedProduct.create() :

id store_id product_id
1 5 1
2 5 4
3 5 19
4 5 40
5 13 2
6 13 20
7 13 35

If you have a good solution to my problem which does not make too many DB requests, I would be more than happy

Thank you

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
fralps
  • 75
  • 8
  • 1
    What exactly is causing you problems with solving this? You just loop over the array with `each` and in that loop another `each` for the product_ids. – Eyeslandic Jan 29 '21 at 08:40
  • It would help to see your code. As a rule of thumb: always show what you've tried, even if it not working. (you'll also get less downvotes that way) – Stefan Jan 29 '21 at 09:03

2 Answers2

2

A simple approach is to just iterate both, array and product ids:

array.each do |hash|
  hash[:product_ids].each do |product_id|
    StoreExcludedProduct.create(
      store_id: hash[:store_id],
      product_id: product_id
    )
  end
end

You can wrap the above code in a transaction call to avoid having separate transactions for each create, i.e.:

StoreExcludedProduct.transaction do
  array.each do |hash|
    # ...
  end
end

This also makes your code run in an all-or-nothing way.

Another option is insert_all which can insert multiple records at once. In order to use it, you have to first build an array of attributes for each record, i.e. you have to split the product ids. This works a lot like the above code:

attributes = array.flat_map do |hash|
  hash[:products_ids].map do |product_id|
    { store_id: hash[:store_id], product_id: product_id }
  end
end
#=> [
#   {:store_id=>5, :product_id=>1},
#   {:store_id=>5, :product_id=>4},
#   {:store_id=>5, :product_id=>19},
#   {:store_id=>5, :product_id=>40},
#   {:store_id=>13, :product_id=>2},
#   #...
# ]

which can be passed to insert_all:

StoreExcludedProduct.insert_all(attributes)

Note that this performs a raw SQL query without instantiating any models or running any callbacks or validations.

Stefan
  • 109,145
  • 14
  • 143
  • 218
  • Alright I see my mistake now .. So which way is better in terms of performance with an array of 10 000 products ? – fralps Jan 29 '21 at 08:54
  • @fralps a bulk insert via `insert_all` should be much faster. Just give both a try (maybe with 100 or 1,000 entries) and you'll see. – Stefan Jan 29 '21 at 08:59
0

You can also use the import method to insert all records in the database with a single transaction.

to_be_import = []
array.each do |h|
  h[:products_ids].each do |product_id|
    to_be_import << StoreExcludedProduct.new(h[:store_id],product_id)
  end
end

StoreExcludedProduct.import(
  to_be_import,
  validate: false,
  on_duplicate_key_update: {
    conflict_target: [:id],
    columns: [:store_id, :product_id]
  }
)

references: activerecord-import

Vishal Jain
  • 446
  • 3
  • 7