Rails批量插入多个表格

6
我有以下情况:我有一些CSV文件要导入Rails应用程序,数据集的大小可能超过100k行,这意味着使用大量内存 - 而我在服务器上没有这个内存。
每个CSV代表一个表转储。
现在,我的问题是我需要导入数据到几个表中,并通过外键维护关系。
到目前为止,我所做的大致如下:
- 创建ids缓存哈希 - 对于每个CSV /表,通过可能的属性find_or_initialize,或执行类似于model.where({complicated conditions}) || model.create({complicated conditions})的操作来保存创建的对象 - 填充ids缓存映射CSV id => DB idcomplicated conditions语句中,可以放置先前表中保存和缓存的某些ID。
请查看此处的代码以获取更多详细信息。
注意:我需要的更多是upsert,而不仅仅是普通的insert
我已经尝试过一些优化:
  • 使用事务 => 使用更少的内存并快速插入
  • 使用crewait gem => 比纯AR更快,但比事务慢
  • model.skip_callbacks(:create) => 速度提升或内存改进没有明显变化
  • 缓存了在所有其他表中广泛使用的user模型 => 高内存使用和较慢的速度(?)
  • 如果行已经存在,则只选择id属性以使用更少的内存 => 速度/内存没有大的区别
  • 优化缓存的哈希结构:使用Google Hashes结构将ids存储为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。

foreign_keys 不是已经在表中了吗?如果是的话,那么你只需要类似于 LOAD DATA INFILE 'path/to/file.csv' INTO TABLE your_table FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' 这样在 MySQL 中操作,然后为了节省内存,直接使用 File.open 读取文件并自己解析 csv,相比使用 ruby 的 CSV 库会节省很多内存。 - bjhaid
如果您事先不知道,那么您打算如何建立关系呢? - bjhaid
1
也许我表达得不太准确。我的意思是,CSV中的关系是通过在CSV中定义的外键来表示的:在一个CSV中,我有一个Ling的ID,在另一个CSV中,我通过“ling_id”链接到该Ling的一个Value。现在,当我导入所有CSV时,我希望保存该值,并保持与上面的Ling实例的关联,无论该特定实例是否已经存在于数据库中 - 这就是为什么我运行一个“find_or_initialize”查询来导入的原因。 - MarcoL
如果我使用MySQL提示符,那么我将使用auto_increment设置ID:这样一来,我不会失去所有关系吗? - MarcoL
我无法使用CSV的ID,因为它们来自不同的数据库,而且可能已经被分配给存储其他行 - 这些行可能属于不同的数据集。这就是为什么以前我没有选择这种方法。我会在问题中提供一些有关CSV格式的更多信息。 - MarcoL
显示剩余3条评论
2个回答

0

既然你要求建议,我会给出一个,但不保证其准确性。

我认为,在构建ID映射的同时一次性插入所有带有错误外键的记录可能会更快,而且肯定会占用更少的内存(就像你正在做的那样)。请注意,您可以使用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则是在保存时执行选择后跟随插入或更新,并且访问是按深度优先顺序跨越表进行的。


谢谢您的建议!我之前考虑过这个问题,但因为需要对代码库进行一些重构和重新排列,所以我在尝试其他选项之前先尝试了其他方法。如果没有其他解决方案,我肯定会尝试这个建议! - MarcoL

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%)-我不知道(哎呀...)可以接受多个参数
我敢打赌,在Rails 3.2中,您也可以使用类似find_or_initialize_by的东西,甚至更加优雅:
model.class.where(complicated_conditions).first_or_create(complicated_conditions)

最后一个还没有测试,但是一旦测试过了,我会尽力回来写出来。

注意:在使用此功能之前,请验证CSV数据,基本上所有检查都在这里禁用了,因此请在将文件传递给导入程序之前预处理文件!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接