【问题标题】:Rails ActiveRecord Date Comparison Database Agnostic?Rails ActiveRecord 日期比较数据库不可知?
【发布时间】:2012-11-20 07:52:37
【问题描述】:

我正在尝试找到一种与数据库无关的方法来将日期与活动记录查询进行比较。我有以下查询:

UserRole.where("(effective_end_date - effective_start_date) > ?", 900.seconds)

这在 MySQL 上运行良好,但在 PG 上会产生错误,因为它生成的 sql 不包含“interval”语法。从控制台:

  ←[1m←[36mUserRole Load (2.0ms)←[0m  ←[1mSELECT "user_roles".* FROM "user_roles" WHERE "user_roles"."effective_end_date" IS NULL AND ((effective_end_d
ate - effective_start_date) > '--- 900
...
')←[0m
ActiveRecord::StatementInvalid: PG::Error: ERROR:  invalid input syntax for type interval: "--- 900

当我使用 to_sql I 选项运行它时,我得到:

irb(main):001:0> UserRole.where("effective_end_date - effective_start_date) > ?", 900.seconds).to_sql
=> "SELECT \"user_roles\".* FROM \"user_roles\"  WHERE \"user_roles\".\"effective_end_date\" IS NULL AND (effective_end_date - effective_start_date) >
'--- 900\n...\n')"

感谢所有帮助。

【问题讨论】:

  • UserRole.where(...) 与您从日志中挖出的 SQL 不匹配;并且在您的日志查询中执行900.to_yaml,这没有任何意义。
  • 这是正在生成的内容。我没有在下面粘贴完整的 rails 错误堆栈,但如果有用的话可以。我添加了一个 to_sql 版本,以便您可以生成生成的内容。感谢您的帮助
  • 知道effective_end_date is null 的来源吗? 900.seconds 的 YAML 化对我来说没有任何意义。而且您在.where 中缺少(,copy'n'paste 错误? effective_start_dateeffective_end_date 是日期还是日期时间/时间戳?
  • 嗯。我使用.where(..., 900.seconds) 的查询也在对 900 进行 YAML 化,Rails 到底在做什么过分聪明的废话?当900.seconds.class 表示Fixnum 时,seconds 实际上返回一个ActiveSupport::Duration。叹息。
  • 这是一个很好的问题 - 没有发现 effective_end_date 为空。它来自模型的默认范围。我会重新运行它,看看它是否会改变任何东西

标签: ruby-on-rails postgresql datetime activerecord rails-activerecord


【解决方案1】:

如果您的 effective_end_dateeffective_start_date 列确实是日期,那么您的查询毫无意义,因为日期的最小分辨率为一天,而 900 比 86400 小很多(AKA 25*60*60 或 1 天)。因此,我假设您的“日期”列实际上是日期时间(AKA 时间戳)列;如果这是真的,那么您可能想要重命名列以避免在维护期间出现混淆,effectively_starts_ateffectively_ends_at 可能非常适合通常的 Rails 约定。如果这个假设无效,那么您应该更改列类型或停止使用 900s。

回到真正的问题。 ActiveRecord 使用 ActiveRecord::ConnectionAdapters::Quoting#quote 方法将 Ruby 值转换为 SQL 值:

def quote(value, column = nil)
  # records are quoted as their primary key
  return value.quoted_id if value.respond_to?(:quoted_id)

  case value
  #...
  else
    "'#{quote_string(YAML.dump(value))}'"
  end
end

因此,如果您尝试将某些内容用作占位符的值,并且该类型没有内置任何特定处理,那么您会得到 YAML(IMO 的默认设置的奇怪选择)。此外,900.seconds 是一个ActiveSupport::Duration 对象(尽管900.seconds.class 说了什么)并且case value 没有ActiveSupport::Duration 的分支,所以900.seconds 将得到YAML 化。

PostgreSQL 适配器在ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#quote 中提供了自己的quote,但它也不知道ActiveSupport::Duration。 MySQL 适配器的quote 也忽略了ActiveSupport::Duration。您可以在这些 quote 方法中添加一些意义。在初始化程序中是这样的:

class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
    # Grab an alias for the standard quote method
    alias :std_quote :quote
    # Bludgeon some sense into things
    def quote(value, column = nil)
        return "interval '#{value.to_i} seconds'" if(value.is_a?(ActiveSupport::Duration))
        std_quote(value, column)
    end
end

有了那个补丁,当你使用 ActiveSupport::Duration 时,你会得到 PostgreSQL 理解的间隔:

> Model.where('a - b > ?', 900.seconds).to_sql
 => "SELECT \"models\".* FROM \"models\"  WHERE (a - b > interval '900 seconds')" 
> Model.where('a - b > ?', 11.days).to_sql
 => "SELECT \"models\".* FROM \"models\"  WHERE (a - b > interval '950400 seconds')"

如果您将类似的补丁添加到 MySQL 适配器的 quote(留给读者作为练习),那么类似:

UserRole.where("(effective_end_date - effective_start_date) > ?", 900.seconds)

将在 PostgreSQL 和 MySQL 中做正确的事,您的代码不必担心它。


也就是说,在不同的数据库上开发和部署是一个非常糟糕的主意,这会让圣诞老人哭泣并为你的长袜寻找一些煤(可能含有砷,可能是放射性的)。所以不要那样做。

另一方面,如果您正在尝试构建与数据库无关的软件,那么您将迎来一段快乐的时光!数据库可移植性在很大程度上是一个神话,与数据库无关的软件总是意味着在您的平台提供的 ORM 和数据库接口之上编写您自己的可移植层。您将不得不对计划支持的每个数据库上的所有内容进行详尽的测试,每个人都对 SQL 标准进行口头上的服务,但似乎没有人完全支持它,每个人都有自己的扩展和怪癖需要担心。您最终将编写自己的可移植层,其中包含实用方法和猴子补丁的混合体。

【讨论】:

  • 非常感谢您的详细回答(远远超出我目前的知识水平 - 但我明白你在说什么)。知道与数据库无关的软件不是 Rails 给定的,这真的很有用。我很高兴坚持使用一个数据库,但希望尽可能保持选项开放。感谢您的帮助。
  • @Paul:与数据库无关的软件的存在方式与与操作系统无关的软件的存在方式相同 :)
猜你喜欢
  • 2018-03-20
  • 2012-02-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多