委托has_many关联方法忽略预加载

Nei*_*eil 10 ruby activerecord ruby-on-rails associations rails-activerecord

是否可以将一个方法委托给has_manyrails中的关联,并且仍然按照demeter定律保存该关联上的预加载数据?目前在我看来,你被迫选择其中一个.即:通过NOT委托保留预加载的数据,或丢失预加载的数据和委托.

示例:我有以下两个模型:

class User < ApplicationRecord
  has_many :blogs

  delegate :all_have_title?, to: :blogs, prefix: false, allow_nil: false

  def all_blogs_have_title?
    blogs.all? {|blog| blog.title.present?}
  end
end


class Blog < ApplicationRecord
  belongs_to :user

  def self.all_have_title?
    all.all? {|blog| blog.title.present?}
  end
end
Run Code Online (Sandbox Code Playgroud)

注意:这User#all_blogs_have_title?与委托方法完全相同all_have_title?.

根据我的理解,以下内容违反了德米特定律.但是:它维护您的预加载数据:

user = User.includes(:blogs).first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Blog Load (0.1ms)  SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" = 1
  => #<User id: 1, name: "all yes", created_at: "2017-12-05 20:28:00", updated_at: "2017-12-05 20:28:00">

user.all_blogs_have_title?
 => true
Run Code Online (Sandbox Code Playgroud)

注意:当我调用user.all_blogs_have_title?它时 ,不要再做一个额外的查询.但是,请注意该方法all_blogs_have_title?询问Blog属性,这违反了demeter定律.

应用demeter定律的其他方法但丢失了预加载的数据:

user = User.includes(:blogs).first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Blog Load (0.1ms)  SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" = 1
  => #<User id: 1, name: "all yes", created_at: "2017-12-05 20:28:00", updated_at: "2017-12-05 20:28:00">

user.all_have_title?
  Blog Load (0.2ms)  SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" = ?  [["user_id", 1]]
  => true 
Run Code Online (Sandbox Code Playgroud)

希望两种实现方式的缺点都很明显.理想情况下:我希望以委托实现的第二种方式实现,但要维护预加载的数据.这可能吗?

pot*_*hin 5

解释

all_have_title?委托在您的示例中无法正常工作的原因是您将方法委托给blogs关联,但仍将其定义为Blog类方法,它们是不同的实体,因此是不同的接收者。

在这一点上,每个关注的人都会问一个问题,为什么NoMethodError在调用user.all_have_title?OP 提供的第二个示例时没有引发异常。这背后的原因在ActiveRecord::Associations::CollectionProxy文档(这是user.blogs调用的结果对象类)中详细说明,由于我们的示例命名状态而重新表述:

在该协会代理user.blogs按目标user@owner,他收集blogs@target,而@reflection对象表示:has_many宏。
此类将未知方法委托给@targetvia method_missing

因此,发生的事情的顺序如下:

  1. delegate在初始化时在模型的范围内定义all_have_title?实例方法;has_manyUser
  2. 当被调用的user all_have_title?方法被委托给has_many关联时;
  3. 因为那里没有定义这样的方法,所以它被委托给Blogall_have_title?方法通过method_missing;
  4. all方法被调用Blogcurrent_scope它保持user_id条件(scoped_attributes此时是保持{"user_id"=>1}值),所以没有关于预加载的信息,因为基本上发生的是:

    Blog.where(user_id: 1)
    
    Run Code Online (Sandbox Code Playgroud)

    对于每个user单独的,这是与之前执行的预加载相比的关键区别,该预加载使用多个值查询关联记录in,但此处执行的查询使用单个记录=(这就是查询本身不是偶数的原因在这两个调用之间缓存)。

解决方案

要显式封装方法并将其标记为基于关系(在User和之间Blog),您应该在has_many关联范围内定义和描述它的逻辑:

class User
  delegate :all_have_title?, to: :blogs, prefix: false, allow_nil: false

  has_many :blogs do
    def all_have_title?
      all? { |blog| blog.title.present? }
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

因此,您所做的调用应仅导致以下 2 个查询:

user = User.includes(:blogs).first
=> #<User:0x00007f9ace1067e0
  User Load (0.8ms)  SELECT  `users`.* FROM `users`  ORDER BY `users`.`id` ASC LIMIT 1
  Blog Load (1.4ms)  SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`user_id` IN (1)
user.all_have_title?
=> true
Run Code Online (Sandbox Code Playgroud)

这种方式User不会隐式操作Blog的属性,并且不会丢失预加载的数据。如果您不想title直接使用属性操作关联方法(all方法中的块),您可以在Blog模型中定义一个实例方法并在那里定义所有逻辑:

class Blog
  def has_title?
    title.present?
  end
end
Run Code Online (Sandbox Code Playgroud)