2015-09-17 115 views
4

在我的Ruby on Rails应用程序中,创建模型Deal时,我使用after_create在DealPrize表上创建奖品。rspec - PG :: UnableToSend:服务器意外关闭了连接这可能意味着服务器异常终止

Deal和DealPrize有一个属于/ has_many的关系:一个交易有很多交易奖品和一个Dealprize属于交易。

它的工作原理是这样的:在我的管理面板(使用activeadmin),在一个交易中,我有一个'奖金号码'专栏,我使用after_create,这样每次管理员创建新交易时,这个奖品号列,并在DealPrize表内创建这个奖品(根据需要插入尽可能多的行)。

我使用rspec和FactoryGirl的测试失败。这可能是由于rspec/factory女孩在prepare_statements中表现不佳。我不确定。

这里是我的代码

型号/ deal.rb

has_many :deal_prizes, dependent: :delete_all 

after_create :create_dealprizes 

# Constants 
TIME_SET = Time.zone.now 
CONNECTION = ActiveRecord::Base.connection.raw_connection 

def create_dealprizes 
    begin 
    CONNECTION.describe_prepared('create_deal_prizes') 
    rescue PG::InvalidSqlStatementName 
    CONNECTION.prepare('create_deal_prizes', 'INSERT INTO deal_prizes (deal_id,created_at,updated_at,admin_user_id,prize_id) values ($1, $2, $3, $4, $5)') 
    end 

    Deal.transaction do 
    self.prizes_number.times do |i| 
     CONNECTION.exec_prepared('create_deal_prizes', [ 
     { value: self.id}, 
     { value: TIME_SET }, 
     { value: TIME_SET }, 
     { value: self.admin_user_id }, 
     { value: 5 } 
     ]) 
    end 
    end 
end 

这里是我的测试:我想,以确保在创建交易时,如果交易的prizes_number是340,然后应该是Dealprizes表上添加了340行。

require 'spec_helper' 

describe DealPrize do 
    describe "the right number of rows are created inside DealPrize table when a Deal is created" do 

    before do 
     @initial_prize_count = DealPrize.count 
     @deal_prize = FactoryGirl.create(:deal_prize) 
     @deal  = FactoryGirl.create(:deal_prizes => [@deal_prize], :prizes_number => 277) 
    end 

    it "does create right nb of rows" do 
     expect(DealPrize.count).to eq(@initial_prize_count + 277) 
    end 
    end 
end 

我用优惠厂:

FactoryGirl.define do 
    factory :deal do 
    country  "France" 
    title   "Neque porro quisquam est qui dolorem" 
    description "lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum" 

    factory :deal_skips_validate do 
     to_create {|instance| instance.save(validate: false) } 
    end  

    end 
end 

,这里是工厂的DealPrizes:

FactoryGirl.define do 
    factory :deal_prize do 
    end 
end 

这里是我得到的错误:

PG::UnableToSend: 
     server closed the connection unexpectedly 
     This probably means the server terminated abnormally 
     before or while processing the request. 

如果需要,这里是我如何处理tran saction在我spec_helper.rb

config.use_transactional_fixtures = false 

config.before(:suite) do 
    DatabaseCleaner.clean_with(:truncation, :except => %w(roles)) 
end 

config.before(:each) do 
    DatabaseCleaner.strategy = :transaction 
end 

config.before(:each, js: true) do 
    DatabaseCleaner.strategy = :truncation 
end 

config.before(:each) do 
    DatabaseCleaner.start 
end 

config.after(:each) do 
    DatabaseCleaner.clean 
end 
+1

您的FactoryGirl用法看起来不正确。你使用什么FG版本? –

+0

编辑了工厂女孩的代码,因为有些部分是不正确的。 – Mathieu

+1

使用最新版本的'宝石'factory_girl_rails'宝石。在我的'宝石列表'我可以阅读:factory_girl(4.5.0,4.0.0) factory_girl_rails(4.5.0,4.4.1,4.0.0) – Mathieu

回答

2

每次运行方法时都需要从池中获取一个新连接(并确保在完成后将其返回池)。看看with_connection

这样的事情应该工作。

class Deal < ActiveRecord::Base 
    has_many :deal_prizes, dependent: :delete_all 

    after_create :create_dealprizes 

    # Constants 
    TIME_SET = Time.zone.now 

    def create_dealprizes 
    self.class.connection_pool.with_connection do |connection| 
     begin 
     connection.describe_prepared('create_deal_prizes') 
     rescue PG::InvalidSqlStatementName 
     connection.prepare('create_deal_prizes', 'INSERT INTO deal_prizes (deal_id,created_at,updated_at,admin_user_id,prize_id) values ($1, $2, $3, $4, $5)') 
     end 

     connection.execute("START TRANSACTION") 
     prizes_number.times do |i| 
     connection.exec_prepared('create_deal_prizes', [ 
      { value: self.id}, 
      { value: TIME_SET }, 
      { value: TIME_SET }, 
      { value: self.admin_user_id }, 
      { value: 5 } 
     ]) 
     end 
     connection.execute("COMMIT") 
    end 
    end 
