4

我的问题是我使用的查询finder_sql在移交给 PostgreSQL 之前没有正确解析,导致数据库语法错误。

为了说明问题,我刚刚使用了这里的示例代码:

http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

我只是更改class_name为,"User"因为我没有人物模型,但这在这里无关紧要。

has_many :subscribers, :class_name => "User", :finder_sql =>
'SELECT DISTINCT people.* ' +
'FROM people p, post_subscriptions ps ' +
'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
'ORDER BY p.first_name'

当我使用它时,我收到以下错误:

User Load (0.3ms)  SELECT DISTINCT people.* FROM people p, post_subscriptions ps WHERE
ps.post_id = #{id} AND ps.person_id = p.id ORDER BY p.first_name
PGError: ERROR:  Syntaxerror near »{« 
LINE 1: ...ople p, post_subscriptions ps WHERE ps.post_id = #{id} AND p...
                                                         ^

如您所见#{id},它不会被对象的 id 替换,这会引发 PostgreSQL 错误。

环境

  • 导轨 3.1
  • 转速
  • PostgreSQL 9.1
  • Ubuntu 11.10
  • Ruby 1.9.2p290(2011-07-09 修订版 32553)[x86_64-linux]
4

3 回答 3

10

我认为您真正要寻找的是:

has_many :posts, :finder_sql =>
    proc {"SELECT p.* from posts p join topics t on p.topic_id = t.id where t.id=#{id}"}

从 Rails 3.1 开始,您必须使用 proc 而不是字符串才能使用#{id}.

在此处查看问题:https ://github.com/rails/rails/issues/3920

于 2012-05-16T14:15:24.007 回答
8

的文档:finder_sql非常不完整,示例代码已损坏。正如您所发现的,这:

has_many :subscribers, :class_name => "User", :finder_sql =>
  'SELECT DISTINCT people.* ' +
  'FROM people p, post_subscriptions ps ' +
  'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
  'ORDER BY p.first_name'

将无法工作,并且基于 ActiveRecord 源,无法工作。如果您检查源代码,您会看到如下内容

def custom_finder_sql
  interpolate(options[:finder_sql])
end

然后interpolate这样做:

def interpolate(sql, record = nil)
  if sql.respond_to?(:to_proc)
    owner.send(:instance_exec, record, &sql)
  else
    sql
  end
end

因此,如果您:finder_sql只是一个字符串(例如在示例中),那么它会按原样使用而根本没有插值,并且最终会出现损坏的 SQL。如果你想要插值,那么你必须interpolate进入第一个分支,所以你需要一个 Lamba for:finder_sql和一个在 lambda 内的双引号字符串,这样#{id}才能工作:

has_many :subscribers, :class_name => "User", :finder_sql => ->(record) do
      "SELECT DISTINCT people.* " +
      "FROM people p, post_subscriptions ps " +
      "WHERE ps.post_id = #{id} AND ps.person_id = p.id " +
      "ORDER BY p.first_name"
end

这应该进入内部的第一个分支,interpolate以便对instance_exec调用进行评估并在相关对象的上下文中插入字符串。我不确定什么时候record不会,nil所以你可能想要这个:

has_many :subscribers, :class_name => "User", :finder_sql => ->(record) do
      record = self if(record.nil?)
      "SELECT DISTINCT people.* " +
      "FROM people p, post_subscriptions ps " +
      "WHERE ps.post_id = #{record.id} AND ps.person_id = p.id " +
      "ORDER BY p.first_name"
end

当我们在这里时,请使用显式连接条件而不是隐式连接条件:

has_many :subscribers, :class_name => "User", :finder_sql => ->(record) do
      record = self if(record.nil?)
      "SELECT DISTINCT people.* " +
      "FROM people p " +
      "JOIN post_subscriptions ps on p.id = ps.person_id " +
      "WHERE ps.post_id = #{record.id} " +
      "ORDER BY p.first_name"
end

您发现的有关单引号/双引号和的博客:finder_sql

http://tamersalama.com/2007/05/17/finder_sql-single-vs-double-quotes/

已过时,似乎不适用于 Rails 3+。上面的摘录来自 3.1,但您看到的行为表明代码和行为可能在 3.0 中发生了变化,但文档没有更新。

于 2011-12-18T01:37:12.887 回答
0

我知道这不是您希望听到的,但问题是您应该让 ActiveRecord 为您完成这项工作。

你真正想要解决这个问题是拥有这三个文件:

# user.rb
class User < ActiveRecord::Base
  self.table_name = 'people'
  has_many :post_subscriptions
end
# post_subscription.rb
class PostSubscription < ActiveRecord::Base
  belongs_to :user
  belongs_to :post
end
# post.rb
class Post < ActiveRecord::Base
  has_many :post_subscriptions
  has_many :subscribers, :through => :post_subscriptions, :source => :user
end

这样您就不必编写任何 SQL。只需致电@post.subscribers获取订阅用户的完整列表。

于 2011-12-18T00:42:08.013 回答