Liên kết đường ray với nhiều khóa ngoại


84

Tôi muốn có thể sử dụng hai cột trên một bảng để xác định mối quan hệ. Vì vậy, sử dụng một ứng dụng tác vụ làm ví dụ.

Nỗ lực 1:

class User < ActiveRecord::Base
  has_many :tasks
end

class Task < ActiveRecord::Base
  belongs_to :owner, class_name: "User", foreign_key: "owner_id"
  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end

Vậy thì Task.create(owner_id:1, assignee_id: 2)

Điều này cho phép tôi thực hiện Task.first.ownertrả về người dùng mộtTask.first.assigneetrả về người dùng hai nhưng User.first.taskkhông trả lại gì. Đó là vì nhiệm vụ không thuộc về người dùng, chúng thuộc về chủ sở hữu và người được chuyển nhượng . Vì thế,

Nỗ lực 2:

class User < ActiveRecord::Base
  has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end

class Task < ActiveRecord::Base
  belongs_to :user
end

Điều đó hoàn toàn không thành công vì hai khóa ngoại dường như không được hỗ trợ.

Vì vậy, những gì tôi muốn là có thể nói User.tasksvà nhận được cả quyền sở hữu của người dùng và các nhiệm vụ được giao.

Về cơ bản bằng cách nào đó, xây dựng một mối quan hệ tương đương với một truy vấn Task.where(owner_id || assignee_id == 1)

Điều đó có thể không?

Cập nhật

Tôi không muốn sử dụng finder_sql, nhưng câu trả lời không được chấp nhận của vấn đề này có vẻ gần với những gì tôi muốn: Rails - Multiple Index Key Association

Vì vậy, phương pháp này sẽ giống như thế này,

Nỗ lực 3:

