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

时间:2017-12-05 20:59:37

标签: ruby-on-rails ruby activerecord associations rails-activerecord

是否可以将方法委托给rails中的has_many关联,并且仍然保存该关联上的预加载数据,同时遵循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

注意: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

注意:当我调用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 

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

3 个答案:

答案 0 :(得分:4)

<强>解释

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

此时,所有关注者都会问一个问题,为什么在OP提供的第二个示例中调用NoMethodError时没有引发user.all_have_title?异常。这背后的原因在ActiveRecord::Associations::CollectionProxy文档(这是user.blogs调用的结果对象类)中详细阐述,由于我们的示例namings状态而改述:

  

user.blogs中的关联代理将user中的对象设为@owner,将blogs收集为@target,{{1} object表示@reflection宏   此类通过:has_many将未知方法委派给@target

所以发生的事情的顺序如下:

  1. method_missing在初始化delegate模型的all_have_title?范围内定义has_many实例方法;
  2. 调用User user时,方法被委托给all_have_title?协会;
  3. 由于此处未定义此类方法,因此通过has_many委托给Blog class all_have_title?方法;
  4. all方法在method_missing上调用Blog,其中包含current_scope条件user_id此时持有scoped_attributes值),所以没有关于预加载的信息,因为基本上发生的是:

    {"user_id"=>1}

    分别为每个Blog.where(user_id: 1) ,这是与之前执行的预加载相比的主要差异,它使用user按多个值查询相关记录,但此处执行的一个查询单个记录使用in(这就是为什么查询本身甚至没有在这两个调用之间缓存的原因)。

  5. <强>解决方案

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

    has_many

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

    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
    

    这种方式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 不会隐含地使用User的属性进行操作,并且您不会丢失预先加载的数据。如果您不希望直接使用Blog属性操作关联方法(title方法中的块),您可以在all模型中定义实例方法并定义其中的所有逻辑:

    Blog

答案 1 :(得分:3)

这是一个符合demeter法则的解决方案,并且尊重预加载的数据(不会再次点击数据库)。这当然有点奇怪,但我找不到任何其他解决方案,我真的想知道其他人对此的看法:

<强>模型

class User < ApplicationRecord     
  has_many :blogs

  def all_blogs_have_title?
    blogs.all_have_title_present?(self)
  end
end

class Blog < ApplicationRecord
  belongs_to :user

  def self.all_have_title_present?(user)
    user.blogs.any? && user.blogs.all? {|blog| blog.title.present?}
  end
end

<强>用法

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 

所以我们注意到它没有再次访问数据库(尊重预加载的数据),而不是user到达它的邻居(Blog)的属性,它将问题委托给它的邻居并允许邻居(再次:Blog)回答关于它自己的属性的问题。

奇怪的是,在Blog模型中,类方法会询问user.blogs,因此Blog知道User上的关联。但也许这没关系,因为毕竟BlogUser彼此之间有着共同关系。

答案 2 :(得分:0)

scope_delegation gem将完成您的工作。

如果你要像这样定义你的作品

WebElement element = driver.findElement(By.id("gbqfd"));
JavascriptExecutor executor = (JavascriptExecutor)driver;
executor.executeScript("arguments[0].click();", element);

这应该有效:)