【问题标题】:Rails: How to limit number of items in has_many association (from Parent)Rails:如何限制 has_many 关联中的项目数(来自父级)
【发布时间】:2011-12-30 11:36:08
【问题描述】:

我想限制关联中的项目数。我想确保用户拥有的东西不超过 X。这个问题was asked before 和解决方案在孩子中有逻辑:

提供的解决方案(针对类似问题):

class User < ActiveRecord::Base
  has_many :things, :dependent => :destroy
end

class Thing <ActiveRecord::Base
  belongs_to :user
  validate :thing_count_within_limit, :on => :create

  def thing_count_within_limit
    if self.user.things(:reload).count >= 5
      errors.add(:base, "Exceeded thing limit")
    end
  end
end

硬编码的“5”是个问题。我的限制根据父母而变化。事物的集合知道它相对于用户的限制。在我们的例子中,经理可以为每个用户调整(事物)的限制,因此用户必须限制其事物的集合。我们可以让 thing_count_within_limit 向其用户请求限制:

if self.user.things(:reload).count >= self.user.thing_limit

但是,这是来自 Thing 的大量用户自省。多次调用用户,尤其是 (:reload) 对我来说是危险信号。

对更合适解决方案的思考:

我认为has_many :things, :before_add =&gt; :limit_things 会起作用,但我们必须向stop the chain 提出一个例外。这迫使我更新 things_controller 来处理异常,而不是 if valid?if save 的 rails 约定。

class User
  has_many :things, :before_add => limit_things

  private
  def limit_things
    if things.size >= thing_limit
      fail "Limited to #{thing_limit} things")
    end
  end
end

这是 Rails。如果我必须如此努力,我可能做错了什么。

为此,我必须更新父模型、子控制器,并且我不能遵循约定?我错过了什么吗?我在滥用has_many, :before_add 吗?我使用 :before_add 查找了一个示例,但找不到任何示例。

我曾考虑将验证移至用户,但这只发生在用户保存/更新时。我没有看到使用它来停止添加事物的方法。

我更喜欢 Rails 3 的解决方案(如果这对这个问题很重要的话)。

【问题讨论】:

  • fyi 我在您的问题中将“事物”类名称更新为事物,因为如果可能,模型名称应始终为单数。
  • 我还会考虑将 errors.add(:base, "Exceeded thing limit") 从模型移动到控制器。你应该仍然有模型级别的方法,但我会用它来设置一个标志(比如说)或者只是从方法中返回真/假,然后在你的控制器中对其进行操作以确定是成功还是失败。我认为在模型中处理这种“状态”不适用于 MVC。让模型执行逻辑(确定有效性),然后让控制器处理控制做什么,去哪里等。
  • 顺便说一句,我将您的问题标题从“如何限制 has_many 关联中的项目数量(来自父级)”更改为“防止在 has_many 关联中添加超过 5 个项目”,因为它似乎更好反映内容
  • 医学博士:感谢语法更新。 更改标题: 从我不喜欢的解决方案中提取值 (5)(这似乎具有误导性),但我想它与我原来的相同。
  • 添加到控制器中的错误:嗯。首先,errors.add 来自我不喜欢的解决方案。但是,我认为使用错误是适当的。验证发生在模型中,错误通常是由验证添加的。我认为您是在建议控制器应该分配 @user.errors.add(:features, "some message") 如果 save 为假?在验证期间添加错误正是我们如何让控制器确定要做什么和去哪里(在 Rails 中)。如果您希望孩子限制自己,我相信他们的回答是有效的。我没有。

标签: ruby-on-rails activerecord callback associations


【解决方案1】:

因此,如果您想为每个用户设置不同的限制,您可以将 things_limit:integer 添加到 User 中并执行

class User
  has_many :things
  validates_each :things do |user, attr, value|
   user.errors.add attr, "too much things for user" if user.things.size > user.things_limit
  end
end

class Thing
  belongs_to :user
  validates_associated :user, :message => "You have already too much things."
end

使用此代码,您不能将 user.things_limit 更新为低于他已经获得的所有东西的数字,当然它会限制用户通过他的 user.things_limit 创建东西。

Rails 4 应用示例:

https://github.com/senayar/user_things_limit

【讨论】:

  • 某种形式的解释可能对 OP 来说很好。
  • 试过这个。我还可以user.things &lt;&lt; existing_thing
【解决方案2】:

在保存完成后,对当前计数进行验证会导致计数大于限制。我找到防止创建发生的唯一方法是在创建之前验证事物的数量是否小于限制。