class Task < ActiveRecord::Base
  def self.by_person(person)
    where("assignee_id => :person_id OR owner_id => :person_id", :person_id => person.id
  end 
end

class Person < ActiveRecord::Base

  def tasks
    Task.by_person(self)
  end 
end

Mặc dù tôi có thể làm cho nó hoạt động Rails 4, tôi vẫn gặp lỗi sau:

ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id

2
Đây có phải là viên ngọc bạn đang tìm kiếm? github.com/composite-primary-keys/composite_primary_keys
mus

Cảm ơn về thông tin mus nhưng đây không phải là thứ tôi đang tìm kiếm. Tôi muốn một truy vấn cho một hoặc cột là một giá trị nhất định. Không phải là khóa chính tổng hợp.
JonathanSimmons

vâng, bản cập nhật làm cho nó rõ ràng. Quên về đá quý. Cả hai chúng tôi đều nghĩ rằng bạn chỉ muốn sử dụng một khóa chính đã soạn. Điều này ít nhất nên có thể bằng cách xác định phạm vi tùy chỉnh một mối quan hệ theo phạm vi. Kịch bản thú vị. Tôi sẽ có một cái nhìn một nó sau này
dre-hh

FWIW mục tiêu của tôi ở đây là nhận một nhiệm vụ cho người dùng nhất định và giữ lại định dạng ActiveRecord :: Relation để tôi có thể tiếp tục sử dụng phạm vi nhiệm vụ trên kết quả để tìm kiếm / lọc.
JonathanSimmons

Câu trả lời:


80

TL; DR

class User < ActiveRecord::Base
  def tasks
    Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
  end
end

Xóa has_many :taskstrong Userlớp.


Sử dụng has_many :taskskhông có ý nghĩa gì cả vì chúng ta không có bất kỳ cột nào được đặt tên user_idtrong bảng tasks.

Những gì tôi đã làm để giải quyết vấn đề trong trường hợp của mình là:

class User < ActiveRecord::Base
  has_many :owned_tasks,    class_name: "Task", foreign_key: "owner_id"
  has_many :assigned_tasks, class_name: "Task", foreign_key: "assignee_id"
end

class Task < ActiveRecord::Base
  belongs_to :owner,    class_name: "User", foreign_key: "owner_id"
  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
  # Mentioning `foreign_keys` is not necessary in this class, since
  # we've already mentioned `belongs_to :owner`, and Rails will anticipate
  # foreign_keys automatically. Thanks to @jeffdill2 for mentioning this thing 
  # in the comment.
end

Bằng cách này, bạn có thể gọi User.first.assigned_taskscũng như User.first.owned_tasks.

Bây giờ, bạn có thể xác định một phương thức được gọi là phương thức taskstrả về kết hợp của assigned_tasksowned_tasks.

Đó có thể là một giải pháp tốt khi khả năng đọc được tăng lên, nhưng từ quan điểm hiệu suất, nó sẽ không tốt như bây giờ, để có được tasks, hai truy vấn sẽ được đưa ra thay vì một lần, và sau đó, kết quả trong số hai truy vấn đó cũng cần được kết hợp với nhau.

Vì vậy, để có được các tác vụ thuộc về người dùng, chúng tôi sẽ xác định một tasksphương thức tùy chỉnh trong Userlớp theo cách sau:

def tasks
  Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end

Bằng cách này, nó sẽ tìm nạp tất cả các kết quả trong một truy vấn duy nhất và chúng tôi sẽ không phải hợp nhất hoặc kết hợp bất kỳ kết quả nào.


Giải pháp này là tuyệt vời! Nó ngụ ý một yêu cầu về quyền truy cập riêng biệt để gán, sở hữu và tất cả các nhiệm vụ tương ứng. Một cái gì đó trong một dự án lớn sẽ là tiên đoán tuyệt vời. Tuy nhiên, đó không phải là một yêu cầu trong trường hợp của tôi. Đối với has_many"không có ý nghĩa" Nếu bạn đọc câu trả lời được chấp nhận, bạn sẽ thấy rằng chúng tôi đã không sử dụng một has_manytuyên bố nào cả. Giả sử các yêu cầu của bạn phù hợp với nhu cầu của mọi khách truy cập khác là không hiệu quả, thì câu trả lời này có thể ít mang tính phán xét hơn.
JonathanSimmons

Điều đó đang được nói, tôi tin rằng đây là cách thiết lập dài hạn tốt hơn mà chúng tôi nên vận động cho những người gặp vấn đề này. Nếu bạn muốn sửa đổi câu trả lời của mình để bao gồm một ví dụ cơ bản về các mô hình khác và phương pháp người dùng để truy xuất tập hợp các nhiệm vụ kết hợp, tôi sẽ sửa lại nó làm câu trả lời đã chọn. Cảm ơn!
JonathanSimmons

Vâng tôi đồng ý với bạn. Sử dụng owned_tasksassigned_tasksnhận tất cả các nhiệm vụ sẽ có gợi ý về hiệu suất. Dù sao, tôi đã cập nhật câu trả lời của mình và đã bao gồm một phương thức trong Userlớp để nhận tất cả các liên kết tasks, nó sẽ tìm nạp kết quả trong một truy vấn và không cần hợp nhất / kết hợp.
Arslan Ali

2
Chỉ là FYI, các khóa ngoại được chỉ định trong Taskmô hình thực sự không cần thiết. Vì bạn có các belongs_tomối quan hệ được đặt tên :owner:assignee, theo mặc định, Rails sẽ giả định rằng các khóa ngoại được đặt tên owner_idassignee_idtương ứng.
jeffdill2

1
@ArslanAli không sao. :-)
jeffdill2 14/12/15

47

Mở rộng câu trả lời của @ dre-hh ở trên, mà tôi thấy không còn hoạt động như mong đợi trong Rails 5. Có vẻ như Rails 5 hiện bao gồm một mệnh đề where mặc định có hiệu lực WHERE tasks.user_id = ?, điều này không thành công vì không có user_idcột trong trường hợp này.

Tôi thấy vẫn có thể làm cho nó hoạt động với một has_manyliên kết, bạn chỉ cần bỏ qua mệnh đề where bổ sung này được thêm bởi Rails.

