You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的记录:

  1. 生成迁移文件:
rails generate migration AddUniqueIndexToMeanOfPayments
  1. 编辑迁移文件:
class AddUniqueIndexToMeanOfPayments < ActiveRecord::Migration[7.0] # 替换为你的Rails版本
  def change
    add_index :mean_of_payments, [:company_id], unique: true, where: "company_standard = true"
  end
end
  1. 执行迁移:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 23:57:32