Rails Scope唯一性验证无理由失效问题求助
解决布尔字段唯一性验证的异常问题
问题根源
你当前设置的唯一性验证validates :company_standard, uniqueness: { scope: :company_id }会对该字段的所有值生效——不管是true还是false,同一个公司下的MeanOfPayment记录,company_standard的值都必须唯一。这就导致:
- 创建未勾选
company_standard(即值为false)的新记录时,如果公司已有其他false的记录,就会触发验证错误 - 将已有
true的记录改为false时,若公司存在其他false记录,同样触发错误
而你的实际需求应该是每个公司仅允许一条company_standard=true的记录,false的记录可以存在多条。
修正验证规则
修改MeanOfPayment模型中的验证,添加条件判断,仅当company_standard为true时执行唯一性验证:
class MeanOfPayment < ApplicationRecord belongs_to :company has_many :in_payments has_many :out_payments has_many :bank_statements, dependent: :destroy # 仅在company_standard为true时验证唯一性 validates :company_standard, uniqueness: { scope: :company_id }, if: -> { company_standard } before_validation :remove_spaces # ... 其他方法保持不变 end
数据库层面的防护(可选但推荐)
为了避免并发场景下的竞态问题(比如同时创建两条company_standard=true的记录),建议添加数据库唯一索引,仅针对company_standard=true的记录:
- 生成迁移文件:
rails generate migration AddUniqueIndexToMeanOfPayments
- 编辑迁移文件:
class AddUniqueIndexToMeanOfPayments < ActiveRecord::Migration[7.0] # 替换为你的Rails版本 def change add_index :mean_of_payments, [:company_id], unique: true, where: "company_standard = true" end end
- 执行迁移:
rails db:migrate
提问者提供的相关代码
Model
class MeanOfPayment < ApplicationRecord belongs_to :company has_many :in_payments has_many :out_payments has_many :bank_statements, dependent: :destroy validates :company_standard, uniqueness: { scope: :company_id } before_validation :remove_spaces def balance ... end def account_name ... end private def remove_spaces ... end end
Controller
class Home::MeanOfPaymentsController < HomeController def index @title = t('titles.mean_of_payments.index') @mean_of_payments = current_company.mean_of_payments end def show @title = t('titles.mean_of_payments.show') @mean_of_payment = current_company.mean_of_payments.find(params[:id]) end def new @title = t('titles.mean_of_payments.new') company_id = Company.find(params[:company_id]).id @mean_of_payment = MeanOfPayment.new(company_id: company_id) end def create fix_number(mean_of_payment_params[:balance_at_date]) @mean_of_payment = (mean_of_payment_params[:type].constantize).new(mean_of_payment_params) if @mean_of_payment.save redirect_to home_company_mean_of_payments_path(Company.find(params[:company_id])), notice: t('mean_of_payment.notifications.create_successfully') else render :new end end def edit @title = t('titles.mean_of_payments.edit') @mean_of_payment = MeanOfPayment.find(params[:id]) end def update fix_number(mean_of_payment_params[:balance_at_date]) @mean_of_payment = MeanOfPayment.find(params[:id]) if @mean_of_payment.update(mean_of_payment_params) redirect_to home_company_mean_of_payments_path(Company.find(params[:company_id])), notice: t('mean_of_payment.notifications.update_successfully') else render :edit end end def destroy mean_of_payment = MeanOfPayment.find(params[:id]) company_id = mean_of_payment.company.id mean_of_payment.update!(marked_deleted: true) redirect_to home_company_mean_of_payments_path(company_id: company_id), method: :destroy, notice: t('mean_of_payment.notifications.delete_successfully') end private def mean_of_payment_params validation = if params[:bank_account].present? params.require(:bank_account) elsif params[:cash_counter].present? params.require(:cash_counter) elsif params[:mean_of_payment].present? params.require(:mean_of_payment) else raise "Zahlungsmethode muss im Controller eingeführt werden!" end validation.permit( :account_name, :account_number, :balance_at_date, :balance_date, :bank_name, :bank_number, :company_id, :company_standard, :for_out_invoice, :owner, :primary_debitable, :type, ) end end
View - new
<div class="row"> <div class="col-8 offset-2"> <%= link_to t('common.buttons.back'), home_company_mean_of_payments_path %> <h1> <%= t('mean_of_payment.new.title') %> </h1> <%= render 'form', path: home_company_mean_of_payments_path(id: current_company.id) %> </div> </div>
View - form
<%= simple_form_for @mean_of_payment, url: path do |f| %> <%= f.hidden_field :company_id, value: current_user.accounting_company.id %> <div class="row"> <div class="col-6"> <div class="ibox float-e-margins"> <div class="ibox-content"> <% if @mean_of_payment.errors.any? %> <div id="error_explanation"> <h4><%= t('activerecord.errors.models.mean_of_payment.prohibited_save', count: @mean_of_payment.errors.count) %></h4> <ul> <% @mean_of_payment.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="form-group"> <%= f.label :type %> <%= f.select :type, options_for_mean_of_payment_general, class: 'form-control' %> </div> <div class="form-group"> <%= f.label :bank_name %> <%= f.text_field :bank_name, class: 'form-control w-50' %> </div> <div class="form-group"> <%= f.label :bank_number %> <%= f.text_field :bank_number, class: 'form-control w-50' %> </div> <div class="form-group"> <%= f.label :account_name %> <%= f.text_field :account_name, class: 'form-control w-50'%> </div> <div class="form-group"> <%= f.label :account_number %> <%= f.text_field :account_number, class: 'form-control w-50' %> </div> <div class="form-group"> <%= f.label :owner %> <%= f.text_field :owner, class: 'form-control w-50' %> </div> <div class="form-group"> <%= f.label :balance_at_date %> <%= f.text_field :balance_at_date, class: 'form-control w-50' %> </div> <div class="form-group"> <%= f.label :balance_date %> <%= f.input :balance_date, as: :date, html5: true, class: 'form-control w-50', label: false %> </div> <div class="form-group"> <%= f.label :company_standard %> <%= f.check_box :company_standard, class: 'form-control w-50' %> </div> <div class="form-group"> <%= f.label :for_out_invoice %> <%= f.check_box :for_out_invoice, class: 'form-control w-50' %> </div> <div class="form-group"> <%= f.label :primary_debitable %> <%= f.check_box :primary_debitable, class: 'form-control w-50' %> </div> <div class="form-group"> <%= f.submit t('common.buttons.create_or_modify'), class: 'btn btn-primary' %> </div> </div> </div> </div> </div> <% end %>
内容的提问来源于stack exchange,提问作者Julian
相关产品推荐
相关产品推荐

