Rails 唯一性验证无缘无故失败

Rails uniqueness validation fails for no reason

提问人:Julian 提问时间:8/28/2023 最后编辑:Julian 更新时间:9/2/2023 访问量:64

问:

我有一个基于范围的唯一性验证:在 Rails 模型上。validates :company_standard, uniqueness: { scope: :company_id }X

在随附的屏幕截图中,我尝试通过表单(=标准流程)为该公司创建一个新的类对象,而没有激活该字段或任何其他布尔字段。Xcompany_standard

尽管如此,保存仍失败,并显示验证错误(请参阅屏幕截图)。uniquenesscompany_standard

该公司存在一个单独的类对象,数据库中存在 company_standard=true(我仔细检查了正确命名的表格)。更新现有记录并将布尔字段设置为 false 似乎也会失败,并出现相同的验证错误。X

不知道为什么会失败。对我来说看起来像是意想不到的行为。

The validation error on the form

@mechnicov评论后更新:模型

    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

控制器

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

视图 1 - 全新

<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>

视图 2 - 表单

<%= 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 %>
Ruby-on-Rails Ruby 验证 Validates-uniqueness-of

评论

0赞 mechnicov 8/28/2023
最好添加日志、模型代码、控制器、视图等,而不是您的图片。
0赞 Julian 8/28/2023
@mechnicov刚刚用所有代码片段编辑了帖子!错误消息来自区域设置,但是唯一性验证错误
0赞 Les Nightingill 8/28/2023
它在创建和更新时都失败了吗?
0赞 Julian 8/28/2023
@Les Nightingill 中,它对于唯一性验证适用的对象(= 唯一为真)进行新建和更新时失败。可以更新其他现有对象(在此新对象失败之前创建的位置)

答:

1赞 smathy 8/28/2023 #1

更新现有记录并将布尔字段设置为 false 似乎也会失败,并出现相同的验证错误。

这强烈意味着您的数据库包含另一条带有 .请从 rails 控制台检查一下,内容如下:company_idcompany_standard=false

> MeanOfPayment.where company_id: the_company_id

请通过复制/粘贴这些控制台命令和对问题的回答来显示这一点。如果只有一条记录,那么下一个明显的嫌疑人就是您的电话,请将该代码添加到您的问题中。remove_spaces

评论

0赞 Allacassar 8/28/2023
如果数据库中有 nil 并保存 nil 的记录 - 它也将失败 uniqness
0赞 Julian 8/31/2023
我稍后会检查,但 DB 中还有其他记录,DB 中肯定有 company_standard = nil。
0赞 Julian 9/1/2023
所有记录都设置为 '''company_standard: false'''。但仍然存在一个问题:新的“公司”>“创建/更新”和“mean_of_payments”与“company_standard:false/true”“一起使用。但是第二个“mean_of_payment”未能通过唯一性验证,这与我如何在第二条记录上设置“company_standard”无关。当第 1 条记录 '''mean_of_payment.company_standard: true''' 并且我尝试在第 2 条记录上设置 '''true''' 时,这应该会失败。但是,如果第二条记录有 '''company_standard: false''' 或第 1 条记录有 '''company_standard: false''',这应该可以完美无缺。
0赞 Julian 9/1/2023
@smathy - 不要回答你的问题。应该有可能有多个记录,但它应该是唯一的。唯一性是否可以不应用于布尔值,因为两个记录都有一个值?'''True''' 还是 '''false'''?
0赞 Julian 9/1/2023
刚刚有了一个想法:“验证者”总体上可能是错误的。它正在检查“uniquness”。我想确保属性'''company_standard'只设置为'''true'',其余的在逻辑上应该是''false''。当布尔值设置为 '''false''' 而不是 '''nil''' 时,唯一性验证可能会失败。我现在认为,唯一性验证不应该用于验证一个为真的布尔属性。如果我能测试并希望回答我自己的问题,如果它有效:-)
1赞 Alex 8/28/2023 #2

您正在从company_idparams[:company_id]

company_id = Company.find(params[:company_id]).id
@mean_of_payment = MeanOfPayment.new(company_id: company_id)

只是为了在您的表单中覆盖它

f.hidden_field :company_id, value: current_user.accounting_company.id

我认为无论参数如何,它都保持不变。

此外,您的表单 url 似乎有点可疑:

home_company_mean_of_payments_path(id: current_company.id)

这给了你.这难道不意味着那个控制器吗? - 复数,所以似乎不合适,你正在创建你还没有 ID。params[:id]mean_of_payment.idhome_company_mean_of_paymentsid

查看您的日志,看看您提交的参数。很难说出您期望拥有什么company_id与提交的内容:

params[:id]
params[:company_id]
params[:mean_of_payment][:company_id] # this is what you're using for your model

current_user.accounting_company.id    # are these different from params[:company_id]
current_company.id                    #

评论

0赞 Julian 9/1/2023
所有记录都设置为 '''company_standard: false'''。但仍然存在一个问题:新的“公司”>“创建/更新”和“mean_of_payments”与“company_standard:false/true”“一起使用。但是第二个“mean_of_payment”未能通过唯一性验证,这与我如何在第二条记录上设置“company_standard”无关。当第 1 条记录 '''mean_of_payment.company_standard: true''' 并且我尝试在第 2 条记录上设置 '''true''' 时,这应该会失败。但是,如果第二条记录有 '''company_standard: false''' 或第 1 条记录有 '''company_standard: false''',这应该可以完美无缺。
1赞 Julian 9/2/2023 #3

似乎我对验证者有错误的理解(花了 2 天才找到......uniquness

我认为它确保一个属性只设置为一次为真(即我的想法:“唯一”)。

正确但它实际上会检查该属性是否普遍存在。所以在这种情况下也存在,即 (如果 Rails 代码检查为 nil,则没有仔细检查)。现在看起来很愚蠢:-)。falsenot nil

所以就我而言

一个,但只有一个布尔属性是 。company has_many mean_of_paymentscompany_standardtrue

唯一性验证当然不起作用,因为其他已设置为 .mean_of_paymentscompany_standardfalse

我需要一个验证器来检查最大发生次数到最大 1。没有找到标准的东西,所以我编写了自己的自定义验证器,它可以解决问题:

在模型上使用 validates_with 进行自定义验证

class MeanOfPayment < ApplicationRecord
  belongs_to :company
...
  validates_with UniqueCompanyStandardValidator, if: :company_standard
end

使用自定义验证器类:

class UniqueCompanyStandardValidator < ActiveModel::Validator
  def validate(record)
    mops = MeanOfPayment.where(company_id: record.company_id, company_standard: true).pluck(:id) # looks in DB not at the current record
    if mops.length > 0 # DB shouldnt have a company_standard: true
      record.errors[:base] << I18n.t('activerecord.errors.models.mean_of_payment.attributes.company_standard.taken')
    end
  end
end

总结

  1. 仅当属性设置为true
  2. 检查数据库是否已存在属性mean_of_payment.company_standardtrue

在阅读了您的所有评论后,今天上午 11 点发生了整件事!非常感谢!感谢您的间接帮助!:-)