【问题标题】:Rails scoped validation with multiple booleans使用多个布尔值进行 Rails 范围验证
【发布时间】:2016-05-25 08:22:39
【问题描述】:

总结:

给定其他四列作为范围,我需要验证两列的唯一性:

validates :preference, uniqueness: { scope: [:voter_id, :term_id, :in_transaction, :transaction_destroy], message: 'must not have the same preference as another vote' }
validates :candidate,  uniqueness: { scope: [:voter_id, :term_id, :in_transaction, :transaction_destroy], message: 'can only be voted for once' }

这是为了确保同一选民、同一任期和同一交易状态中只有唯一的偏好和候选人。

问题是,in_transactiontransaction_destroy 是布尔值,这意味着 Rails 验证不起作用。

如何编写解决方法?


背景:

我正在开发 STV 选举后端。

整个项目已经完成——前端站点、数据库、结果生成、精美的动画结果差异等。我唯一无法做的就是唯一性验证。

根据 STV 的运作方式,每个选民都可以为任意数量的候选人输入偏好 (int)。如果他们的第一偏好被淘汰,他们的投票将转移到他们的第二偏好,依此类推。所有这些都存储在council_votes 表中,列有voter_idcandidate_idpreference

用户也需要能够交换偏好。但是,鉴于偏好唯一性约束,个别更新会破坏验证。为了解决这个问题,并防止网络超时导致数据丢失,我添加了事务。

客户端应用发送begin transaction 消息,发送其偏好更改,最后发送commit 消息。在事务期间,所有更改都会创建带有in_transaction: true 的记录;销毁使用in_transcation: true, transaction_destroy: true 创建记录。提交事务首先会破坏记录,然后使用正确的首选项重新创建记录。如果发生错误,它会回滚更改并通知客户端。

考虑到它的工作原理,基本上有三组“投票”:

  • 正常投票
  • in_transaction投票
  • in_transaction && transaction_destroy投票

为了防止重复的候选人/偏好,我必须确保它们在这三个集合中是唯一的。但鉴于两个状态列都是布尔值,我该怎么做?

或者更容易更改架构并将in_transactiontransaction_destroy 替换为transaction_state (null|create|destroy) 并改为范围?这似乎是一个更明智的选择。

【问题讨论】:

    标签: ruby-on-rails validation


    【解决方案1】:

    使用自定义验证来检查现有记录是否存在:

    validate :is_new_preference
    
    def is_new_preference
      !Item.exists?(preference: preference, voter_id: voter_id, term_id: term_id, in_transaction: in_transaction, transaction_destroy: transaction_destroy)
    end
    

    【讨论】:

    • 啊哈,这应该可以。我已经重构了我的代码以使用更理智的transaction_state,所以我无法对此进行测试。我会将其标记为已接受的答案。 ^^
    【解决方案2】:

    您可能处于修复此问题有点晚的状态,但这似乎非常脆弱并且容易出现竞争条件。

    class Voter < ActiveRecord::Base
      has_many :council_votes
    end
    
    class Candidate < ActiveRecord::Base
      has_many :candidacies
      has_many :terms, through: :candidacies
    end
    
    class Candidacy < ActiveRecord::Base
      enum status: [:running, :dropped]
      belongs_to :candidate
      belongs_to :term
      validates_uniqueness_of :candidate_id, scope: :term_id
    end
    
    class Term < ActiveRecord::Base
      has_many :candidacies,
      has_many :candidates, through: :candidacies
    end
    
    class CouncilVote < ActiveRecord::Base
      belongs_to :voter
      belongs_to :candidacy
      has_one :candidate, through: :candidacy
      has_one :term, through: :candidacy
      validates_uniqueness_of :voter_id, scope: :candidacy_id
    end 
    

    这里我们在CandidateTerm 之间添加一个candidacy m-2-m 连接表。使用枚举位掩码列,我们可以设置状态。这意味着我们只需要保证两列的唯一性。

    让我们添加一些数据库约束来防止竞争条件并提高性能:

    class AddUniqenessToCandidacy < ActiveRecord::Migration
      def change
        add_index :candidacies, [:candidate_id, :term_id], unique: true
      end
    end
    
    class AddUniqenessToCouncilVote < ActiveRecord::Migration
      def change
        add_index :council_votes, [:candidacy_id, :voter_id], unique: true
      end
    end
    

    这里最大的不同是我们只是在候选人被淘汰时更新candidacies表。

    @canditate = Canditate.find_by(name: 'Berny')
    @canditate.candidacies.last.dropped! # sorry Berny
    

    这是一种软删除。而不是拉和重新插入整个事务Schrödingers Cat dilemma,我们保留所有内容并使用rating 列对结果进行排序。

    @term = Term.find_by(year: 2016)
    @votes = CouncilVote.joins(:candidacy, :candidate, :term)
               .where(candidacy: { status: :running })
               .where(voter: @voter)
               .where(term: @term)
               .order(rating: :desc)
    

    【讨论】:

    • 此代码更多的是一种理论方法,而不是实际工作代码 - 它很可能充满了拼写错误且未经测试。
    • 这需要一些关键功能(例如在候选人退出时影响选民的偏好),但这绝对是一个好的开始。但是,在两个候选人之间交换两个preferences(你的rating)时,它仍然存在唯一性问题。这段代码实际上允许重复的ratings。 :(
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-06-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-06
    • 1970-01-01
    相关资源
    最近更新 更多