【问题标题】:Constructing a Rails ActiveRecord where clause构建 Rails ActiveRecord where 子句
【发布时间】:2011-08-14 18:50:21
【问题描述】:

使用 Rails ActiveRecord 构造 where 子句的最佳方法是什么?例如,假设我有一个返回博客文章列表的控制器操作:

def index
  @posts = Post.all
end

现在,假设我希望能够传入一个 url 参数,以便此控制器操作仅返回特定作者的帖子:

def index
  author_id = params[:author_id]

  if author_id.nil?
    @posts = Post.all
  else
    @posts = Post.where("author = ?", author_id)
  end
end

这对我来说不是很干燥。如果我要添加排序或分页,或者更糟糕的是,更多可选的 URL 查询字符串参数来过滤,这个控制器操作会变得非常复杂。

【问题讨论】:

    标签: ruby-on-rails activerecord


    【解决方案1】:

    怎么样:

    def index
      author_id = params[:author_id]
    
      @posts = Post.scoped
    
      @post = @post.where(:author_id => author_id) if author_id.present?
    
      @post = @post.where(:some_other_condition => some_other_value) if some_other_value.present?
    end
    

    Post.scoped 本质上是一个延迟加载的等价于 Post.all(因为 Post.all 返回一个数组 立即,而 Post.scoped 只返回一个关系对象)。此查询将不会执行,直到 您实际上尝试在视图中迭代它(通过调用 .each)。

    【讨论】:

    【解决方案2】:

    嗯,您想使用的最佳方法是通过 2 个操作来传播它

    def index
       @post = Post.all
    end
    
    def get
      @post = Post.where("author=?", params[:author_id])
    end
    

    恕我直言,如果您考虑一个 RESTful API,索引意味着列出所有并获取(或显示)以获取请求的 API 并显示它!

    【讨论】:

    • 这无法扩展。如果现在我想显示特定作者的所有帖子,并且能够按发布日期和/或类别进一步缩小范围,该怎么办?另外,我认为“get”操作更常用于通过 id 查找资源。我希望“posts/1”返回一个 id == 1 的帖子,而不是 author_id == 1。
    • 确切地说,预期的 url 将是 /authors/1/posts。将作者视为资源。阅读本指南中的嵌套资源:guides.rubyonrails.org/routing.html(滚动至 2.7 - 嵌套资源)。
    【解决方案3】:

    这个问题已经很老了,但它在 2019 年仍然出现在谷歌中,而且一些早期的答案已被弃用,所以我想我会分享一个可能的解决方案。

    在模型中引入一些作用域,并测试传递的参数是否存在:

    class Post
        scope :where_author_ids, ->(ids){ where(author_id: ids.split(‘,’)) if ids }
        scope :where_topic_ids,  ->(ids){ where(topic_id:  ids.split(‘,’)) if ids }
    

    然后在控制器中,您可以根据需要放入任意数量的过滤器,例如:

    def list
        @posts = Post.where_author_ids(params[:author_ids])
                     .where_topic_ids(params[:topic_ids])                                  
                     .where_other_condition_ids(params[:other_condition_ids])
                     .order(:created_at)
    

    然后参数可以是单个值或逗号分隔的值列表,两者都可以正常工作。

    如果一个参数不存在,它只是跳过那个 where 子句并且不过滤那个特定的条件。如果参数存在但其值为空字符串,则它将“过滤掉”所有内容。

    当然,此解决方案并不适合所有情况。如果您有一个带有多个过滤器的视图页面,但在第一次打开时您想要显示所有数据而不是没有数据,直到您按下“提交”按钮或类似按钮(如该控制器那样),那么您将不得不稍微调整它.

    我已经尝试过使用 SQL 注入它,而且据我所知,rails 似乎可以很好地保证一切安全。

    【讨论】:

      【解决方案4】:

      您应该使用嵌套资源对 url 进行建模。预期的 url 是 /authors/1/posts。将作者视为资源。阅读本指南中的嵌套资源:http://guides.rubyonrails.org/routing.html(滚动至 2.7 - 嵌套资源)。

      【讨论】:

      • 如果我想选择性地按作者、创建日期和类别进行过滤怎么办?随着条件数量的增加,我将很快达到需要有条件地附加到 where 子句的地步。我不需要创建控制器操作来处理可选条件的每种组合。一定有比这更好、更具可扩展性的方法。
      • 是的,在某些时候您将使用简单的参数进行过滤。 url 允许识别请求的对象,但可以使用参数进行过滤。不要将嵌套推得太远!找到适当的平衡并不总是一件容易的事。
      • 你基本上是说在某个时候我会回到我遇到的同样的问题上。在这一点上,我的问题仍然存在。如何根据某些 URL 参数的存在动态构造查询?
      【解决方案5】:

      这样的东西有用吗?

      def get
        raise "Bad parameters...why are you doing this?" unless params[:filter].is_a?(Hash)
        @post = Post.where(params[:filter])
      end
      

      然后您可以执行以下操作: ?filter[author_id]=1&filter[post_date]=...等

      【讨论】:

      • 那不是 SQL 注入漏洞吗?
      • 怎么样?创建 where 条件时会过滤键,并且您使用变量进行插入,与“author_id = ?”不同。查看api.rubyonrails.org/classes/ActiveRecord/Base.html
      • 因为您从查询字符串中获取 where 子句。根据 ActiveRecord 文档的“条件”部分:“字符串形式可用于不涉及受污染数据的语句”。这意味着如果您从字符串而不是数组或哈希构造 where 子句,您的输入将不会被清理。使用您的解决方案,恶意用户可以制作类似“posts?filter[author_id]=1;drop table posts;”的 URL它会删除我的“帖子”表,对吗?
      • 即字符串形式,我们使用的是哈希形式,与上例中的数组形式一样受到保护。 “当条件输入被污染并需要清理时,将使用数组形式。字符串形式可用于不涉及污染数据的语句。哈希形式的工作原理与数组形式非常相似,除了相等和范围是可能的”。 Se 应该仔细检查以确保 params[:filter] 是一个散列。编辑了我的答案以显示这一点。
      • 这是一种有趣的方法,但我仍然担心将任意(如果安全的话)where 子句传递给数据库。用户可以限制没有索引的字段导致过多的数据库负载,并通过查看哪些参数导致错误来猜测您的表模式。我想可以通过 params[:filter].slice('author_id', 'post_date') 来实现白名单。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多