web-dev-qa-db-ja.com

Rails複数の外部キーとの関連付け

1つのテーブルで2つの列を使用してリレーションシップを定義できるようにしたい。そのため、タスクアプリを例として使用します。

試み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
_

それでTask.create(owner_id:1, assignee_id: 2)

これにより、ser oneを返す_Task.first.owner_とser twoを返す_Task.first.assignee_を実行できますが、_User.first.task_は何も返しません。これは、タスクがユーザーに属しておらず、ownerおよびassigneeに属しているためです。そう、

試み2:

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

class Task < ActiveRecord::Base
  belongs_to :user
end
_

2つの外部キーがサポートされていないように見えるため、これは完全に失敗します。

したがって、私が望むのは_User.tasks_と言い、ユーザーが所有するタスクと割り当てられたタスクの両方を取得できるようにすることです。

基本的に、何らかの方法でTask.where(owner_id || assignee_id == 1)のクエリと同等の関係を構築します

それは可能ですか?

更新

_Finder_sql_を使用するつもりはありませんが、この問題の受け入れられない答えは、私が望むものに近いようです: Rails-Multiple Index Key Association

したがって、このメソッドは次のようになります。

試み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
_

_Rails 4_で動作させることはできますが、次のエラーが発生し続けます。

_ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
_
71
JonathanSimmons

TL; DR

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

Userクラスのhas_many :tasksを削除します。


テーブルtaskshas_many :tasksという名前の列がないため、user_idを使用しても意味がありません。

私の場合の問題を解決するために私がしたことは:

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

このようにして、User.first.assigned_tasksUser.first.owned_tasksを呼び出すことができます。

これで、assigned_tasksowned_tasksの組み合わせを返すtasksというメソッドを定義できます。

読みやすさに関してはこれは良い解決策かもしれませんが、パフォーマンスの観点からは、tasksを取得するために、1回ではなく2つのクエリが発行されます。 、その後、これら2つのクエリの結果も結合する必要があります。

したがって、ユーザーに属するタスクを取得するには、次の方法でtasksクラスのカスタムUserメソッドを定義します。

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

この方法では、1つのクエリですべての結果を取得し、結果をマージまたは結合する必要はありません。

73
Arslan Ali

上記の@ dre-hhの回答を拡張すると、Rails 5で期待どおりに動作しなくなったことがわかりました。Rails 5には、WHERE tasks.user_id = ?、このシナリオにはuser_id列がないため失敗します。

has_manyアソシエーションで動作させることはまだ可能であることがわかりました。Railsによって追加されたこの追加のwhere句のスコープを解除する必要があります。

class User < ApplicationRecord
  has_many :tasks, ->(user) { unscope(:where).where("owner_id = :id OR assignee_id = :id", id: user.id) }
end
27
Dwight

Rails 5:

has_manyの関連付けが必要な場合は、デフォルトのwhere句で@Dwightの回答を参照する必要があります。

User.joins(:tasks)は私に

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

もはや不可能なので、@ Arslan ALiソリューションも使用できます。

Rails 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

Update1:​​@JonathanSimmonsコメントについて

UserオブジェクトをUserモデルのスコープに渡す必要があるのは、逆向きのアプローチのようです

ユーザーモデルをこのスコープに渡す必要はありません。現在のユーザーインスタンスは、このラムダに自動的に渡されます。次のように呼び出します。

user = User.find(9001)
user.tasks

Update2:

可能であれば、この答えを展開して何が起こっているのか説明してもらえますか?同様のことを実装できるように、よりよく理解したいと思います。ありがとう

ActiveRecordクラスでhas_many :tasksを呼び出すと、ラムダ関数がクラス変数に格納され、このラムダを呼び出すオブジェクトでtasksメソッドを生成するための単なる便利な方法になります。生成されたメソッドは、次の擬似コードのようになります。

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
24
dre-hh

私はこれに対する解決策を考え出しました。私はこれをどのように改善できるかについてのポインタを受け入れています。

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

これは基本的にhas_manyアソシエーションをオーバーライドしますが、ActiveRecord::Relation探していたオブジェクト。

だから今、私はこのようなことをすることができます:

User.first.tasks.completedそして、結果はすべて所有されているか、最初のユーザーに割り当てられた完了したタスクです。

13
JonathanSimmons

Associations and(multiple)foreign key in Rails(3.2):モデルでそれらを記述し、マイグレーションを記述する方法 に対する私の答えはあなたのためです!

あなたのコードについては、ここに私の修正があります

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

警告:RailsAdminを使用していて、新しいレコードを作成したり、既存のレコードを編集する必要がある場合は、私が提案したことをしないでください。このようなことをすると問題が発生します。

current_user.tasks.build(params)

理由は、Railsはcurrent_user.idを使用してtask.user_idを埋めようとしますが、user_idのようなものがないことを見つけるためだけです。

ですから、私のハック方法を箱の外の方法と考えてください。しかし、そうしないでください。

1
sunsoft