10

Có thể ủy quyền một phương thức cho một hiệp hội has_many trong đường ray, VÀ vẫn lưu dữ liệu đã tải trước vào liên kết đó, tất cả trong khi tuân theo luật của demeter? Hiện tại có vẻ như với tôi rằng bạn buộc phải chọn cái này hay cái kia. Tức là: giữ dữ liệu được tải sẵn của bạn bằng cách KHÔNG ủy quyền hoặc mất dữ liệu được tải trước của bạn và ủy quyền.Phương thức ủy nhiệm cho liên kết has_many bỏ qua việc nạp trước

Ví dụ: Tôi có hai mô hình sau:

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 

Chú ý: đó User#all_blogs_have_title? làm điều chính xác giống như các phương pháp phân cấp all_have_title?.

Sau đây, như tôi đã hiểu, vi phạm pháp luật về demeter. Tuy nhiên: nó duy trì dữ liệu được tải trước của bạn:

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 

Lưu ý: khi tôi gọi user.all_blogs_have_title? nó KHÔNG làm một truy vấn bổ sung. Tuy nhiên, lưu ý rằng phương thức all_blogs_have_title? hỏi về các thuộc tính Blog, vi phạm pháp luật về demeter.

cách khác mà áp dụng pháp luật của Demeter nhưng bạn bị mất dữ liệu tải trước:

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 

Hy vọng rằng các nhược điểm của cả hai hiện thực là rõ ràng. Lý tưởng nhất: Tôi muốn làm điều đó theo cách thứ hai với việc thực hiện ủy nhiệm, nhưng để duy trì dữ liệu được tải sẵn đó. Điều này có thể không?

Trả lời

4

Giải thích

Lý do tại sao all_have_title? đoàn không hoạt động đúng trong ví dụ của bạn là của bạn được ủy thác phương pháp để blogs hiệp hội, nhưng chưa xác định nó như là một phương pháp Blog lớp, đó là các đơn vị khác nhau và do đó người nhận.

Tại thời điểm này, mọi người sau đây sẽ đặt câu hỏi tại sao không có NoMethodError ngoại lệ khi gọi user.all_have_title? trong ví dụ thứ hai do OP cung cấp. Lý do đằng sau này được xây dựng trong các tài liệu ActiveRecord::Associations::CollectionProxy (đó là kết quả lớp đối tượng của user.blogs cuộc gọi), mà rephrasing do dụ namings của chúng tôi khẳng định:

rằng proxy hiệp hội trong user.blogs có các đối tượng trong user như @owner, bộ sưu tập của mình là blogs@target và đối tượng @reflection đại diện cho một macro :has_many.
Lớp này ủy quyền các phương thức không xác định cho @target qua method_missing.

Vì vậy, thứ tự của điều đang xảy ra như sau:

  1. delegate xác định phương pháp all_have_title? dụ trong has_many phạm vi trong User mô hình trên khởi tạo;
  2. khi được gọi theo phương thức userall_have_title? được ủy quyền cho liên kết has_many;
  3. vì không có phương pháp nào được xác định ở đó được ủy quyền cho Blog phương thức all_have_title? qua method_missing;
  4. all phương pháp được gọi là trên Blog với current_scope nắm giữ user_id điều kiện (scoped_attributes vào thời điểm này đang nắm giữ {"user_id"=>1} giá trị), vì vậy không có thông tin về gia tải, bởi vì về cơ bản những gì đang xảy ra là:

    Blog.where(user_id: 1) 
    

    cho mỗi user riêng biệt, đó là sự khác biệt chính so với tải trước được thực hiện trước đó, truy vấn các bản ghi được liên kết bằng nhiều giá trị sử dụng in, nhưng thực hiện tại đây truy vấn một bản ghi với = (đây là lý do tại sao bản thân truy vấn thậm chí không được lưu trữ giữa hai cuộc gọi này).

Giải pháp

Để cả hai đóng gói phương pháp này một cách rõ ràng và đánh dấu nó như là một mối quan hệ dựa trên (giữa UserBlog), bạn nên xác định và mô tả nó là logic trong phạm vi has_many hiệp hội:

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 

Do đó, cuộc gọi bạn thực hiện sẽ chỉ dẫn đến 2 truy vấn sau:

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 

theo cách này User không hoạt động ngầm với thuộc tính của Blog và bạn không làm mất dữ liệu được tải sẵn.Nếu bạn không muốn các phương pháp liên kết hoạt động với title thuộc tính trực tiếp (khối trong phương pháp all), bạn có thể xác định một phương pháp dụ trong Blog mô hình và xác định tất cả các logic có:

class Blog 
    def has_title? 
    title.present? 
    end 
end 
3

Dưới đây là giải pháp tuân theo luật demeter VÀ tôn trọng dữ liệu được tải trước (không nhấn lại vào cơ sở dữ liệu). Nó chắc chắn là một chút kỳ quặc, nhưng tôi không thể tìm thấy bất kỳ giải pháp khác, và tôi thực sự muốn biết những gì người khác nghĩ về điều này:

Models

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 

Cách sử dụng

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 

Vì vậy, chúng tôi nhận thấy rằng nó không phải là nhấn cơ sở dữ liệu một lần nữa (tôn vinh các dữ liệu tải sẵn), và thay vì user tiếp cận các thuộc tính của hàng xóm của nó (Blog), nó đại biểu các câu hỏi cho người hàng xóm của nó và cho phép hàng xóm (một lần nữa: Blog) để trả lời các câu hỏi về thuộc tính riêng của nó.

Điều kỳ lạ rõ ràng là bên trong mô hình Blog nơi phương thức lớp yêu cầu user.blogs, vì vậy Blog biết về liên kết trên User. Nhưng có lẽ điều này là ok bởi vì, sau khi tất cả, BlogUser chia sẻ liên kết với nhau.

0

scope_delegation gem này sẽ làm công việc của bạn .

nếu bạn sẽ xác định công việc của bạn như thế này

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 

này nên làm việc :)

Các vấn đề liên quan