class User < ApplicationRecord
  has_many :tasks, ->(user) {
    unscope(:where).where(owner: user).or(where(assignee: user)
  }
end

Cảm ơn @Dwight, tôi chưa bao giờ nghĩ đến điều này!
Gabe Kopley

Không thể làm cho điều này hoạt động cho a belongs_to(trong đó cha mẹ không có id, vì vậy nó phải dựa trên PK nhiều cột). Nó chỉ nói rằng mối quan hệ là nil (và nhìn vào bảng điều khiển, tôi không thể thấy truy vấn lambda từng được thực thi).
fgblomqvist

@fgblomqvist Thuộc_to có một tùy chọn được gọi là: primary_key, chỉ định phương thức trả về khóa chính của đối tượng được liên kết được sử dụng cho liên kết. Theo mặc định, đây là id. Tôi nghĩ rằng điều này có thể giúp bạn.
Ahmed Kamal

@AhmedKamal Tôi không thấy điều đó hữu ích như thế nào?
fgblomqvist

24

Đường ray 5:

bạn cần bỏ hiển thị mệnh đề where mặc định, hãy xem câu trả lời @Dwight nếu bạn vẫn muốn có has_many liên kết.

Mặc dù User.joins(:tasks)cho tôi

ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.

Vì không thể nữa, bạn cũng có thể sử dụng giải pháp @Arslan Ali.

Đường ray 4:

class User < ActiveRecord::Base
  has_many :tasks, ->(user){ where("tasks.owner_id = :user_id OR tasks.assignee_id = :user_id", user_id: user.id) }
end

Cập nhật1: Về nhận xét @JonathanSimmons

Phải chuyển đối tượng người dùng vào phạm vi trên mô hình Người dùng có vẻ giống như một cách tiếp cận ngược

Bạn không cần phải chuyển mô hình người dùng đến phạm vi này. Phiên bản người dùng hiện tại được chuyển tự động đến lambda này. Gọi nó như thế này:

user = User.find(9001)
user.tasks

Cập nhật2:

nếu có thể, bạn có thể mở rộng câu trả lời này để giải thích những gì đang xảy ra? Tôi muốn hiểu rõ hơn về nó để có thể triển khai một cái gì đó tương tự. cảm ơn

Việc gọi has_many :taskstrên lớp ActiveRecord sẽ lưu trữ một hàm lambda trong một số biến lớp và chỉ là một cách ưa thích để tạo một tasksphương thức trên đối tượng của nó, phương thức này sẽ gọi lambda này. Phương thức được tạo sẽ tương tự như mã giả sau:

class User

  def tasks
   #define join query
   query = self.class.joins('tasks ON ...')
   #execute tasks_lambda on the query instance and pass self to the lambda
   query.instance_exec(self, self.class.tasks_lambda)
  end

end

1
rất tuyệt vời, ở khắp mọi nơi khác tôi nhìn người giữ cố gắng đề nghị sử dụng đá quý lỗi thời này cho phím composite
FireDragon

2
Nếu có thể, bạn có thể mở rộng câu trả lời này để giải thích những gì đang xảy ra? Tôi muốn hiểu rõ hơn về nó để có thể triển khai một cái gì đó tương tự. thanks
Stephen Lead

1
mặc dù điều này vẫn giữ được liên kết, nhưng nó gây ra các vấn đề khác. từ tài liệu: Lưu ý: Việc tham gia, tải nhanh và tải trước các liên kết này là không thể hoàn toàn. Các hoạt động này xảy ra trước khi tạo cá thể và phạm vi sẽ được gọi với đối số nil. Điều này có thể dẫn đến hành vi không mong muốn và không được dùng nữa. api.rubyonrails.org/classes/ActiveRecord/Associations/…
s2t2

1
Đây hứa hẹn nhưng với Rails 5 đầu lên vẫn sử dụng foreign_key với một ngoại hình wheretruy vấn thay vì chỉ sử dụng lambda trên
Mohamed El Mahallawy

1
Vâng, có vẻ như điều này không còn hoạt động trong Rails 5.
Dwight

13

Tôi đã tìm ra một giải pháp cho điều này. Tôi cởi mở với mọi ý kiến ​​về cách tôi có thể làm cho điều này tốt hơn.

class User < ActiveRecord::Base

  def tasks
    Task.by_person(self.id)
  end 
end

class Task < ActiveRecord::Base

  scope :completed, -> { where(completed: true) }   

  belongs_to :owner, class_name: "User", foreign_key: "owner_id"
  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"

  def self.by_person(user_id)
    where("owner_id = :person_id OR assignee_id = :person_id", person_id: user_id)
  end 
end

Về cơ bản, điều này ghi đè liên kết has_many nhưng vẫn trả về ActiveRecord::Relationđối tượng mà tôi đang tìm kiếm.

Vì vậy, bây giờ tôi có thể làm một cái gì đó như sau:

User.first.tasks.completed và kết quả là tất cả nhiệm vụ đã hoàn thành thuộc sở hữu hoặc được giao cho người dùng đầu tiên.


bạn vẫn đang sử dụng phương pháp này để giải quyết câu hỏi của bạn? Tôi đang ở trong cùng một con thuyền và đang tự hỏi liệu bạn đã học được một cách mới hay đây vẫn là lựa chọn tốt nhất.
Dan

Vẫn là lựa chọn tốt nhất mà tôi đã tìm thấy.
JonathanSimmons

Đó có thể là dấu tích của những nỗ lực cũ. Đã chỉnh sửa câu trả lời để xóa nó.
JonathanSimmons

1
Liệu câu trả lời này và câu trả lời của dre-hh dưới đây có đạt được điều tương tự không?
dkniffin

1
> Việc phải chuyển đối tượng người dùng vào phạm vi trên mô hình Người dùng có vẻ như là một cách tiếp cận ngược. Bạn không cần phải chuyển bất cứ thứ gì, đây là lambda và phiên bản người dùng hiện tại được chuyển tự động đến nó
dre-hh

2

Câu trả lời của tôi cho các Liên kết và (nhiều) khóa ngoại trong rails (3.2): cách mô tả chúng trong mô hình và viết các di chuyển chỉ dành cho bạn!

Đối với mã của bạn, đây là sửa đổi của tôi

class User < ActiveRecord::Base
  has_many :tasks, ->(user) { unscope(where: :user_id).where("owner_id = ? OR assignee_id = ?", user.id, user.id) }, class_name: 'Task'
end

class Task < ActiveRecord::Base
  belongs_to :owner, class_name: "User", foreign_key: "owner_id"
  belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end

Cảnh báo: Nếu bạn đang sử dụng RailsAdmin và cần tạo bản ghi mới hoặc chỉnh sửa bản ghi hiện có, vui lòng không làm theo những gì tôi đã đề xuất, vì bản hack này sẽ gây ra sự cố khi bạn làm điều gì đó như sau:

current_user.tasks.build(params)

Lý do là rails sẽ cố gắng sử dụng current_user.id để điền vào task.user_id, chỉ để thấy rằng không có gì giống như user_id.

Vì vậy, hãy coi phương pháp hack của tôi như một cách bên ngoài, nhưng đừng làm vậy.


Không chắc chắn câu trả lời này cung cấp bất cứ điều gì câu trả lời được phê duyệt chưa có. Hơn nữa, nó giống như một quảng cáo cho một câu hỏi khác.
JonathanSimmons

@JonathanSimmons Cảm ơn bạn. Tôi sẽ xóa câu trả lời này nếu nó không hoạt động như một quảng cáo.
sunoft

2

Kể từ Rails 5, bạn cũng có thể làm điều đó, đây là cách an toàn hơn cho ActiveRecord:

def tasks
  Task.where(owner: self).or(Task.where(assignee: self))
end
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.