【问题标题】:How to instruct Rails to generate the correct SQL on uniqueness validation when case insensitive不区分大小写时如何指示 Rails 生成正确的唯一性验证 SQL
【发布时间】:2011-12-04 13:15:43
【问题描述】:

假设 Rails 3 使用 MySQL DB 和不区分大小写的排序规则

故事是什么:

Rails 允许您使用“唯一性”验证器来验证模型的属性。但是根据 Rails 文档,默认比较是区分大小写的。

这意味着在验证时它会执行如下 SQL:

SELECT 1 FROM `Users` WHERE (`Users`.`email` = BINARY 'FOO@email.com') LIMIT 1

对于拥有 CI Collat​​ion 数据库的我来说,这完全是错误的。它会认为 'FOO@email.com' 有效,即使用户表中已经有另一个用户 'foo@email.com'。换句话说,这意味着,如果应用程序的用户尝试使用电子邮件“FOO@email.com”创建一个新用户,这对于 Rails 来说将是完全有效的(默认情况下),并且 INSERT 将被发送到 db。如果您碰巧在电子邮件上没有唯一索引,那么您会大吃一惊 - 将毫无问题地插入行。如果你碰巧有一个唯一索引,那么就会抛出异常。

好的。 Rails 说:由于您的数据库具有不区分大小写的排序规则,因此请执行不区分大小写的唯一性验证。 这是怎么做到的?它告诉您可以通过在特定属性验证器上设置 ":case_sensitive => false" 来覆盖默认的唯一性比较敏感度。在验证时,它会创建以下 SQL:

SELECT 1 FROM `Users` WHERE (LOWER(`Users`.`email`) = LOWER('FOO@email.com') LIMIT 1

这是数据库表用户的灾难,您设计为在电子邮件字段上具有唯一索引,因为它不使用索引,所以会进行全表扫描。

我现在看到SQL中的LOWER函数是由ActiveRecordUniquenessValidator插入的(文件uniqueness.rb,模块ActiveRecord,模块ValidationsUniquenessValidator)。这是执行此操作的一段代码:

if value.nil? || (options[:case_sensitive] || !column.text?)
  sql = "#{sql_attribute} #{operator}"
else
  sql = "LOWER(#{sql_attribute}) = LOWER(?)"
end

所以问题转到 Rails/ActiveRecord 而不是 MySQL 适配器。

问题:有没有办法告诉 Rails 将有关唯一性验证大小写敏感性的要求传递给 MySQL 适配器,而不是“聪明”地改变查询?要么 为了澄清而改述的问题:是否有另一种方法可以对属性进行唯一性验证(请注意...我不是只谈论电子邮件,以电子邮件为例)关闭区分大小写并生成将在相应列上使用简单唯一索引的查询?

这两个问题是等价的。我希望现在,我让自己更清楚,以便得到更准确的答案。

【问题讨论】:

  • 你在 rails-core 小组发过这个吗?他们在那里说什么?

标签: mysql ruby-on-rails


【解决方案1】:

解决方法
如果您想要不区分大小写的比较,请执行以下操作:

SELECT 1 FROM Users WHERE (Users.email LIKE 'FOO@email.com') LIMIT 1;

没有通配符的LIKE 总是像不区分大小写的= 一样工作。
= 可以区分大小写或不区分大小写,具体取决于各种因素(转换、字符集...)

【讨论】:

  • 您的回答不适用于我的问题/主题。我说的是 Rails 如何处理验证“:uniqueness => {:case_sensitive => false}”。我不是在谈论搜索记录。当数据库排序为 CI 时,Rails finder 与 MySQL 完美配合。
  • @PanayotisMatsinopoulos,这是一种解决方法。因此,它很多不适用于您的问题,但确实可以解决:This works completely wrong for me who has a DB with CI Collation. It will consider the 'FOO@email.com' valid, even if there is another user with 'foo@email.com' already in Users table.
  • @PanayotisMatsinopoulos,解决方法仍然有效,至少 LIKE 会保留索引,请注意,没有通配符的 LIKE 与 = 确实相同
  • 问题是关于如何让 Rails 做到这一点,例如唯一性验证。您关于 sql select 唯一性的回答似乎不太匹配。
【解决方案2】:

验证唯一性而不考虑大小写

如果您想坚持以大写或小写形式存储电子邮件,那么无论大小写如何,您都可以使用以下命令来强制唯一性:

validates_uniqueness_of :email, case_sensitive: false

(另请参阅此问题: Rails "validates_uniqueness_of" Case Sensitivity)

完全删除案例问题

与其进行不区分大小写的匹配,不如在验证之前(因此也是)小写电子邮件:

before_validation {self.email = email.downcase}

由于大小写与电子邮件无关,这也将简化您所做的一切,并将阻止您将来可能进行的任何比较或数据库搜索

【讨论】:

  • 这是一种变通方法,适用于如果小写,您可以接受的属性。如果您有一个在保存之前不应该被小写的属性(例如,假设您有一个公司名称),那么您就不能这样做。您将更改用户希望存储在 db 中的值。对于电子邮件,这是可以接受的,但不适用于所有其他情况。以电子邮件为例。我还有其他字段需要保持用户输入的方式。
  • 我认为我的问题在您回复之前已更新。我还添加了实际进行不区分大小写的唯一性比较的方法。但是在电子邮件的情况下我不会将before_validationfilter 描述为一种解决方法,它实际上是一种非常明智的预防措施。在其他情况下,这是一种解决方法,但以多格存储电子邮件确实没有什么好处
【解决方案3】:

我已经四处搜索,根据我今天的知识,唯一可以接受的答案是创建一个验证方法来执行正确的查询和检查。换句话说,停止使用:uniqueness => true 并执行以下操作:

class User
  validate :email_uniqueness

  protected

  def email_uniqueness
    entries = User.where('email = ?', email)
    if entries.count >= 2 || entries.count == 1 && (new_record? || entries.first.id != self.id )
      errors[:email] << _('already taken')
    end
  end
end

这肯定会使用我在email 上的索引,并且可以在创建和更新时使用(或者至少在我测试过的情况下它确实可以)。

在 RubyOnRails Core Google 小组询问后

我从 RubyOnRails Core Google Group 得到了以下答案:Rails 正在 3.2 上修复这个问题。读这个: https://github.com/rails/rails/commit/c90e5ce779dbf9bd0ee53b68aee9fde2997be123

【讨论】:

    【解决方案4】:

    http://guides.rubyonrails.org/active_record_querying.html#finding-by-sql开头

    然后添加他们的输入 @约翰, @PanayotisMatsinopoulos 和这个 http://guides.rubyonrails.org/active_record_validations_callbacks.html#custom-methodshttp://www.w3schools.com/sql/sql_like.asp

    那么我们有这个:

     class User < ActiveRecord::Base
       validate :email_uniqueness
    
       protected
    
       def email_uniqueness
           like_emails = User.where("email LIKE ?", email))
           if (like_emails.count >= 2 || like_emails.count == 1 
               && (new_record? || like_emails.first.id != self.id ))
             errors[:email] << _('already taken')
           end
       end
     end
    

    【讨论】:

    • 您的解决方案不适用于模型更新。当您尝试更新模型时,它将验证为 false 并且更新将失败。
    • 那么为什么要使用“email =”?而不是“像电子邮件一样?”....我只是要去一个山洞里爬行并死去。
    • 我没有得到你的评论,真的。我的评论是 2 天前对您的初始答案不适合 更新。我没有说 LIKE VS =。 17 小时前,您编辑了错误的答案,现在它是正确的,可以更新,并且与我的答案一致。
    • 嗯,也许你应该从回答问题中去掉情绪,记住追求真理(而不是分数)才是这里的全部。上面的评论只是这个讨论的逻辑进展之后的一个问题,主要围绕找到实现所需结果的最佳路径。这个问题仍然围绕着我们两个答案之间的主要区别展开......“LIKE 与 =” amirite?我真的只是好奇。
    【解决方案5】:
    validates :email, uniqueness: {case_sensitive: false}
    

    像 Rails 4.1.0.rc2 中的魅力一样工作

    ;)

    【讨论】:

      【解决方案6】:

      在使用 MySQL 二进制修饰符后,我找到了一种方法,可以从所有比较字段的查询中删除该修饰符(不限于唯一性验证,但包括它)。

      首先: 为什么 MySQL 添加二进制修饰符?这是因为默认情况下 MySQL 以不区分大小写的方式比较字段。

      第二: 我应该在意吗? 我一直在设计我的系统时假设字符串比较是以不区分大小写的方式进行的,所以这是一个理想的功能我。 如果您不这样做,请注意

      这是添加二进制修饰符的地方:

      https://github.com/rails/rails/blob/ee291b9b41a959e557b7732100d1ec3f27aae4f8/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb#L545

        def case_sensitive_modifier(node)
          Arel::Nodes::Bin.new(node)
        end
      

      所以我覆盖了这个。我使用以下代码创建了一个名为“mysql-case-sensitive-override.rb”的初始化程序(在 config/initializers):

      # mysql-case-sensitive-override.rb
      class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter
        def case_sensitive_modifier(node)
          node
        end
      end
      

      就是这样。我的查询不再有二进制修饰符:D

      请注意,这并不能解释为什么验证器的“{case_sensitive: false}”选项不起作用,也不能解决它。它将默认且不可覆盖的区分大小写的行为更改为默认且不可覆盖的不区分大小写的新行为。我必须坚持,对于实际使用二进制修饰符进行区分大小写行为的任何比较,这也会改变(我希望)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-07-11
        • 2011-09-19
        • 1970-01-01
        • 1970-01-01
        • 2012-01-31
        • 1970-01-01
        相关资源
        最近更新 更多