将方法委托给 has_many 关联会忽略预加载
Delegating method to has_many association ignores preloading
是否可以将方法委托给 rails 中的 has_many
关联,并且仍将预加载的数据保存在该关联上,同时遵循得墨忒耳法则?目前在我看来,你被迫选择一个或另一个。即:通过不委托来保留预加载的数据,或者丢失预加载的数据并进行委托。
例子:我有以下两个模型:
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
属性,这违反了得墨忒耳法则。
应用得墨忒耳定律但丢失预加载数据的其他方式:
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
希望这两种实现的缺点是显而易见的。理想情况下:我想用委托实现的第二种方式来做,但要维护预加载的数据。这可能吗?
这是一个遵循得墨忒耳法则并尊重预加载数据的解决方案(不会再次访问数据库)。这当然有点奇怪,但我找不到任何其他解决方案,我真的很想知道其他人对此有何看法:
型号
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
模型内部,其中 class 方法询问 user.blogs
,因此 Blog
知道 User
上的关联。但这也许没关系,因为毕竟 Blog
和 User
彼此共享关联。
说明
all_have_title?
委托在您的示例中无法正常工作的原因是您将方法委托给 blogs
关联,但仍将其定义为 Blog
class 方法,它们是不同的实体,因此是接收者。
到这里大家一定会问为什么在OP提供的第二个例子中调用user.all_have_title?
时没有出现NoMethodError
异常。这背后的原因在 ActiveRecord::Associations::CollectionProxy
文档(这是 user.blogs
调用的结果对象 class)中进行了详细说明,由于我们的示例命名而改写为:
that the association proxy in user.blogs
has the object in user
as @owner
, the collection of his blogs
as @target
, and the @reflection
object represents a :has_many
macro.
This class delegates unknown methods to @target
via method_missing
.
所以事情发生的顺序如下:
delegate
在 User
初始化模型的 has_many
范围内定义 all_have_title?
实例方法;
- 在
user
上调用时 all_have_title?
方法被委托给 has_many
关联;
- 因为那里没有定义这样的方法,所以它通过
method_missing
; 委托给 Blog
class all_have_title?
方法
all
方法在 Blog
上调用,current_scope
保持 user_id
条件(此时 scoped_attributes
保持 {"user_id"=>1}
值),所以没有关于预加载的信息,因为基本上发生的事情是:
Blog.where(user_id: 1)
分别为每个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
因此您所做的调用应该只会导致以下 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
这种方式 User
不会隐式操作 Blog
的属性,您也不会丢失预加载的数据。如果您不希望关联方法直接与 title
属性一起操作(在 all
方法中阻塞),您可以在 Blog
模型中定义一个实例方法并在那里定义所有逻辑:
class Blog
def has_title?
title.present?
end
end
这个 scope_delegation gem 会完成你的工作。
如果你会这样定义你的作品
class User < ApplicationRecord
has_many :blogs
delegate :all_have_title?, to: :blogs, prefix: false, allow_nil: false
end
class Blog < ApplicationRecord
belongs_to :user
def self.all_have_title?
where.not(title: nil)
end
end
这应该有效:)
是否可以将方法委托给 rails 中的 has_many
关联,并且仍将预加载的数据保存在该关联上,同时遵循得墨忒耳法则?目前在我看来,你被迫选择一个或另一个。即:通过不委托来保留预加载的数据,或者丢失预加载的数据并进行委托。
例子:我有以下两个模型:
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
属性,这违反了得墨忒耳法则。
应用得墨忒耳定律但丢失预加载数据的其他方式:
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
希望这两种实现的缺点是显而易见的。理想情况下:我想用委托实现的第二种方式来做,但要维护预加载的数据。这可能吗?
这是一个遵循得墨忒耳法则并尊重预加载数据的解决方案(不会再次访问数据库)。这当然有点奇怪,但我找不到任何其他解决方案,我真的很想知道其他人对此有何看法:
型号
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
模型内部,其中 class 方法询问 user.blogs
,因此 Blog
知道 User
上的关联。但这也许没关系,因为毕竟 Blog
和 User
彼此共享关联。
说明
all_have_title?
委托在您的示例中无法正常工作的原因是您将方法委托给 blogs
关联,但仍将其定义为 Blog
class 方法,它们是不同的实体,因此是接收者。
到这里大家一定会问为什么在OP提供的第二个例子中调用user.all_have_title?
时没有出现NoMethodError
异常。这背后的原因在 ActiveRecord::Associations::CollectionProxy
文档(这是 user.blogs
调用的结果对象 class)中进行了详细说明,由于我们的示例命名而改写为:
that the association proxy in
user.blogs
has the object inuser
as@owner
, the collection of hisblogs
as@target
, and the@reflection
object represents a:has_many
macro.
This class delegates unknown methods to@target
viamethod_missing
.
所以事情发生的顺序如下:
delegate
在User
初始化模型的has_many
范围内定义all_have_title?
实例方法;- 在
user
上调用时all_have_title?
方法被委托给has_many
关联; - 因为那里没有定义这样的方法,所以它通过
method_missing
; 委托给 all
方法在Blog
上调用,current_scope
保持user_id
条件(此时scoped_attributes
保持{"user_id"=>1}
值),所以没有关于预加载的信息,因为基本上发生的事情是:Blog.where(user_id: 1)
分别为每个
user
,这是与之前执行的预加载的主要区别,后者使用in
按多个值查询关联记录,但这里执行的是查询一个=
的单条记录(这就是为什么在这两个调用之间甚至没有缓存查询本身的原因)。
Blog
class all_have_title?
方法
解决方案
要显式封装该方法并将其标记为基于关系(介于 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
因此您所做的调用应该只会导致以下 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
这种方式 User
不会隐式操作 Blog
的属性,您也不会丢失预加载的数据。如果您不希望关联方法直接与 title
属性一起操作(在 all
方法中阻塞),您可以在 Blog
模型中定义一个实例方法并在那里定义所有逻辑:
class Blog
def has_title?
title.present?
end
end
这个 scope_delegation gem 会完成你的工作。
如果你会这样定义你的作品
class User < ApplicationRecord
has_many :blogs
delegate :all_have_title?, to: :blogs, prefix: false, allow_nil: false
end
class Blog < ApplicationRecord
belongs_to :user
def self.all_have_title?
where.not(title: nil)
end
end
这应该有效:)