4

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

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')"

所有帮助表示赞赏。

4

1 回答 1

3

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

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

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 适配器提供了自己的quote输入,ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#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

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

> 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 标准进行口头服务,但似乎没有人完全支持它,每个人都有自己的扩展和怪癖需要担心。您最终将编写自己的可移植层,其中包含实用方法和猴子补丁的混合。

于 2012-12-04T01:55:36.450 回答