Nei*_*eil 10 ruby activerecord ruby-on-rails associations rails-activerecord
是否可以将一个方法委托给has_many
rails中的关联,并且仍然按照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)
希望两种实现方式的缺点都很明显.理想情况下:我希望以委托实现的第二种方式实现,但要维护预加载的数据.这可能吗?
解释
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
宏。
此类将未知方法委托给@target
viamethod_missing
。
因此,发生的事情的顺序如下:
delegate
在初始化时在模型的范围内定义all_have_title?
实例方法;has_many
User
user
all_have_title?
方法被委托给has_many
关联时;Blog
类all_have_title?
方法通过method_missing
;all
方法被调用Blog
,current_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)
归档时间: |
|
查看次数: |
816 次 |
最近记录: |