Rails批量插入多个表中

时间:2014-04-13 12:22:54

标签: mysql sql ruby-on-rails ruby csv

我有以下情况:我在Rails应用程序中导入了一些CSV,数据集的大小可能超过10万行,这意味着使用了大量的内存 - 我没有在服务器上。

每个CSV代表一个表转储 现在,我的问题是我需要在几个表中导入数据,通过外键维护关系。

到目前为止,我所做的大致是:

  • 创建ids缓存哈希
  • 对于每个CSV /表find_or_initialize,尽可能使用属性,或执行类似model.where({complicated conditions}) || model.create({complicated conditions})保存已创建对象的内容
  • 填充ids缓存映射CSV id => DB id

complicated conditions语句中,可以保存以前表中保存的一些ID并进行缓存。

查看at the code here了解更多详情。

注意:我需要的更多是upsert,而不仅仅是普通insert

我已尝试过一些优化措施:

  • 二手交易=>使用的内存更少,插入更快
  • 已使用crewait gem =>比普通AR快,但比交易慢
  • model.skip_callbacks(:create) =>加速或记忆改善不明显
  • 缓存user模型,该模型在所有其他表中广泛使用=>高内存使用和慢速(?!)
  • 如果已存在行,请仅选择id属性以使用更少的内存=>速度/记忆没有太大差异
  • 缓存的优化哈希结构:使用Google Hashes结构将ID存储为INT-> INT =>内存使用量减少10%

我看过的其他东西却无法弄清楚如何使用:

  • 单个和长SQL查询:它基本上是crewait背后的想法,但就我的尝试而言它并没有很好地工作
  • activerecord-import:导入速度更快,但我会丢失所有关系或CSV到数据库ID映射
  • upsert:我已经看过了,但我想用它作为最后的手段(它有点棘手恕我直言)。

任何建议,关于如何改进的建议都是非常受欢迎的:谈论工具,图书馆,战略等等。

更新

这是我所拥有的CSV的简化示例:

lings.csv

------------------------
| id | name    | depth |
------------------------
| 0  | English |   0   |
------------------------
| 1  | French  |   0   |
------------------------
| etc..                |
------------------------

properties.csv

-----------------------------------
| id | name         | description |
-----------------------------------
| 0  | Subject_Verb | bla, bla... |
-----------------------------------
| 1  | Verb_Subject | bla, bla... |
-----------------------------------
| etc..                           |
-----------------------------------

lings_properties.csv

--------------------------------------
| id | value | ling_id | property_id |
--------------------------------------
| 0  | Yes   |    0    |     0       |
--------------------------------------
| 1  | No    |    1    |     1       |
--------------------------------------
| etc..                              |
--------------------------------------

查看上面的示例,当我导入Lings和Properties时,将为它们分配不同的ID,但我仍然希望将LingsProperties链接到英语和法语。 我无法在数据库中使用CSV ID - 它们由另一个应用程序分配,该应用程序具有与我导入它们的模式不同的模式。

更新2

我的Rails版本是3.0.20。 我正在使用Rails 3.2(或更高版本),我可以使用first_or_create(或类似的),但目前我还没有使用Rails 3.0。

2 个答案:

答案 0 :(得分:0)

由于您要求提供建议,我会给出一个,但没有明示或暗示的保证。

我认为它可能会更快,并且在同时构建ID映射的同时在一次传递中插入具有错误外键的所有记录肯定会减少内存密集度(正如您所做)。请注意,您可以使用带有list参数的create向服务器发送一批多条记录。这可能通过减少锁定开销而具有优势。

然后使用update_all调用将好(新)外键替换为坏(旧)外键。类似的东西:

PropertyOwnership.where(:ling_id => old_id).update_all('ling_id = ?', new_id) 

有了这个,你主要从处理循环中取出Active Record ORM,这应该有所帮助。唯一的内存开销应该是整数 - >整数id映射。

为了防止旧的ID与新的ID冲突,只需将从CSV读取的外键字段增加一个大于表中当前最大id的数字加上其大小。这应该使它超出插入期间创建的新id的范围。

这应该更快的原因是update_all调用将完全在单个表中的服务器端进行,而find_or_initialize正在进行选择,然后在保存时进行插入或更新,并且访问在表格中以深度优先顺序发生。

答案 1 :(得分:0)

最终,我设法保持相同的代码结构并找到适合我特定情况的解决方案。

不幸的是,使用Rails 3.0我没有那么多选择,所以我只想出以下模式:

model.class.skip_callback(:create)
model.class.transaction do
  CSV.foreach(file_path, :headers => true) do |row|
    // unfortunately this bit here has to be customised on the model
    // so append after the _by_ all the conditions you are looking for
    item = model.class.find_or_initialize_by_this_and_that(this, that, ...) do |m|
      m.more = row["more"]
    end


    item.save!(:validate => false)

    ids_cache[row["id"]] = item.id

  end
end
model.class.set_callback(:create)

上述解决方案的缺点:你必须为每个模型定制find_or_initialize_by方法,但是我已经设法删除了GoogleHash结构,并且探查器显示了一半的内存在此过程中使用。 即使在时间方面,它也相当不错,与之前的代码库相比,速度更快(~10%)。

在保存时禁用的验证对我的测试有很大帮助(约80%)与find_or_initialize_by方法(约20%)一起降低内存使用量 - 这是我不知道的( ops ...)could take multiple arguments

我打赌你可以在Rails 3.2中使用类似find_or_initialize_by的东西,更优雅:

model.class.where(complicated_conditions).first_or_create(complicated_conditions)

这最后一个还没有测试过,但是一旦我测试它就会尝试回来写它。

注意:在使用之前验证CSV数据,基本上所有检查都在此方面被禁用,因此在将文件传递给导入器之前对文件进行预处理!