Rails如何创建两个具有has_one依赖关系的模型

时间:2014-03-21 21:19:52

标签: ruby-on-rails ruby-on-rails-4

我有两个模型,帐户模型取决于用户模型的ID。我需要将它们保存在同一个事务中,如果其中一个验证失败,则事务应该失败。问题是即使帐户模型无效,用户模型也会被保存。我做错了什么?

为什么rails在保存后会打开另一个交易?

以下是代码:

# == Schema Information
#
# Table name: users
#
#  id              :integer          not null, primary key
#  username        :string(255)
#  password_digest :string(255)
#  active          :boolean
#  created_at      :datetime
#  updated_at      :datetime
#

class User < ActiveRecord::Base
  has_secure_password

  has_one :account, dependent: :destroy, autosave: true

  validates :username, presence: true, uniqueness: true
end



# == Schema Information
#
# Table name: accounts
#
#  id              :integer          not null, primary key
#  name            :string(255)
#  first_name      :string(255)
#  last_name       :string(255)
#  user_id         :integer
#  account_type_id :integer
#  created_at      :datetime
#  updated_at      :datetime
#

class Account < ActiveRecord::Base
  belongs_to :user

  validates_presence_of :first_name, :last_name, if: Proc.new { |record| record.is_customer? }
  validates_presence_of :name, if: Proc.new { |record| record.is_business? }

  validates_presence_of :user_id, :account_type_id

  def is_customer?
    self.account_type_id == 1
  end

  def is_business?
    self.account_type_id == 2
  end

  def define_business
    self.account_type_id = 2
  end
end




def create
  @user = User.new user_params
  @user.create_account business_params

  @user.account.define_business

  respond_to do |format|
    if @user.save && @account.save
      set_current_account @user

      format.html { redirect_to business_dashboard_path }
      format.json { head :created, location: business_dashboard_path }
    else
      format.html { render :new }
      format.json { render json: { errors: @account.errors }, status: :unprocessable_entity }
    end
  end
end

控制台输出:

SQL (0.4ms)  INSERT INTO "users" ("created_at", "password_digest", "updated_at", "username") VALUES (?, ?, ?, ?)  [["created_at", Fri, 21 Mar 2014 21:10:15 UTC +00:00], ["password_digest", "$2a$10$93pKlgadnoetCpBCdSSRtenLDbWnribprhLVX.gZ6i35WcsI6UuRi"], ["updated_at", Fri, 21 Mar 2014 21:10:15 UTC +00:00], ["username", "business12@email.com"]]
(7.2ms)  commit transaction
(0.0ms)  begin transaction
(0.0ms)  rollback transaction

2 个答案:

答案 0 :(得分:1)

默认情况下,Rails会在事务中包装模型操作。如果你执行:

@user.save && @account.save

你得到两个交易,每次调用一个方法save。如果第一个DB操作成功,则执行第二个DB操作。

使用此代码,可能会发生三件事:

  1. @user无效,因此@user.save失败并返回false,在这种情况下,数据库中不会创建任何记录。
  2. @user有效,因此@user.save成功并返回true,但@account无效,因此@account.save失败,在这种情况下1(用户)记录在DB中创建。
  3. @user有效,因此@user.save成功并返回true@account也有效,在这种情况下,两个记录都会在数据库中创建。
  4. 你想要消除#2。 您无法在案例中使用@user.save && @account.save它不会在一个事务中执行两个数据库操作。

    首先我要说的是,为了在一个事务中保存两个模型,您有两个选择:

    1。使用显式事务

    您可以在transaction中明确地同时执行这两项操作:

    User.transaction do
      @user.save!
      @account.save!
    end
    

    请注意,此处使用了爆炸方法(save!,而不是save),因为它们会在触发回滚事务的无效模型的情况下引发异常。

    这样,您最终会在数据库中输入0或2条记录。您可以在其中一个模型或控制器中将其实现为便捷方法。

    2。使用回调

    您也可以使用callbacks。这只能在模型中完成,如下所示:

    class User < ActiveRecord::Base
      has_secure_password
    
      has_one :account, dependent: :destroy, autosave: true
    
      after_create :create_account
    
      private
    
      def create_account
        # do something
      end
    end
    

    这会触发用户模型中的方法create_account(考虑将其设为私有),该方法在用户在数据库中成功创建后执行。

    鉴于您的用户模型可能没有创建帐户所需的所有相关信息,因此选项#1(即Account.transaction do似乎更适合您。

答案 1 :(得分:1)

您已使用autosave: true这是正确的,但遗憾的是您正在使用.create_account而不是build_accounthas_one Association Reference解释说:

  

build_association方法返回关联类型的新对象。   该对象将从传递的属性中实例化,   并设置通过其外键的链接,   但相关对象尚未保存。

由于您在用户关联中使用autosave: true,您可以构建(而不是创建)帐户,然后然后只保存用户,然后将关联的帐户保存为好。使用它的好处是您根本不需要担心交易。

prepared a gist来证明您的代码与我的代码之间的区别。您可以下载这两个文件并按如下方式运行它们:

ruby what_you_have.rb
ruby what_you_need.rb

您会注意到what_you_have.rb上的测试失败并传递what_you_need.rb

使用create_accountuser.save && account.save时,测试始终会因无效用户和有效帐户而失败。这是为什么?由于使用user.create_account,您在尝试保存用户之前已经在数据库中保存了有效帐户。

使用build_account然后只使用user.save时,您告诉ActiveRecord保存用户(假设它有效)并保存帐户。如果其中一个无效,则user.save不会保存任何内容。