这并不是说验证 User 模型中的计数没有用,但这样做不会阻止调用 User.things.create,因为用户的计数集合是有效的 em> 直到新的Thing对象被保存,保存后失效。

class User
  has_many :things
end

class Thing
  belongs_to :user
  validate :on => :create do
    if user && user.things.length >= thing_limit
      errors.add(:user, :too_many_things)
    end
  end
end

【讨论】:

    【解决方案3】:

    试试这个,就像字符串一样:

    class User < ActiveRecord::Base
      has_many :things, :dependent => :destroy
      validates :things, length: {maximum: 4}
    end
    

    【讨论】:

      【解决方案4】:

      我想我会在这里插话。似乎这里的大多数答案在竞争条件下都失败了。我正在尝试限制可以在我们的应用程序中以特定价格点注册的用户数量。检查 Rails 中的限制意味着 10 个同时注册可以通过,即使它超过了我试图设置的限制。

      例如,假设我想限制注册不超过 10 个。假设我已经有 5 个用户注册。假设同时有 6 个新用户尝试注册。在 6 个不同的线程中,Rails 读取剩余的插槽数,并得到答案5。这通过了验证。然后 Rails 允许所有注册通过,我有 11 个注册。 :/

      我是这样解决这个问题的:

      def reserve_slot(price_point)
        num_updated = PricePoint.where(id: price_point.id)
          .where('num_remaining <= max_enrollments')
          .update_all('num_remaining = num_remaining + 1')
      
        if num_updated == 0
          raise ActiveRecord::Rollback
        end
      end
      

      使用这种方法,我永远不会允许比max_enrollments 更多的注册,即使在应用程序处于负载状态时也是如此。这是因为验证和增量是在单个原子数据库操作中完成的。另请注意,我总是在事务中调用此方法,因此它会在失败时回滚。

      【讨论】:

        【解决方案5】:

        Rails 4 中,也许早期版本您可以简单地验证counter_cache 的值。

        class User
          has_many :things
          validates :things_count, numericality: { less_than: 5 }
        end
        
        class Thing
          belongs_to :user, counter_cache: true
          validates_associated :user
        end
        

        请注意,我使用了:less_than,因为:less_than_or_equal_to 将允许things_count 成为6,因为它在计数器缓存更新后得到验证。

        如果您想为每个用户设置限制,您可以创建一个things_limit 列来动态地与您设置的限制值进行比较。

        validates :things_count, numericality: { less_than: :things_limit }
        

        【讨论】:

        • 这似乎使用户处于无效状态。我认为您不能更改用户电子邮件,因为该实例将无法通过 things_count 验证。
        • 拥有things_count 默认为0 而不是nil
        【解决方案6】:

        您如何使用 Accept_nested_attributes_for 进行调查?

        accepts_nested_attributes_for :things, :limit => 5

        http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

        也就是说,我认为 accept_nested_attributes_for 似乎只适用于某些类型的情况。例如,如果您正在创建一个命令行 API,我认为这是一个非常糟糕的解决方案。但是,如果您有一个嵌套表单,它就可以很好地工作(大部分时间)。

        【讨论】:

        • 有趣。我不知道 :limit 选项。这不是我想要的,但它可能会有所帮助。我可以使用嵌套属性,但它很容易被绕过(例如 user.things.create(...))。我希望查看 :limit 的来源可以给我一个想法。
        【解决方案7】:

        你可以试试validates_length_ofvalidates_associated

        class Client < ActiveRecord::Base
        
          has_many :orders
          validates :orders, :length => { :maximum => 3 }
        
        end
        
        class Order < ActiveRecord::Base
        
          belongs_to :client
          validates_associated :client
        
        end
        

        A quick test 表明valid? 方法按预期工作,但它不会阻止您添加新对象。

        【讨论】:

        • 我尝试了一些不同的验证,正如你提到的,它们都没有阻止孩子得救。它们阻止父对象被保存,但是,由于关联已经持久化到数据库中,这有什么好处?
        【解决方案8】:

        你应该试试这个。

        class Thing <ActiveRecord::Base
          belongs_to :user
          validate :thing_count, :on => :create
        
          def thing_count
              user = User.find(id)
              errors.add(:base, "Exceeded thing limit") if user.things.count >= 5
          end
        end
        

        【讨论】:

        • @muistooshort 编辑了答案,感谢评论
        • 在上面的“建议的解决方案”中讨论了这个选项,包括这个解决方案让我暂停的原因。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-04-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多