ActiveRecord::Base.transaction 应该在哪里?

where should ActiveRecord::Base.transaction be?

我有三个模型:列表、食物和数量。 List 和 Food 通过 Quantity via has_many :through 关联。所以每个 Quantity 都有三个参数:food_id、list_id 和 amount(一个整数)。

我的目标是在每次创建列表时创建一个新的数量(与该列表关联)。我希望使用事务来执行此操作,以便必须成功创建所有对象,否则 none 将是。

我的主要问题是:我应该在我的代码中的什么地方写这个交易?我认为它应该在 List 模型中,但我不确定;如果它应该在列表模型中,我不知道它会在 列表模型中的什么位置。我认为它不应该在 List 控制器中,我发现 advice on a comment on Mark Daggett's blog 它可以在一个独立的数据访问对象中,但我不确定该怎么做。

其次:交易本身。很难说我的错误是在交易中还是在交易中。

万一它是相关的,我在 之后提出了这个问题,但我认为这应该是一个新问题,因为我没有找到专门针对交易的类似问题。

我的列表模型,交易当前所在的位置:

class List < ActiveRecord::Base
  has_many :quantities
  has_many :foods, :through => :quantities

  before_save { self.name = name.downcase }

  validates :days, presence: true, :numericality => { :greater_than => 0 }
  validates :name, length: { maximum: 140 }, uniqueness: { case_sensitive: false }
end

ActiveRecord::Base.transaction do
      @list = List.create
      @a = Food.all.sample(1) 
      Quantity.create(food_id: @a, list_id: @list.id, amount: rand(6))
end

没有错误,但是没有创建新的 Quantity,这让我觉得除了至少有一点错误之外,我做对了一些事情。

我也试过了

List.transaction do

而不是

ActiveRecord::Base.transaction do

得到了相同的结果。

我将不胜感激关于这个问题的任何指导或提示,我认为这些指导或提示源于对一个非常基本的观点的误解(如此基本以至于我在 the docs 中找不到任何关于它的信息)。谢谢。

Rails 4.2.3,Cloud9。开发数据库 = SQLite3,生产数据库 = postgres heroku。

您遇到了错误,只是您没有看到它们。如果您想看到他们,请致电 Quantity.create!而不是 Quantity.create。 (或者您可以分配给临时对象并查看它们,例如:q = Quantity.create(...); q.errors

这些错误是因为您在 class 列表(顺便说一句,名称不正确)中进行了 2 次验证,检查了日期和名称。

正如 ilan berci 上面所说,可能存在您没有发现的错误。使用事务时,最好使用 bang 版本的方法保存记录,这样当任何验证未通过时您将得到一个异常,然后事务将被回滚。

关于您的第一个问题,有很多关于在 Rails 项目中放置复杂性的讨论。几年前,座右铭是 "skinny controllers, fat models",这意味着应该提取模型的复杂性,使更难测试且应用程序流程必须显而易见的控制器更具可读性。我更喜欢 "skinny everything" 的哲学:没有绝对的规则,你可以把所有东西放在任何地方,但是一旦它开始变得有点复杂,就把它提取到 class 中,它只负责一个或很少的事情.保持小、简单、经过测试,并在需要时组合它们。

对于您的情况,您可以创建一个服务或用例,然后简单地在控制器中使用它:

class CreateList
  def create!
    ActiveRecord::Base.transaction do
      @list = List.create!
      @food = Food.all.sample(1) 
      Quantity.create!(food: @food, list: @list, amount: rand(6))
    end
  end
end

如您所见,测试这个class非常简单!大多数 "right ways" 只有通过经验才能显示出它们的真正价值,而且从来没有唯一的、正确的做事方法,因此请不断尝试不同的技术,直到您掌握自己的方法来解决问题![​​=11=]

我终于让我的交易开始工作并进入服务!感谢 ilan 和 mrodrigues 的建议。我的 create new Quantity 逻辑不在控制器和模型中,我正在处理 "skinny everything" 代码,我有刘海需要保存。

作为参考,this post on John Nunemaker's Railstips 帮助我以列表控制器可以识别的方式定义了 WriteList 方法(通过使用 self.)。

这是我编写的用于在创建列表时处理数量创建的服务:

class WriteList
  def self.write!(a)
     ActiveRecord::Base.transaction do
      a.save!
      @a = Food.all.sample.id
      Quantity.create!(food_id: @a, list_id: a.id, amount: rand(6))
    end
  end
end

这是我的控制器的创建部分:

def create
  list = List.new(list_params) 
  if WriteList.write!(list)
    flash[:success] = "A list has been created!"
    redirect_to list
  else
    render 'new'
  end
end

如果您觉得有任何不妥,我将不胜感激。

我已经根据 mrodrigues 的反馈更新了我的代码:

  • 为清楚起见,编辑变量和参数的名称。
  • 添加救援以便 write 方法在失败时 return 为 false。
  • 重新组织代码,以便 write 方法(在服务中)可以在 create 方法(在控制器中)中实例化一个对象。
  • 将实际保存新列表的代码重新定位到 write 方法。

为了让这一切都起作用,我在 WriteList class 中添加了一个 def initialize 方法,你们可能都认为它在那里,但实际上并没有。我希望这是明智的(而不是古怪的)做法。

编辑

我在走上正轨后做了这些改变:

  • 将 return 列表添加到 write 方法的末尾。
  • 而不是拯救到假,使用不保存的参数创建一个新列表;这会导致有关正确读取验证问题的错误消息。

结束编辑

它现在可以正常工作以达到我的目的;对于长期 organization/structure/building 的任何进一步反馈,我表示感谢。

app/services/write_list.rb:

class WriteList

  def initialize(params)
      @params=params
  end

  def write
      ActiveRecord::Base.transaction do
          food = Food.all.sample.id
          list = List.new(days: @params[:days], name: @params[:name])
          list.save!
          Quantity.create!(food_id: food, list_id: list[:id], amount: 1+rand(6))
          return list
      end
    rescue
      return List.new(days: @params[:days], name: @params[:name])
  end
end

app\controllers\lists_controller的相关部分:

def create
  @list = WriteList.new(list_params).write
  if @list.save
    flash[:success] = "A list has been created!"
    redirect_to @list
  else
    render 'new'
  end
end

顺便说一句:我希望我发布比我以前 answers/question 有所改进的答案的做法是合适的;有人请告诉我是否不是。我发现它对跟踪我的进度和收集更多反馈很有帮助,我希望其他人(尤其是像我这样的初学者)可以从更新中有所收获。