克隆 rails 中的关系

Cloning a relation in rails

我在升级到 rails 4.2.1 时遇到弃用错误,Modifying already cached Relation. The cache will be reset. Use a cloned Relation to prevent this warning.

我正在尝试的操作 运行 按月获取登录用户数。

我的测试很简单:

get :page
expect(response).to be_success

控制器动作:

def page
  @months = {}
  (0..11).each do |month|
     @months[month] = User.group(:usergroup).number_first_logged_in(Date.new(Date.today.year,month+1, 1))
  end
end

用户模型

class Model < ActiveRecord::Base
   ...
   def number_first_logged_in(month)
     where(first_logged_in_at: month.beginning_of_month..month.end_of_month).count
   end
end

我意识到我 运行几乎相同的查询 12 次,但参数不同。当用户未分组时,此方法在其他地方使用。我怎样才能 'clone' 弃用警告中建议的关系?

我不想简单地忽略它,因为它在 运行 宁测试时填满了我的屏幕,这不是很有帮助

简答:

Ignore it. The deprecation message has been removed from the master branch of rails. Rails 5 will not be complaining about it.

长答案:

以下 issues/commits 似乎是弃用消息被错误添加并已被删除的印象:

可能值得在 rails 的主分支上尝试您的代码。

已编辑

首先也是最重要的您的代码有效。它不会在 rails 4.2.1 上引发 ImmutableRelation 或显示弃用消息 运行。

页面操作上的代码不需要克隆关系,因为它会在每个月的步骤中创建一个新关系(使用:User...)。是的,它执行 12 个查询,但这对您来说不是问题。

不是很重要,但是你的问题有两个错别字。您的模型必须将 def number_first_logged_in(month) 更改为 def self.number_first_logged_in(month) 并且模型名称必须是 User 而不是 Model.

我在 rails 控制台测试它(在 Products 而不是 User,并使用 :created_at 而不是 :first_logged_in_at 字段),但它是一样,工作正常。而且我很确定,如果您开始一个新的 rails 4.2.1 应用程序并使用问题处的代码(修正拼写错误),它将起作用。

alejandro@work-one [ruby-2.1.1@rails42]: ~/rails/r42example 
[09:14:04] $ rails c
Loading development environment (Rails 4.2.1)
~/rails/r42example (development) > @m = {};(0..11).each {|m| @m[m] = Product.group(:name).number_first_logged_in(Date.new(Date.today.year,m+1, 1)) }
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-01-01' AND '2015-01-31') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-02-01' AND '2015-02-28') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-03-01' AND '2015-03-31') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-04-01' AND '2015-04-30') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-05-01' AND '2015-05-31') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-06-01' AND '2015-06-30') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-07-01' AND '2015-07-31') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-08-01' AND '2015-08-31') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-09-01' AND '2015-09-30') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-10-01' AND '2015-10-31') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-11-01' AND '2015-11-30') GROUP BY "products"."name"
   (0.1ms)  SELECT COUNT(*) AS count_all, name AS name FROM "products" WHERE ("products"."created_at" BETWEEN '2015-12-01' AND '2015-12-31') GROUP BY "products"."name"
=> 0..11

但是,你有一个问题。此问题与 public rails API 无关,因为它没有可能引发这些错误或弃用的方法。 Rails 团队表示任何 #nodoc public 方法都是 而不是 public API 的一部分。许多查询方法(如果不是全部的话)都有一个 pair bang 方法(code),它修改关系(而不是 return 克隆)并引发 InmutableRelation 错误(或弃用以前版本的消息)。这些 public 方法是 #nodoc,不属于 public rails API。

怎么办?不容易:

  1. 在您的代码中搜索这些 bang 方法,也许是在 AR 上完成的猴子补丁。
  2. 检查您正在使用的宝石。也许,启动一个代码正常运行的新应用程序,并添加您在目标应用程序中拥有的所有 gem,如果失败,反复试验删除 gem,直到它再次运行。

我指的是 bang 方法,但是修改关系的方法也是如此(这不能由 public API 完成)。你必须寻找猴子补丁或扩展关系。

这一定足以解决问题。

我看了你的评论,我明白了,我认为这些选项:

  • Rails 数据库独立性通过 arel 实现。而arel没有 直接使用数据库函数(日期字段的月份)的方法。 您可以扩展 arel 并编写它,但是您需要编写一个用于 PostgreSql 一个用于 MySql 另一个用于 Sqlite。 (太贵了,在这个 点)

  • 如果您在 dev/test/prod 上使用相同的数据库管理器,您可以使用部分 按照我的建议进行文本查询。 (这不喜欢你)

  • 保留 12 个查询(我想你可以接受这个)

  • 将专用字段添加到 group_by(year_month 可以)。 (非常严格, 很难改变)

我保留旧答案,因为:如果我是你,我会这样做:

class User
  scope :for_current_year, -> { where(created_at: Date.today.beginning_of_year..Date.today.end_of_year }
end

在控制器页面操作上你可以使用:(我建议使用那个)

User.for_current_year
  .group("date_trunc('month', users.created_at)", "usergroup").count

其中 return 具有此模式的散列:(更多计数 here

{
  [<first date of the month of created_at>, <usergroup>] => count,
  ...
}

但是如果你想得到与之前相同的 @months,你必须将结果映射到 ruby。

def page
  @months = User.for_current_year
    .group("date_trunc('month', users.created_at)", "usergroup").count
    .map { |k,v| {k[0].month => {k[1] => v}} }
end

注意 1:此代码适用于 PostgreSQL,因为它使用函数 date_trunc(...),如果您需要与 MySql 一起使用,则需要使用 month(users.created_at)。使用 MySql 映射时,您需要使用 k[0] 而不是 k[0].month.

注意 2:group 调用已为其字段分隔参数,因为您希望 returned 散列的键上有两个值。

我看到两个可能的解决方案:

您可以清除查询之间的缓存: ActiveRecord::Base.connection.query_cache.clear 来自 Clearing active Record Cache

您可以在操作之前使用 .dup 克隆关系,这样您就不会使缓存失效。

在我的例子中,是 squeel gem 产生了弃用警告。一个简单的 monkeypatch 修复了警告。

module Squeel
  module Adapters
    module ActiveRecord
      module RelationExtensions

        def execute_grouped_calculation(operation, column_name, distinct)
          super
        end

      end
    end
  end
end

我不确定它是否会破坏 squeel 行为,但它对我有用。 Rails 4.2.x 与 squeel 的结合似乎是个问题。我还将其推送到 squeel 问题跟踪器 https://github.com/activerecord-hackery/squeel/issues/374