【问题标题】:Validate unique array values in Rails在 Rails 中验证唯一的数组值
【发布时间】:2018-01-25 02:17:44
【问题描述】:

我的交易模型使用以下控制器逻辑根据数量(整数列)生成唯一票号数组:

@transaction.quantity.times.uniq { @transaction.ticket_numbers << rand(100000..999999) }

但是,这只能确保数组中的数字是唯一的。

我需要一个检查所有 Transaction.ticket_numbers 数组的数据库验证,以确保每个值(票号)在所有数组中都是唯一的。

这是我在 schema.rb 中的事务表:

  create_table "transactions", force: :cascade do |t|
    t.string "payee"
    t.integer "quantity"
    t.decimal "debt", precision: 8, scale: 2
    t.string "email"
    t.string "ministry"
    t.integer "status"
    t.integer "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "ticket_numbers", default: [], array: true
  end

来自 transaction.rb 的交易模型:

require 'csv'

class Transaction < ApplicationRecord
  belongs_to :user

  validates :email, :format => { :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/, :on => [:create, :update] }
  validates :payee, presence: true
  validates :quantity, numericality: { greater_than_or_equal_to: 1 }
  validates :debt, numericality: { greater_than_or_equal_to: 0 }

  def self.to_csv
    attributes = %w{payee email ministry quantity debt status ticket_numbers}
    CSV.generate(headers: true) do |csv|
      csv << attributes
      all.each do |transaction|
        csv << attributes.map{ |attr| transaction.send(attr) }
      end
    end
  end
end

事务控制器:

class TransactionsController < ApplicationController
  before_action :load_transaction, only: [:edit, :update, :destroy]

  def create
    @transaction = Transaction.new(transaction_params)
    if @transaction.save
      @transaction.quantity.times.uniq { @transaction.ticket_numbers << rand(100000..999999) }
    end
    @user = @transaction.user
    @transactions = @user.transactions
    respond_to do |format|
      if @transaction.save && @transaction.status == 1
        UserMailer.payment_confirmation(@transaction).deliver_later
        format.html { redirect_to user_url(@user), notice:'Transaction added & ticket sent' }
        format.json { render json: @user, status: :created, location: @user }
      elsif @transaction.save && @transaction.status != 1
        format.html { redirect_to user_url(@user), notice:'Transaction added' }
        format.json { render json: @user, status: :created, location: @user }
      else
        format.html { render 'users/show' }
        format.json { render json: @transaction.errors, status: :unprocessable_entity }
      end
    end
  end

  def edit
    @user = current_user
  end

  def update
    update_ticket_numbers
    respond_to do |format|
      if @transaction.update_attributes(transaction_params) && @transaction.status == 1
        UserMailer.payment_confirmation(@transaction).deliver_later
        format.html { redirect_to user_url(current_user), notice: 'Transaction info updated & confirmation email sent to payee' }
        format.json { render json: current_user, status: :created, location: current_user }
      elsif @transaction.update_attributes(transaction_params) && @transaction.status != 1
        format.html { redirect_to user_url(current_user), notice: 'Transaction info updated' }
        format.json { render json: current_user, status: :created, location: current_user }
      else
        format.html { render :edit }
        format.json { render json: @transaction.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @transaction.destroy
    redirect_to user_url(current_user), notice: 'Transaction deleted'
  end

  private

  def load_transaction
    @transaction = Transaction.find(params[:id])
  end

  def transaction_params
    params.require(:transaction).permit(:payee, :email, :ministry, :debt,
    :quantity, :status, :user_id, :ticket_numbers)
  end

  def update_ticket_numbers
    if @transaction.update_attributes(transaction_params)
      if @transaction.ticket_numbers.length < @transaction.quantity
        i = @transaction.quantity - @transaction.ticket_numbers.length
        i.times.uniq { @transaction.ticket_numbers << rand(100000..999999) }
      elsif @transaction.ticket_numbers.length > @transaction.quantity
        i = @transaction.ticket_numbers.length - @transaction.quantity
        i.times { @transaction.ticket_numbers.pop }
      end
    end
  end

结束

【问题讨论】:

  • 如果票号位于单独的模型/表中而不是数组中,使用票号会更容易吗?
  • 我可以看到你在做什么,但如果我使用一个单独的模型,它纯粹是为了生成可以在编辑 Transaction.quantity 时添加和删除的票号。当交易在表单上标记为完成时,我还使用 ActionMailer 自动发送电子邮件。票号模型不会过分吗?谢谢!
  • 我不是专家,但你不能尝试所有数组的交集并检查交集是否为空以找到唯一性吗?
  • @D_Cheeles 我认为 Brad 的建议最终会少于你目前所走的道路。数据库擅长独特的约束。如果可以的话,你应该利用它。
  • @DCheeles,有一个笑话是“老板,我写了一个程序做错了事,但做的很快”。先把它做好,以后当它成为问题时再担心性能。就像其他人说的那样,我认为您应该为 Ticket 创建一个单独的模型并使用唯一索引实现约束。

标签: ruby-on-rails arrays postgresql validation unique


【解决方案1】:

您可以为模型添加自定义票号验证。你可以阅读更多关于他们的信息here

class Transaction < ApplicationRecord
  validate :ticket_number_uniqueness

  def ticket_number_uniqueness
    # First check all new ticket numbers are unique from each other
    errors.add(:ticket_number, "is not unique") unless ticket_numbers == ticket_numbers.uniq
    
    # Next check against other ticket numbers
    ticket_numbers.each do |ticket|
      if ::Transaction.where("ticket_numbers @> '{?}'", ticket).exists?
        errors.add(:ticket_number, "is not unique")
      end
    end
  end
end

注意:这是利用 Postgres 包含数组方法 @>。你可以了解更多关于数组方法here

【讨论】:

  • 这看起来应该在没有单独的 Ticket 模型的情况下工作。我要改变的一件事是 ticket_numbers == ticket_numbers.uniq 比较。无法保证调用 uniq 后订单与原始订单一致。应该应用排序,或者应该进行集合比较。
  • 是什么让您认为订单无法保证? apidock.com/ruby/Array/uniq。我建议使用简单的长度比较 ticket_numbers.size == ticket_numbers.uniq.size 作为替代方案,这也可能会提高性能,但希望在我的回答中保持简单。
  • 严格来说,订单是保证的。那里的文档没有说明返回数组的顺序。实际上,您可能是对的。
猜你喜欢
  • 2011-11-17
  • 1970-01-01
  • 1970-01-01
  • 2018-04-30
  • 2013-11-23
  • 1970-01-01
  • 2021-09-11
  • 2012-11-15
  • 1970-01-01
相关资源
最近更新 更多