end 

这应该有效,但在我看来(这是值得的),这不是Ruby-ish。这听起来像你关心性能,所以我会用一个INSERT语句添加数据。你可以手动建立,但是activerecord-import宝石非常容易。

使用宝石可能看起来像:

class Deal < ActiveRecord::Base 
    has_many :deal_prizes, dependent: :delete_all 

    after_create :create_dealprizes 

    def create_dealprizes 
    prizes = prizes_number.times.map do 
       deal_prizes.build(created_by: admin_user, prize_id: 5) 
      end 
    DealPrize.import(prizes) 
    end 
end 

这是一个使用ActiveRecord的导入sample app

+0

嗨詹姆斯会尝试。如果你有帮助,你的意见很重要:)所以我会尝试你的第二个解决方案activerecord-import。我已经看到了这个宝石,但它对我来说并不明显:我认为这是导入csv数据和类似的东西,而不是将数据从一个表传输到另一个表。我想我错了。是的性能是非常重要的,因为prizes_number可以等于400,000 :) – Mathieu

+0

这是行不通的。它不会创造任何deal_prizes。在你的代码中有“时代”,这是没有必要的,我删除了dit,但仍然没有发生。我没有收到任何错误,只是在创建交易时,我会看到DealPrizes表,并且没有添加任何行。我删除了模型中的所有验证,以确保它不是原因。我读过这个:https://github.com/zdennis/activerecord-import/wiki/Callbacks =>所以当我使用回调时,我可能无法在回调中使用activerecord-import? – Mathieu

+0

对这个错误感到抱歉。不要编码累,呃?检查你的日志。如果导入正在运行,您应该看到'Class Create Many Without Validations或Callbacks'。您还可以将一些日志语句添加到回调中,以确保该方法正在运行。 Callbacks doc是指'DealPrize'上的回调函数,所以在这种情况下它不应该是相关的。 –

1

你只检索ActiveRecord::Base.connection.raw_connection一次CONNECTION,作为类被加载。因此,框架可以自由关闭该连接并创建一个新连接。你应该在实际需要的时候检索它,然后我想它可能会工作。

更新:afaik PostgreSQL适配器也会创建预准备语句,并且数据库应该能够重新使用它们,因此手动执行这些操作所带来的性能提升不应该那么大(特别是因为您“只有“谈论几百个对象)。当您可以创建DealPrize对象时,由于非惯用的Rails代码,您将失去与其他适配器的兼容性以及可维护性。

+0

对不起,但在这里用postgresql新手,你会如何编码? – Mathieu

+0

只需将'CONNECTION'替换为'self.class.connection.raw_connection',就可以做到。 –

+0

不,我使用的交易,因为我创建约500000每次我创建与我的管理面板德拉奖 – Mathieu

1

我可以提出以下建议吗?

def create_deal_prizes 
    batch_size = 1000 
    deal_prizes = [] 
    time = Time.zone.now 
    prize_id = 5 # wasn't sure where you got this id from 

    Deal.transaction do 
    deal_prizes = [] 
    prizes_number.times do 
     deal_prizes << "(#{self.id}, #{self.admin_user_id}, #{prize_id}, '#{time}', '#{time}')" 

     if deal_prizes.size >= batch_size 
     sql = "INSERT INTO deal_prizes (deal_id, admin_user_id, prize_id, created_at, updated_at) VALUES #{deal_prizes.join(", ")}" 
     ActiveRecord::Base.connection.execute sql 
     deal_prizes = [] 
     end 
    end 
    if deal_prizes.any? 
     sql = "INSERT INTO deal_prizes (deal_id, admin_user_id, prize_id, created_at, updated_at) VALUES #{deal_prizes.join(", ")}" 
     ActiveRecord::Base.connection.execute sql 
    end 
    end 
end 

这是更多的rails-esque,高效的,当您切换到[INSERT DB HERE]时不需要重写。随意玩batch_size以及。

另一个说明,如果你把时间设定为一个常数,它几乎是肯定的,它不会改变,是你的意图?相反,我把它移到了方法中,所以它会锁定它被调用的时间,而不是类初始化(引导)的时间。

+0

感谢您的回答。我根据这个基准进行了交易+原始sql方式(https://www.coffeepowered.net/2009/01/23/mass-inserting-data-in-rails-without-killing-your-performance/很老的文章虽然)。因为每个交易创造我需要添加像400K到500K交易奖品。但它是真正的方式更铁路系统,但你不改变你的分贝类型obtenir?我会尝试你的建议并比较速度。是的,你最好把时间放在方法里面! – Mathieu

+0

意思是说你不经常改变的一件事就是你的db类型。 – Mathieu

+0

这是很好,你是基准。这是真的,你多久换一次你的分贝。我想从扩展的角度来看,批量插入会更好,特别是如果它与您网站上发生的其他数据库操作竞争。所以你应该考虑这一点,甚至超过速度,我想他们会很快结束呢? – omarvelous

相关问题