如何验证数据库中是否已经存在一个列组合,rails 6、postgres

How to validate if a column combination already exists in database, rails 6, postgres

这里是新手!

ruby 2.7.1, gem --version 3.1.4, Rails 6.0.3.4

我的查询出现错误

def entry_uniqueness
    if Entry.where("lower(from) = ? AND lower(to) = ?",
                   from.downcase, to.downcase).exists?
      errors.add(:base, 'Identical combination already exists. YUP.')
    end
  end

错误:

PG::SyntaxError: ERROR: syntax error at or near "from" LINE 1: SELECT 1 AS one FROM "entries" WHERE (lower(from) = 'here' A... ^

完全错误

app/models/entry.rb:35:in entry_uniqueness' app/controllers/entries_controller.rb:9:in create' Started POST "/entries" for ::1 at 2021-02-13 16:17:47 +0800 Processing by EntriesController#create as HTML Parameters: {"authenticity_token"=>"98sjupNso6NW5xUonE/414I7ZvJQETMPBNWS+jcN+PffHaAJ3K0pdQofVPgnrBfflYn2SDXMlB17Q2G/gzideA==", "entry"=>{"translation"=>"1", "from"=>"here", "to"=>"there"}, "commit"=>"Post Translation"} [1m[36mEntry Exists? (10.2ms)[0m [1m[34mSELECT 1 AS one FROM "entries" WHERE (lower(from) = 'here' AND lower(to) = 'there') LIMIT [0m [["LIMIT", 1]] ↳ app/models/entry.rb:35:in `entry_uniqueness' Completed 500 Internal Server Error in 38ms (ActiveRecord: 10.2ms | Allocations: 2140)

ActiveRecord::StatementInvalid (PG::SyntaxError: ERROR: syntax error at or near "from" LINE 1: SELECT 1 AS one FROM "entries" WHERE (lower(from) = 'here' A... ^ ): app/models/entry.rb:35:in entry_uniqueness' app/controllers/entries_controller.rb:9:in create'


我想要实现的是:


| from  |  to   |
-----------------
| Here  | there |

=> 验证应防止用户添加 heRE | THERE 但可以添加 THERE | heRE

:from:to 位于 table entries


注意:我已经尝试过范围,它们在唯一性方面起作用但在不区分大小写方面失败

validates_uniqueness_of :from, scope: :to

也试过

validates_uniqueness_of :from, :scope => :to, :case_sensitive => false

也尝试了@r4cc00n 的实现,但它不起作用

scope :get_entries_by_from_and_to, ->(from, to) { where(arel_table[:from].lower.eq(from)).where(arel_table[:to].lower.eq(to))}
  validates_uniqueness_of :from, if: :entry_uniqueness?

  def entry_uniqueness?
    Entry.get_entries_by_from_and_to('from','to').nil?
  end

这是我认为您可以解决查询的方法:

您可以在 Entry 模型中定义一个范围,然后在任何地方使用该范围,如下所示:

scope :get_entries_by_from_and_to, ->(from, to) { where(arel_table[:from].lower.eq(from)).where(arel_table[:to].lower.eq(to))}

然后你可以使用它:Entry.get_entries_by_from_and_to('your_from','your_to') 如果那个 returns nil 那么你的数据库中没有符合你条件的记录。

话虽如此,如果您想将其与您拥有的内容和验证范围结合起来,您可以像下面那样进行:

def entry_uniqueness?
    Entry.get_entries_by_from_and_to('your_from','your_to').nil?
end
validates_uniqueness_of :from, if: :entry_uniqueness? 

请注意 validates_uniqueness_of 不 thread/concurrency 安全,这意味着在非常奇怪的情况下,您可以 运行 进入某些场景,在这些场景中您的数据库中没有唯一数据,为避免这种情况,您应该始终在数据库中创建唯一索引,以便数据库为您避免那些 'duplicated' 场景。

希望对您有所帮助!

from 是一个 reserved name in PostgreSQL。这很有意义,因为读取查询通常包含这个词(想想 SELECT value FROM table)。因此 Rails 根据您的条件构建的查询

SELECT 1 AS one FROM "entries" WHERE (lower(from) = 'here' A...
                ^^^^                        ^^^^

混淆了数据库。顺便说一句 to 也是一个保留名称。

避免 运行 出现这种问题的一种方法是您应该考虑重命名该列。

另一个简单的解决方法是将列名用双引号引起来。只需将您的条件更改为:

Entry.exists?('lower("from") = ? AND lower("to") = ?', from.downcase, to.downcase)

请注意,我在列名周围使用了双引号,在整个条件周围使用了单引号。

除了@Spickermann 提到的列命名问题,您还应该考虑使用 citext (case insentive) type column。与您的方法相比,它有许多优点:

  • 您不必使用冗长的查询。 WHERE foo = ? 不分大小写。
  • 您可以声明一个多列唯一索引以确保数据库级别的唯一性 - 这是不区分大小写的。这可以防止潜在的 duplicates due to race conditions.

缺点是,如果您必须切换到另一个数据库,如 MySQL 或 Oracle,那么您的应用程序的可移植性将降低,这可能不是一个真正的问题。

这当然要求您能够在您的 Postgres 数据库中启用扩展,并且您还需要确保您在测试、开发和生产中使用相同的数据库(无论如何这是一个好主意)。

class EnableCitext < ActiveRecord::Migration
  def change
    enable_extension("citext")
  end
end

class ChangeFoosToCitext < ActiveRecord::Migration
  def change
    change_column :foos, :bar, :citext
    change_column :foos, :baz, :citext 
    add_index :foos, [:bar, :baz], unique: true
  end
end

这将让您使用一个简单的验证范围:

validates :bar, uniqueness: { scope: :baz }

您不需要使用生成无效查询的 case_sentive: false 选项。